From 8e8202f8a6508404b71ad0bf2cc344a981481ef2 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 13 Mar 2025 23:36:25 -0600 Subject: [PATCH] Getting the popover Application working on the most superficial level, and creating a generic popover mixin --- module/Apps/ActorSheets/HeroSkillsCardV1.mjs | 63 +++++++- module/Apps/popovers/AmmoTracker.mjs | 158 ++++++++++++++++++- module/Apps/popovers/GenericPopoverMixin.mjs | 4 + module/api.mjs | 2 + module/consts.mjs | 11 ++ templates/Apps/HeroSkillsCardV1/content.hbs | 8 +- templates/Apps/HeroSkillsCardV1/style.css | 10 ++ 7 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 module/Apps/popovers/GenericPopoverMixin.mjs diff --git a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs index b9992b5..78448a1 100644 --- a/module/Apps/ActorSheets/HeroSkillsCardV1.mjs +++ b/module/Apps/ActorSheets/HeroSkillsCardV1.mjs @@ -1,9 +1,10 @@ import { deleteItemFromElement, editItemFromElement } from "../utils.mjs"; -import { documentSorter, filePath } from "../../consts.mjs"; +import { documentSorter, filePath, getTooltipDelay } from "../../consts.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"; const { HandlebarsApplicationMixin } = foundry.applications.api; const { ActorSheetV2 } = foundry.applications.sheets; @@ -39,16 +40,19 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin }; // #endregion + // #region Instance Data + /** @type {number | undefined} */ + #ammoTrackerHoverTimeout = null; + + /** @type {AmmoTracker | null} */ + #ammoTracker = null; + // #endregion + // #region Lifecycle async _onRender(context, options) { await super._onRender(context, options); HeroSkillsCardV1._onRender.bind(this)(context, options); - - const ammo = this.element.querySelector(`.ammo`); - - ammo.addEventListener(`mouseenter`, () => {console.log(`mouseenter-ing`)}); - - ammo.addEventListener(`contextmenu`, () => {console.log(`right-clicking`)}); + await this.#createAmmoTrackerEvents(); }; static async _onRender(_context, options) { @@ -81,6 +85,13 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin ); }; + async #createAmmoTrackerEvents() { + 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)); + }; + async _preparePartContext(partId, ctx, opts) { ctx = await super._preparePartContext(partId, ctx, opts); ctx.actor = this.document; @@ -177,6 +188,44 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin }; // #endregion + // #region Event Listeners + /** + * @param {PointerEvent} event + */ + async #ammoInfoPointerEnter(event) { + console.log(event.x, event.y); + const { x, y } = event; + + 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; + }; + }; + + async #ammoInfoClick() {}; + // #endregion + // #region Actions // #endregion }; diff --git a/module/Apps/popovers/AmmoTracker.mjs b/module/Apps/popovers/AmmoTracker.mjs index b216185..b6d59db 100644 --- a/module/Apps/popovers/AmmoTracker.mjs +++ b/module/Apps/popovers/AmmoTracker.mjs @@ -1,9 +1,9 @@ import { filePath } from "../../consts.mjs"; -import { GenericAppMixin } from "../GenericApp.mjs"; +import { GenericPopoverMixin } from "./GenericPopoverMixin.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; -export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(ApplicationV2)) { +export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin(ApplicationV2)) { // #region Options static DEFAULT_OPTIONS = { classes: [ @@ -30,13 +30,163 @@ export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(Appl // #endregion // #region Instance Data + popover = {}; + constructor({ popover, ...options}) { + + // For when the caller doesn't provide anything, we want this to behave + // like a normal Application. + popover.framed ??= true; + popover.locked ??= true; + + if (popover.framed) { + options.window.frame = true; + options.window.minimizable = true; + } + + super(options); + + this.popover = popover; + }; // #endregion // #region Lifecycle + _insertElement(element) { + // console.log(this.popover); + const existing = document.getElementById(element.id); + if (existing) { + existing.replaceWith(element); + return; + }; + // const pos = element.getBoundingClientRect(); + + // const horizontalOffset = Math.floor(pos.width / 2); + // console.log({ x: this.popover.x, y: this.popover.y, height: pos.height, xOffset: horizontalOffset }); + + element.style.position = `absolute`; + element.style.color = `black`; + element.style.background = `greenyellow`; + element.style[`z-index`] = 10000; + // element.style.left = `${this.popover.x - horizontalOffset}px`; + // element.style.top = `${this.popover.y - pos.height}px`; + // this.position = { + // left: this.popover.x - horizontalOffset, + // top: this.popover.y - pos.height, + // }; + + // standard addition + document.body.append(element); + }; + + // _updatePosition(position) { + // const pos = super._updatePosition(position); + // if (this.popover.framed) { return pos }; + + // delete pos.left; + // delete pos.top; + + // const el = this.element; + // let bounds; + // let width, height; + + // // Implicit height + // if ( true ) { + // Object.assign(el.style, {width: `${width}px`, height: ""}); + // bounds = el.getBoundingClientRect(); + // height = bounds.height; + // } + + // // Implicit width + // if ( true ) { + // Object.assign(el.style, {height: `${height}px`, width: ""}); + // bounds = el.getBoundingClientRect(); + // width = bounds.width; + // } + + // // const { width, height } = this.element.getBoundingClientRect(); + // const horizontalOffset = Math.floor(width / 2); + // pos.left = this.popover.x - horizontalOffset; + // pos.top = this.popover.y - height; + + // console.log({ x: this.popover.x, y: this.popover.y, height, xOffset: horizontalOffset, width }); + + // return pos; + // } + + /** + * @override + * Custom implementation in order to make it show up approximately where I + * want it to when being created. + */ + _updatePosition(position) { + if ( !this.element ) { return 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, + }; + } + async _onFirstRender(context, options) { await super._onFirstRender(context, options); - const ammoContainer = this.element.querySelector(`.ammo`); - console.dir(ammoContainer); }; // #endregion diff --git a/module/Apps/popovers/GenericPopoverMixin.mjs b/module/Apps/popovers/GenericPopoverMixin.mjs new file mode 100644 index 0000000..89b7bfd --- /dev/null +++ b/module/Apps/popovers/GenericPopoverMixin.mjs @@ -0,0 +1,4 @@ +export function GenericPopoverMixin(HandlebarsApp) { + class GenericRipCryptPopover extends HandlebarsApp {}; + return GenericRipCryptPopover; +}; diff --git a/module/api.mjs b/module/api.mjs index a56f7b1..ee40143 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -9,6 +9,7 @@ import { RichEditor } from "./Apps/RichEditor.mjs"; import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs"; import { documentSorter } from "./consts.mjs"; import { rankToInteger } from "./utils/rank.mjs"; +import { AmmoTracker } from "./Apps/popovers/AmmoTracker.mjs"; const { deepFreeze } = foundry.utils; @@ -18,6 +19,7 @@ Object.defineProperty( { value: deepFreeze({ Apps: { + AmmoTracker, DicePool, CombinedHeroSheet, HeroSummaryCardV1, diff --git a/module/consts.mjs b/module/consts.mjs index 84d8740..242d332 100644 --- a/module/consts.mjs +++ b/module/consts.mjs @@ -54,3 +54,14 @@ export function documentSorter(a, b) { }; return Math.sign(a.name.localeCompare(b.name)); }; + +// MARK: getTooltipDelay +/** + * Retrieves the configured minimum delay between the user hovering an element + * and a tooltip showing up. Used for the pseudo-tooltip Applications that I use. + * + * @returns The number of milliseconds for the timeout + */ +export function getTooltipDelay() { + return 1000; // game.tooltip.constructor.TOOLTIP_ACTIVATION_MS; +}; diff --git a/templates/Apps/HeroSkillsCardV1/content.hbs b/templates/Apps/HeroSkillsCardV1/content.hbs index eee8d4a..776f673 100644 --- a/templates/Apps/HeroSkillsCardV1/content.hbs +++ b/templates/Apps/HeroSkillsCardV1/content.hbs @@ -104,7 +104,13 @@ {{/each}} -
+
+
{{ rc-i18n "RipCrypt.common.ammo"}}
diff --git a/templates/Apps/HeroSkillsCardV1/style.css b/templates/Apps/HeroSkillsCardV1/style.css index 9c092ca..c19aaff 100644 --- a/templates/Apps/HeroSkillsCardV1/style.css +++ b/templates/Apps/HeroSkillsCardV1/style.css @@ -116,6 +116,16 @@ --input-background: var(--base-background); --input-text: var(--base-text); + &.with-icon { + grid-template-columns: min-content minmax(0, 1.5fr) minmax(0, 1fr); + gap: 4px; + padding: 2px 0 2px 4px; + .label { + padding: 0; + } + } + + .input { margin: 2px; border-radius: 999px;