Moved the popup manager into its own file. Made it more robust so there should...
This commit is contained in:
133
system/scripts/misc/l5r5e-popup-manager.js
Normal file
133
system/scripts/misc/l5r5e-popup-manager.js
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user