Moved the popup manager into its own file. Made it more robust so there should...

This commit is contained in:
Litasa
2025-07-25 15:48:21 +00:00
committed by Vlyan
parent e9b7b5b42f
commit 6a8d25f140
3 changed files with 173 additions and 78 deletions

View File

@@ -1,6 +1,8 @@
import { L5r5ePopupManager } from '../misc/l5r5e-popup-manager.js';
const HandlebarsApplicationMixin = foundry.applications.api.HandlebarsApplicationMixin;
const ApplicationV2 = foundry.applications.api.ApplicationV2;
export class GmMonitor extends HandlebarsApplicationMixin(ApplicationV2) {
/** @override ApplicationV2 */
static get DEFAULT_OPTIONS() {
@@ -136,33 +138,34 @@ export class GmMonitor extends HandlebarsApplicationMixin(ApplicationV2) {
}).bind(this.element);
// Tooltips
game.l5r5e.HelpersL5r5e.popupManager($(this.element).find(".actor-infos-control"), async (event) => {
const type = $(event.currentTarget).data("type");
if (!type) {
return;
new L5r5ePopupManager(
$(this.element).find(".actor-infos-control"),
async (event) => {
const type = $(event.currentTarget).data("type");
if (!type) return;
if (type === "text") {
return $(event.currentTarget).data("text");
}
const uuid = $(event.currentTarget).data("actor-uuid");
if (!uuid) return;
const actor = this.context.actors.find(actor => actor.uuid === uuid);
if (!actor) return;
switch (type) {
case "armors":
return this.#getTooltipArmors(actor);
case "weapons":
return this.#getTooltipWeapons(actor);
case "global":
return actor.isArmy
? this.#getTooltipArmiesGlobal(actor)
: this.#getTooltipGlobal(actor);
}
}
if (type === "text") {
return $(event.currentTarget).data("text");
}
const uuid = $(event.currentTarget).data("actor-uuid");
if (!uuid) {
return;
}
const actor = this.context.actors.find((actor) => actor.uuid === uuid);
if (!actor) {
return;
}
switch (type) {
case "armors":
return this.#getTooltipArmors(actor);
case "weapons":
return this.#getTooltipWeapons(actor);
case "global":
return actor.isArmy ? this.#getTooltipArmiesGlobal(actor) : this.#getTooltipGlobal(actor);
}
});
);
}
/** @override ApplicationV2 */

View File

@@ -1,3 +1,5 @@
import { L5r5ePopupManager } from './misc/l5r5e-popup-manager.js';
/**
* Extends the actor to process special things from L5R.
*/
@@ -509,13 +511,16 @@ export class HelpersL5r5e {
});
// Item detail tooltips
this.popupManager(html.find(".l5r5e-tooltip"), async (event) => {
const item = await HelpersL5r5e.getEmbedItemByEvent(event, actor);
if (!item) {
return;
new L5r5ePopupManager(
html.find(".l5r5e-tooltip"),
async (event) => {
const item = await HelpersL5r5e.getEmbedItemByEvent(event, actor);
if (!item) {
return;
}
return await item.renderTextTemplate();
}
return await item.renderTextTemplate();
});
);
// Open actor sheet
html.find(".open-sheet-from-uuid").on("click", async (event) => {
@@ -534,52 +539,6 @@ export class HelpersL5r5e {
});
}
/**
* Do the Popup for the selected element
* @param {Selector} selector HTML Selector
* @param {function} callback Callback function(event), must return the html to display
*/
static popupManager(selector, callback) {
const popupPosition = (event, popup) => {
let left = +event.clientX + 60,
top = +event.clientY;
let maxY = window.innerHeight - popup.outerHeight();
if (top > maxY) {
top = maxY - 10;
}
let maxX = window.innerWidth - popup.outerWidth();
if (left > maxX) {
left -= popup.outerWidth() + 100;
}
return { left: left + "px", top: top + "px", visibility: "visible" };
};
selector
.on("mouseenter", async (event) => {
$(document.body).find("#l5r5e-tooltip-ct").remove();
const tpl = await callback(event);
if (!tpl) {
return;
}
$(document.body).append(
`<div id="l5r5e-tooltip-ct" class="l5r5e-tooltip l5r5e-tooltip-ct">${tpl}</div>`
);
})
.on("mousemove", (event) => {
const popup = $(document.body).find("#l5r5e-tooltip-ct");
if (popup) {
popup.css(popupPosition(event, popup));
}
})
.on("mouseleave", () => {
$(document.body).find("#l5r5e-tooltip-ct").remove();
}); // tooltips
}
/**
* Get a Item from a Actor Sheet
* @param {Event} event HTML Event

View File

@@ -0,0 +1,133 @@
/**
* Manages tooltips for specified elements, showing asynchronously generated content on hover.
* Tooltips are appended to a customizable container element.
* Automatically cleans up event handlers and observers when target elements are removed.
*/
export class L5r5ePopupManager {
/** @type {string|jQuery} */
#selector = null;
/** @type {(event: MouseEvent) => Promise<string>} */
#callback = null;
/** @type {jQuery|null} */
#elements = null;
/** @type {MutationObserver|null} */
#observer = null;
/** @type {HTMLElement} */
#container = null;
/**
* @param {string|jQuery} selector - Selector or jQuery object for tooltip-bound elements.
* @param {(event: MouseEvent) => Promise<string>} callback - Async function returning tooltip HTML content.
* @param {HTMLElement|jQuery} [container=document.body] - DOM element or jQuery object to contain the tooltip.
*/
constructor(selector, callback, container = document.body) {
this.#selector = selector;
this.#callback = callback;
this.#container = container instanceof jQuery ? container[0] : container;
this.#bindEvents();
this.#observeDOM();
}
/**
* Bind mouseenter, mousemove, and mouseleave events to target elements.
* Creates/removes tooltip elements on hover, and updates position on mouse move.
* @private
*/
#bindEvents() {
this.#elements = $(this.#selector);
this.#elements
.on("mouseenter.popup", async (event) => {
$(this.#container).find("#l5r5e-tooltip-ct").remove();
const tpl = await this.#callback(event);
// Abort if no content or the target element is no longer in the DOM
if (!tpl || !document.body.contains(event.currentTarget)) return;
$(this.#container).append(
`<div id="l5r5e-tooltip-ct" class="l5r5e-tooltip l5r5e-tooltip-ct">${tpl}</div>`
);
})
.on("mousemove.popup", (event) => {
const popup = $(this.#container).find("#l5r5e-tooltip-ct");
if (popup.length) {
popup.css(this.popupPosition(event, popup));
}
})
.on("mouseleave.popup", () => {
$(this.#container).find("#l5r5e-tooltip-ct").remove();
});
}
/**
* Creates a MutationObserver that watches for removal of tooltip-bound elements
* and automatically destroys the manager if none remain.
* @private
*/
#observeDOM() {
this.#observer = new MutationObserver(() => {
const anyStillPresent = this.#elements?.toArray().some(el => document.body.contains(el));
if (!anyStillPresent) {
this.destroy();
}
});
this.#observer.observe(this.#container, {
childList: true,
subtree: true
});
}
/**
* Calculates CSS positioning for the tooltip based on mouse event,
* constraining it inside the viewport.
* @param {MouseEvent} event - The mouse event for position reference.
* @param {jQuery} popup - The jQuery object representing the tooltip element.
* @returns {{left: string, top: string, visibility: string}} CSS styles for tooltip positioning.
*/
popupPosition(event, popup) {
let left = event.clientX + 60;
let top = event.clientY;
const maxY = window.innerHeight - popup.outerHeight();
if (top > maxY) {
top = maxY - 10;
}
const maxX = window.innerWidth - popup.outerWidth();
if (left > maxX) {
left -= popup.outerWidth() + 100;
}
return {
left: `${left}px`,
top: `${top}px`,
visibility: "visible"
};
}
/**
* Unbind all events, remove tooltip elements, and disconnect the MutationObserver.
* Cleans up all references for proper garbage collection.
*/
destroy() {
if (this.#elements) {
this.#elements.off(".popup");
}
$(this.#container).find("#l5r5e-tooltip-ct").remove();
if (this.#observer) {
this.#observer.disconnect();
this.#observer = null;
}
this.#elements = null;
}
}