import { updateForeignDocumentFromEvent } from "../utils.mjs"; 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. 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 { static DEFAULT_OPTIONS = { id: `popover-{id}`, classes: [ `popover`, ], window: { frame: false, positioned: true, resizable: false, minimizable: false, }, actions: {}, }; popover = {}; constructor({ popover, ...options}) { // For when the caller doesn't provide anything, we want this to behave // like a normal Application instance. popover.framed ??= 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 _onRender(...args) { await super._onRender(...args); /* Foreign update listeners so that we can easily update items that may not be this document itself, but are useful to be able to be edited from this sheet. Primarily useful for editing the Actors' Item collection, or an Items' ActiveEffect collection. */ this.element.querySelectorAll(`input[data-foreign-update-on]`).forEach(el => { const events = el.dataset.foreignUpdateOn.split(`,`); for (const event of events) { el.addEventListener(event, updateForeignDocumentFromEvent); }; }); }; async close(options = {}) { // prevent locked popovers from being closed if (this.popover.locked && !options.force) { return }; if (!this.popover.framed) { options.animate = false; }; return super.close(options); }; /** * @override * Custom implementation in order to make it show up approximately where I * want it to when being created. * * Most of this implementation is identical to the ApplicationV2 * implementation, the biggest difference is how targetLeft and targetTop * are calculated. */ _updatePosition(position) { if (!this.element) { return position }; if (this.popover.framed) { return super._updatePosition(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, }; }; /** * 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; };