Getting the popover Application working on the most superficial level, and creating a generic popover mixin

This commit is contained in:
Oliver-Akins 2025-03-13 23:36:25 -06:00
parent af5cf4acd5
commit 8e8202f8a6
7 changed files with 244 additions and 12 deletions

View file

@ -1,9 +1,10 @@
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
import { documentSorter, filePath } from "../../consts.mjs"; import { documentSorter, filePath, getTooltipDelay } from "../../consts.mjs";
import { gameTerms } from "../../gameTerms.mjs"; import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericAppMixin } from "../GenericApp.mjs";
import { localizer } from "../../utils/Localizer.mjs"; import { localizer } from "../../utils/Localizer.mjs";
import { Logger } from "../../utils/Logger.mjs"; import { Logger } from "../../utils/Logger.mjs";
import { AmmoTracker } from "../popovers/AmmoTracker.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ActorSheetV2 } = foundry.applications.sheets; const { ActorSheetV2 } = foundry.applications.sheets;
@ -39,16 +40,19 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
}; };
// #endregion // #endregion
// #region Instance Data
/** @type {number | undefined} */
#ammoTrackerHoverTimeout = null;
/** @type {AmmoTracker | null} */
#ammoTracker = null;
// #endregion
// #region Lifecycle // #region Lifecycle
async _onRender(context, options) { async _onRender(context, options) {
await super._onRender(context, options); await super._onRender(context, options);
HeroSkillsCardV1._onRender.bind(this)(context, options); HeroSkillsCardV1._onRender.bind(this)(context, options);
await this.#createAmmoTrackerEvents();
const ammo = this.element.querySelector(`.ammo`);
ammo.addEventListener(`mouseenter`, () => {console.log(`mouseenter-ing`)});
ammo.addEventListener(`contextmenu`, () => {console.log(`right-clicking`)});
}; };
static async _onRender(_context, options) { static async _onRender(_context, options) {
@ -81,6 +85,13 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
); );
}; };
async #createAmmoTrackerEvents() {
const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`);
ammoInfoIcon.addEventListener(`pointerenter`, this.#ammoInfoPointerEnter.bind(this));
ammoInfoIcon.addEventListener(`pointerout`, this.#ammoInfoPointerOut.bind(this));
ammoInfoIcon.addEventListener(`click`, this.#ammoInfoClick.bind(this));
};
async _preparePartContext(partId, ctx, opts) { async _preparePartContext(partId, ctx, opts) {
ctx = await super._preparePartContext(partId, ctx, opts); ctx = await super._preparePartContext(partId, ctx, opts);
ctx.actor = this.document; ctx.actor = this.document;
@ -177,6 +188,44 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
}; };
// #endregion // #endregion
// #region Event Listeners
/**
* @param {PointerEvent} event
*/
async #ammoInfoPointerEnter(event) {
console.log(event.x, event.y);
const { x, y } = event;
this.#ammoTrackerHoverTimeout = setTimeout(
() => {
this.#ammoTrackerHoverTimeout = null;
const tracker = new AmmoTracker({
popover: {
framed: false,
x, y,
},
});
tracker.render({ force: true });
this.#ammoTracker = tracker;
},
getTooltipDelay(),
);
};
async #ammoInfoPointerOut() {
if (this.#ammoTracker) {
// this.#ammoTracker.close();
};
if (this.#ammoTrackerHoverTimeout !== null) {
clearTimeout(this.#ammoTrackerHoverTimeout);
this.#ammoTrackerHoverTimeout = null;
};
};
async #ammoInfoClick() {};
// #endregion
// #region Actions // #region Actions
// #endregion // #endregion
}; };

View file

@ -1,9 +1,9 @@
import { filePath } from "../../consts.mjs"; import { filePath } from "../../consts.mjs";
import { GenericAppMixin } from "../GenericApp.mjs"; import { GenericPopoverMixin } from "./GenericPopoverMixin.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(ApplicationV2)) { export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin(ApplicationV2)) {
// #region Options // #region Options
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: [ classes: [
@ -30,13 +30,163 @@ export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(Appl
// #endregion // #endregion
// #region Instance Data // #region Instance Data
popover = {};
constructor({ popover, ...options}) {
// For when the caller doesn't provide anything, we want this to behave
// like a normal Application.
popover.framed ??= true;
popover.locked ??= true;
if (popover.framed) {
options.window.frame = true;
options.window.minimizable = true;
}
super(options);
this.popover = popover;
};
// #endregion // #endregion
// #region Lifecycle // #region Lifecycle
_insertElement(element) {
// console.log(this.popover);
const existing = document.getElementById(element.id);
if (existing) {
existing.replaceWith(element);
return;
};
// const pos = element.getBoundingClientRect();
// const horizontalOffset = Math.floor(pos.width / 2);
// console.log({ x: this.popover.x, y: this.popover.y, height: pos.height, xOffset: horizontalOffset });
element.style.position = `absolute`;
element.style.color = `black`;
element.style.background = `greenyellow`;
element.style[`z-index`] = 10000;
// element.style.left = `${this.popover.x - horizontalOffset}px`;
// element.style.top = `${this.popover.y - pos.height}px`;
// this.position = {
// left: this.popover.x - horizontalOffset,
// top: this.popover.y - pos.height,
// };
// standard addition
document.body.append(element);
};
// _updatePosition(position) {
// const pos = super._updatePosition(position);
// if (this.popover.framed) { return pos };
// delete pos.left;
// delete pos.top;
// const el = this.element;
// let bounds;
// let width, height;
// // Implicit height
// if ( true ) {
// Object.assign(el.style, {width: `${width}px`, height: ""});
// bounds = el.getBoundingClientRect();
// height = bounds.height;
// }
// // Implicit width
// if ( true ) {
// Object.assign(el.style, {height: `${height}px`, width: ""});
// bounds = el.getBoundingClientRect();
// width = bounds.width;
// }
// // const { width, height } = this.element.getBoundingClientRect();
// const horizontalOffset = Math.floor(width / 2);
// pos.left = this.popover.x - horizontalOffset;
// pos.top = this.popover.y - height;
// console.log({ x: this.popover.x, y: this.popover.y, height, xOffset: horizontalOffset, width });
// return pos;
// }
/**
* @override
* Custom implementation in order to make it show up approximately where I
* want it to when being created.
*/
_updatePosition(position) {
if ( !this.element ) { return position };
const el = this.element;
let {width, height, left, top, scale} = position;
scale ??= 1.0;
const computedStyle = getComputedStyle(el);
let minWidth = ApplicationV2.parseCSSDimension(computedStyle.minWidth, el.parentElement.offsetWidth) || 0;
let maxWidth = ApplicationV2.parseCSSDimension(computedStyle.maxWidth, el.parentElement.offsetWidth) || Infinity;
let minHeight = ApplicationV2.parseCSSDimension(computedStyle.minHeight, el.parentElement.offsetHeight) || 0;
let maxHeight = ApplicationV2.parseCSSDimension(computedStyle.maxHeight, el.parentElement.offsetHeight) || Infinity;
let bounds = el.getBoundingClientRect();
const {clientWidth, clientHeight} = document.documentElement;
// Explicit width
const autoWidth = width === `auto`;
if ( !autoWidth ) {
const targetWidth = Number(width || bounds.width);
minWidth = parseInt(minWidth) || 0;
maxWidth = parseInt(maxWidth) || (clientWidth / scale);
width = Math.clamp(targetWidth, minWidth, maxWidth);
}
// Explicit height
const autoHeight = height === `auto`;
if ( !autoHeight ) {
const targetHeight = Number(height || bounds.height);
minHeight = parseInt(minHeight) || 0;
maxHeight = parseInt(maxHeight) || (clientHeight / scale);
height = Math.clamp(targetHeight, minHeight, maxHeight);
}
// Implicit height
if ( autoHeight ) {
Object.assign(el.style, {width: `${width}px`, height: ``});
bounds = el.getBoundingClientRect();
height = bounds.height;
}
// Implicit width
if ( autoWidth ) {
Object.assign(el.style, {height: `${height}px`, width: ``});
bounds = el.getBoundingClientRect();
width = bounds.width;
}
// Left Offset
const scaledWidth = width * scale;
const targetLeft = left ?? (this.popover.x - Math.floor( scaledWidth / 2 ));
const maxLeft = Math.max(clientWidth - scaledWidth, 0);
left = Math.clamp(targetLeft, 0, maxLeft);
// Top Offset
const scaledHeight = height * scale;
const targetTop = top ?? (this.popover.y - scaledHeight);
const maxTop = Math.max(clientHeight - scaledHeight, 0);
top = Math.clamp(targetTop, 0, maxTop);
// Scale
scale ??= 1.0;
return {
width: autoWidth ? `auto` : width,
height: autoHeight ? `auto` : height,
left,
top,
scale,
};
}
async _onFirstRender(context, options) { async _onFirstRender(context, options) {
await super._onFirstRender(context, options); await super._onFirstRender(context, options);
const ammoContainer = this.element.querySelector(`.ammo`);
console.dir(ammoContainer);
}; };
// #endregion // #endregion

View file

@ -0,0 +1,4 @@
export function GenericPopoverMixin(HandlebarsApp) {
class GenericRipCryptPopover extends HandlebarsApp {};
return GenericRipCryptPopover;
};

View file

@ -9,6 +9,7 @@ import { RichEditor } from "./Apps/RichEditor.mjs";
import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs"; import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs";
import { documentSorter } from "./consts.mjs"; import { documentSorter } from "./consts.mjs";
import { rankToInteger } from "./utils/rank.mjs"; import { rankToInteger } from "./utils/rank.mjs";
import { AmmoTracker } from "./Apps/popovers/AmmoTracker.mjs";
const { deepFreeze } = foundry.utils; const { deepFreeze } = foundry.utils;
@ -18,6 +19,7 @@ Object.defineProperty(
{ {
value: deepFreeze({ value: deepFreeze({
Apps: { Apps: {
AmmoTracker,
DicePool, DicePool,
CombinedHeroSheet, CombinedHeroSheet,
HeroSummaryCardV1, HeroSummaryCardV1,

View file

@ -54,3 +54,14 @@ export function documentSorter(a, b) {
}; };
return Math.sign(a.name.localeCompare(b.name)); return Math.sign(a.name.localeCompare(b.name));
}; };
// MARK: getTooltipDelay
/**
* Retrieves the configured minimum delay between the user hovering an element
* and a tooltip showing up. Used for the pseudo-tooltip Applications that I use.
*
* @returns The number of milliseconds for the timeout
*/
export function getTooltipDelay() {
return 1000; // game.tooltip.constructor.TOOLTIP_ACTIVATION_MS;
};

View file

@ -104,7 +104,13 @@
{{/each}} {{/each}}
</ol> </ol>
<div class="ammo half-pill"> <div class="ammo half-pill with-icon">
<rc-icon
class="ammo-info-icon"
name="icons/info-circle"
var:size="16px"
var:fill="currentColor"
></rc-icon>
<div class="label"> <div class="label">
{{ rc-i18n "RipCrypt.common.ammo"}} {{ rc-i18n "RipCrypt.common.ammo"}}
</div> </div>

View file

@ -116,6 +116,16 @@
--input-background: var(--base-background); --input-background: var(--base-background);
--input-text: var(--base-text); --input-text: var(--base-text);
&.with-icon {
grid-template-columns: min-content minmax(0, 1.5fr) minmax(0, 1fr);
gap: 4px;
padding: 2px 0 2px 4px;
.label {
padding: 0;
}
}
.input { .input {
margin: 2px; margin: 2px;
border-radius: 999px; border-radius: 999px;