Get the reusable foundations of custom popovers finished.

This commit is contained in:
Oliver-Akins 2025-03-14 22:52:40 -06:00
parent 4b75526708
commit e594b6beb0
8 changed files with 219 additions and 63 deletions

View file

@ -1,10 +1,11 @@
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
import { documentSorter, filePath, getTooltipDelay } from "../../consts.mjs";
import { documentSorter, filePath } from "../../consts.mjs";
import { AmmoTracker } from "../popovers/AmmoTracker.mjs";
import { gameTerms } from "../../gameTerms.mjs";
import { GenericAppMixin } from "../GenericApp.mjs";
import { localizer } from "../../utils/Localizer.mjs";
import { Logger } from "../../utils/Logger.mjs";
import { AmmoTracker } from "../popovers/AmmoTracker.mjs";
import { PopoverEventManager } from "../../utils/PopoverEventManager.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ActorSheetV2 } = foundry.applications.sheets;
@ -40,19 +41,15 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
};
// #endregion
// #region Instance Data
/** @type {number | undefined} */
#ammoTrackerHoverTimeout = null;
/** @type {AmmoTracker | null} */
#ammoTracker = null;
// #endregion
// #region Lifecycle
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
HeroSkillsCardV1._createPopoverListeners.bind(this)();
};
async _onRender(context, options) {
await super._onRender(context, options);
HeroSkillsCardV1._onRender.bind(this)(context, options);
await this.#createAmmoTrackerEvents();
};
static async _onRender(_context, options) {
@ -85,11 +82,15 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
);
};
async #createAmmoTrackerEvents() {
/** @type {Map<string, PopoverEventManager>} */
#popoverManagers = new Map();
/** @this {HeroSkillsCardV1} */
static async _createPopoverListeners() {
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));
this.#popoverManagers.set(
`.ammo-info-icon`,
new PopoverEventManager(ammoInfoIcon, AmmoTracker, { lockable: true }),
);
};
async _preparePartContext(partId, ctx, opts) {
@ -186,45 +187,14 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
}
return ctx;
};
// #endregion
// #region Event Listeners
/**
* @param {PointerEvent} event
*/
async #ammoInfoPointerEnter(event) {
const pos = event.target.getBoundingClientRect();
const x = pos.x + Math.floor(pos.width / 2);
const y = pos.y;
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;
_tearDown(options) {
for (const manager of this.#popoverManagers.values()) {
manager.destroy();
};
this.#popoverManagers.clear();
super._tearDown(options);
};
async #ammoInfoClick() {};
// #endregion
// #region Actions

View file

@ -8,11 +8,12 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin(
static DEFAULT_OPTIONS = {
classes: [
`ripcrypt`,
`ripcrypt--AmmoTracker`,
],
position: {
width: 100,
height: 30,
window: {
title: `Ammo Tracker`,
contentClasses: [
`ripcrypt--AmmoTracker`,
],
},
actions: {},
};

View file

@ -1,8 +1,14 @@
const { ApplicationV2 } = foundry.applications.api;
/**
* This mixin provides the ability to designate an Application as a "popover",
* which means that it will spawn near the x/y coordinates provided it won't
* overflow the bounds of the screen.
*/
export function GenericPopoverMixin(HandlebarsApp) {
class GenericRipCryptPopover extends HandlebarsApp {
static DEFAULT_OPTIONS = {
id: `popover-{id}`,
classes: [
`popover`,
],
@ -21,21 +27,48 @@ export function GenericPopoverMixin(HandlebarsApp) {
// For when the caller doesn't provide anything, we want this to behave
// like a normal Application instance.
popover.framed ??= true;
popover.locked ??= true;
popover.locked ??= false;
if (popover.framed) {
options.window ??= {};
options.window.frame = true;
options.window.minimizable = true;
};
}
options.classes ??= [];
options.classes.push(popover.framed ? `framed` : `frameless`);
super(options);
this.popover = popover;
};
toggleLock() {
this.popover.locked = !this.popover.locked;
this.classList.toggle(`locked`, this.popover.locked);
};
/**
* This render utility is intended in order to make the popovers able to be
* used in both framed and frameless mode, making sure that the content classes
* from the framed mode get shunted onto the frameless Application's root
* element.
*/
async _onFirstRender(...args) {
await super._onFirstRender(...args);
const hasContentClasses = this.options?.window?.contentClasses?.length > 0;
if (!this.popover.framed && hasContentClasses) {
this.classList.add(...this.options.window.contentClasses);
};
};
async close(options = {}) {
// prevent locked popovers from being closed
if (this.popover.locked && !options.force) { return };
if (!this.popover.framed) {
options.animate ??= false;
options.animate = false;
};
return super.close(options);
};
@ -51,7 +84,7 @@ export function GenericPopoverMixin(HandlebarsApp) {
*/
_updatePosition(position) {
if (!this.element) { return position };
if (this.popover.framed) { return position }
if (this.popover.framed) { return super._updatePosition(position) };
const el = this.element;
let {width, height, left, top, scale} = position;