diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index 36e8ed3..ccffbef 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -82,22 +82,16 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin ); }; - /** @type {Map} */ - #popoverManagers = new Map(); - /** @type {Map} */ - #hookIDs = new Map(); /** @this {HeroSkillsCardV1} */ static async _createPopoverListeners() { const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`); + const idPrefix = this.actor.uuid; - this.#popoverManagers.set( - `.ammo-info-icon`, - new PopoverEventManager(ammoInfoIcon, AmmoTracker), - ); - - this.#hookIDs.set(Hooks.on(`get${AmmoTracker.name}Options`, (opts) => { - opts.ammo = this.actor.itemTypes.ammo; - }), `get${AmmoTracker.name}Options`); + const manager = new PopoverEventManager(`${idPrefix}.ammo-info-icon`, ammoInfoIcon, AmmoTracker); + this._popoverManagers.set(`.ammo-info-icon`, manager); + this._hookIDs.set(Hooks.on(`prepare${manager.id}Context`, (ctx) => { + ctx.ammos = this.actor.itemTypes.ammo; + }), `prepare${manager.id}Context`); }; async _preparePartContext(partId, ctx, opts) { @@ -194,17 +188,6 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin } return ctx; }; - - _tearDown(options) { - for (const manager of this.#popoverManagers.values()) { - manager.destroy(); - }; - this.#popoverManagers.clear(); - for (const [id, hook] of this.#hookIDs.entries()) { - Hooks.off(hook, id); - } - super._tearDown(options); - }; // #endregion // #region Actions diff --git a/module/Apps/GenericApp.mjs b/module/Apps/GenericApp.mjs index 17cfc0c..7c1077e 100644 --- a/module/Apps/GenericApp.mjs +++ b/module/Apps/GenericApp.mjs @@ -31,6 +31,13 @@ export function GenericAppMixin(HandlebarsApp) { }; // #endregion + // #region Instance Data + /** @type {Map} */ + _popoverManagers = new Map(); + /** @type {Map} */ + _hookIDs = new Map(); + // #endregion + // #region Lifecycle /** * @override @@ -45,6 +52,13 @@ export function GenericAppMixin(HandlebarsApp) { }; }; + async _onRender() { + await super._onRender(); + for (const manager of this._popoverManagers.values()) { + manager.render(); + }; + }; + async _preparePartContext(partId, ctx, opts) { ctx = await super._preparePartContext(partId, ctx, opts); delete ctx.document; @@ -60,6 +74,22 @@ export function GenericAppMixin(HandlebarsApp) { return ctx; }; + + _tearDown(options) { + // Clear all popovers associated with the app + for (const manager of this._popoverManagers.values()) { + manager.destroy(); + }; + this._popoverManagers.clear(); + + // Remove any hooks added for this app + for (const [id, hook] of this._hookIDs.entries()) { + Hooks.off(hook, id); + }; + this._hookIDs.clear(); + + super._tearDown(options); + }; // #endregion // #region Actions diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs index 81e5721..017c2b0 100644 --- a/module/Apps/popovers/GenericPopoverMixin.mjs +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -3,7 +3,10 @@ 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. + * overflow the bounds of the screen. This also implements a _preparePartContext + * in order to allow the parent application passing new data into the popover + * whenever it rerenders; how the popover handles this data is up to the + * specific implementation. */ export function GenericPopoverMixin(HandlebarsApp) { class GenericRipCryptPopover extends HandlebarsApp { @@ -29,7 +32,6 @@ export function GenericPopoverMixin(HandlebarsApp) { popover.framed ??= true; popover.locked ??= false; - if (popover.framed) { options.window ??= {}; options.window.frame = true; @@ -151,6 +153,17 @@ export function GenericPopoverMixin(HandlebarsApp) { scale, }; }; + + /** + * This is here in order allow things that are not this Application + * to provide / augment the context data for the lifecycle of the app. + */ + async _prepareContext(_partId, _context, options) { + const context = {}; + Hooks.callAll(`prepare${this.constructor.name}Context`, context, options); + Hooks.callAll(`prepare${this.popover.managerId}Context`, context, options); + return context; + }; }; return GenericRipCryptPopover; }; diff --git a/module/utils/PopoverEventManager.mjs b/module/utils/PopoverEventManager.mjs index 4bb8cf1..6c0b0c8 100644 --- a/module/utils/PopoverEventManager.mjs +++ b/module/utils/PopoverEventManager.mjs @@ -1,13 +1,28 @@ import { getTooltipDelay } from "../consts.mjs"; +import { Logger } from "./Logger.mjs"; export class PopoverEventManager { #options; + id; + + /** @type {Map} */ + static #existing = new Map(); /** * @param {HTMLElement} element The element to attach the listeners to. * @param {GenericPopoverMixin} popoverClass The class reference that represents the popover app */ - constructor(element, popoverClass, options = {}) { + constructor(id, element, popoverClass, options = {}) { + id = `${id}-${popoverClass.name}`; + this.id = id; + + if (PopoverEventManager.#existing.has(id)) { + const manager = PopoverEventManager.#existing.get(id); + manager.#addListeners(element); + return manager; + }; + + options.managerId = id; options.locked ??= false; options.lockable ??= true; @@ -15,11 +30,19 @@ export class PopoverEventManager { this.#element = element; this.#class = popoverClass; + this.#addListeners(element); + PopoverEventManager.#existing.set(id, this); + }; + + /** + * @param {HTMLElement} element + */ + #addListeners(element) { element.addEventListener(`pointerenter`, this.#pointerEnterHandler.bind(this)); element.addEventListener(`pointerout`, this.#pointerOutHandler.bind(this)); element.addEventListener(`click`, this.#clickHandler.bind(this)); - if (options.lockable) { + if (this.#options.lockable) { element.addEventListener(`pointerup`, this.#pointerUpHandler.bind(this)); }; }; @@ -55,6 +78,19 @@ export class PopoverEventManager { } }; + get rendered() { + return Boolean(this.#frameless?.rendered || this.#framed?.rendered); + }; + + render(options) { + if (this.#framed?.rendered) { + this.#framed.render(options); + }; + if (this.#frameless?.rendered) { + this.#frameless.render(options); + }; + }; + #element; #class; #openTimeout = null; @@ -64,12 +100,14 @@ export class PopoverEventManager { #framed; #construct(options) { - const valid = Hooks.call(`get${this.#class.name}Options`, options); - if (!valid) { return }; + options.popover ??= {}; + options.popover.managerId = this.id; + return new this.#class(options); }; #clickHandler() { + Logger.debug(`click event handler`); // Cleanup for the frameless lifecycle this.stopOpen(); this.stopClose();