Get the reusable foundations of custom popovers finished.
This commit is contained in:
parent
4b75526708
commit
e594b6beb0
8 changed files with 219 additions and 63 deletions
|
|
@ -36,6 +36,7 @@ export default [
|
||||||
Combatant: `readonly`,
|
Combatant: `readonly`,
|
||||||
canvas: `readonly`,
|
canvas: `readonly`,
|
||||||
Token: `readonly`,
|
Token: `readonly`,
|
||||||
|
Tour: `readonly`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
|
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
|
||||||
import { documentSorter, filePath, getTooltipDelay } from "../../consts.mjs";
|
import { documentSorter, filePath } from "../../consts.mjs";
|
||||||
|
import { AmmoTracker } from "../popovers/AmmoTracker.mjs";
|
||||||
import { gameTerms } from "../../gameTerms.mjs";
|
import { gameTerms } from "../../gameTerms.mjs";
|
||||||
import { GenericAppMixin } from "../GenericApp.mjs";
|
import { GenericAppMixin } from "../GenericApp.mjs";
|
||||||
import { localizer } from "../../utils/Localizer.mjs";
|
import { localizer } from "../../utils/Localizer.mjs";
|
||||||
import { Logger } from "../../utils/Logger.mjs";
|
import { Logger } from "../../utils/Logger.mjs";
|
||||||
import { AmmoTracker } from "../popovers/AmmoTracker.mjs";
|
import { PopoverEventManager } from "../../utils/PopoverEventManager.mjs";
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
const { ActorSheetV2 } = foundry.applications.sheets;
|
const { ActorSheetV2 } = foundry.applications.sheets;
|
||||||
|
|
@ -40,19 +41,15 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
|
||||||
};
|
};
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Instance Data
|
|
||||||
/** @type {number | undefined} */
|
|
||||||
#ammoTrackerHoverTimeout = null;
|
|
||||||
|
|
||||||
/** @type {AmmoTracker | null} */
|
|
||||||
#ammoTracker = null;
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region Lifecycle
|
// #region Lifecycle
|
||||||
|
async _onFirstRender(context, options) {
|
||||||
|
await super._onFirstRender(context, options);
|
||||||
|
HeroSkillsCardV1._createPopoverListeners.bind(this)();
|
||||||
|
};
|
||||||
|
|
||||||
async _onRender(context, options) {
|
async _onRender(context, options) {
|
||||||
await super._onRender(context, options);
|
await super._onRender(context, options);
|
||||||
HeroSkillsCardV1._onRender.bind(this)(context, options);
|
HeroSkillsCardV1._onRender.bind(this)(context, options);
|
||||||
await this.#createAmmoTrackerEvents();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static async _onRender(_context, options) {
|
static async _onRender(_context, options) {
|
||||||
|
|
@ -85,11 +82,15 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async #createAmmoTrackerEvents() {
|
/** @type {Map<string, PopoverEventManager>} */
|
||||||
|
#popoverManagers = new Map();
|
||||||
|
/** @this {HeroSkillsCardV1} */
|
||||||
|
static async _createPopoverListeners() {
|
||||||
const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`);
|
const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`);
|
||||||
ammoInfoIcon.addEventListener(`pointerenter`, this.#ammoInfoPointerEnter.bind(this));
|
this.#popoverManagers.set(
|
||||||
ammoInfoIcon.addEventListener(`pointerout`, this.#ammoInfoPointerOut.bind(this));
|
`.ammo-info-icon`,
|
||||||
ammoInfoIcon.addEventListener(`click`, this.#ammoInfoClick.bind(this));
|
new PopoverEventManager(ammoInfoIcon, AmmoTracker, { lockable: true }),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async _preparePartContext(partId, ctx, opts) {
|
async _preparePartContext(partId, ctx, opts) {
|
||||||
|
|
@ -186,45 +187,14 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
|
||||||
}
|
}
|
||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region Event Listeners
|
_tearDown(options) {
|
||||||
/**
|
for (const manager of this.#popoverManagers.values()) {
|
||||||
* @param {PointerEvent} event
|
manager.destroy();
|
||||||
*/
|
|
||||||
async #ammoInfoPointerEnter(event) {
|
|
||||||
const pos = event.target.getBoundingClientRect();
|
|
||||||
const x = pos.x + Math.floor(pos.width / 2);
|
|
||||||
const y = pos.y;
|
|
||||||
|
|
||||||
this.#ammoTrackerHoverTimeout = setTimeout(
|
|
||||||
() => {
|
|
||||||
this.#ammoTrackerHoverTimeout = null;
|
|
||||||
const tracker = new AmmoTracker({
|
|
||||||
popover: {
|
|
||||||
framed: false,
|
|
||||||
x, y,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
tracker.render({ force: true });
|
|
||||||
this.#ammoTracker = tracker;
|
|
||||||
},
|
|
||||||
getTooltipDelay(),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
this.#popoverManagers.clear();
|
||||||
async #ammoInfoPointerOut() {
|
super._tearDown(options);
|
||||||
if (this.#ammoTracker) {
|
|
||||||
this.#ammoTracker.close();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.#ammoTrackerHoverTimeout !== null) {
|
|
||||||
clearTimeout(this.#ammoTrackerHoverTimeout);
|
|
||||||
this.#ammoTrackerHoverTimeout = null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async #ammoInfoClick() {};
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Actions
|
// #region Actions
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@ export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin(
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: [
|
classes: [
|
||||||
`ripcrypt`,
|
`ripcrypt`,
|
||||||
|
],
|
||||||
|
window: {
|
||||||
|
title: `Ammo Tracker`,
|
||||||
|
contentClasses: [
|
||||||
`ripcrypt--AmmoTracker`,
|
`ripcrypt--AmmoTracker`,
|
||||||
],
|
],
|
||||||
position: {
|
|
||||||
width: 100,
|
|
||||||
height: 30,
|
|
||||||
},
|
},
|
||||||
actions: {},
|
actions: {},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
const { ApplicationV2 } = foundry.applications.api;
|
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.
|
||||||
|
*/
|
||||||
export function GenericPopoverMixin(HandlebarsApp) {
|
export function GenericPopoverMixin(HandlebarsApp) {
|
||||||
class GenericRipCryptPopover extends HandlebarsApp {
|
class GenericRipCryptPopover extends HandlebarsApp {
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
|
id: `popover-{id}`,
|
||||||
classes: [
|
classes: [
|
||||||
`popover`,
|
`popover`,
|
||||||
],
|
],
|
||||||
|
|
@ -21,21 +27,48 @@ export function GenericPopoverMixin(HandlebarsApp) {
|
||||||
// For when the caller doesn't provide anything, we want this to behave
|
// For when the caller doesn't provide anything, we want this to behave
|
||||||
// like a normal Application instance.
|
// like a normal Application instance.
|
||||||
popover.framed ??= true;
|
popover.framed ??= true;
|
||||||
popover.locked ??= true;
|
popover.locked ??= false;
|
||||||
|
|
||||||
|
|
||||||
if (popover.framed) {
|
if (popover.framed) {
|
||||||
|
options.window ??= {};
|
||||||
options.window.frame = true;
|
options.window.frame = true;
|
||||||
options.window.minimizable = true;
|
options.window.minimizable = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
options.classes ??= [];
|
||||||
|
options.classes.push(popover.framed ? `framed` : `frameless`);
|
||||||
|
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.popover = popover;
|
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 close(options = {}) {
|
async close(options = {}) {
|
||||||
|
// prevent locked popovers from being closed
|
||||||
|
if (this.popover.locked && !options.force) { return };
|
||||||
|
|
||||||
if (!this.popover.framed) {
|
if (!this.popover.framed) {
|
||||||
options.animate ??= false;
|
options.animate = false;
|
||||||
};
|
};
|
||||||
return super.close(options);
|
return super.close(options);
|
||||||
};
|
};
|
||||||
|
|
@ -51,7 +84,7 @@ export function GenericPopoverMixin(HandlebarsApp) {
|
||||||
*/
|
*/
|
||||||
_updatePosition(position) {
|
_updatePosition(position) {
|
||||||
if (!this.element) { return position };
|
if (!this.element) { return position };
|
||||||
if (this.popover.framed) { return position }
|
if (this.popover.framed) { return super._updatePosition(position) };
|
||||||
|
|
||||||
const el = this.element;
|
const el = this.element;
|
||||||
let {width, height, left, top, scale} = position;
|
let {width, height, left, top, scale} = position;
|
||||||
|
|
|
||||||
134
module/utils/PopoverEventManager.mjs
Normal file
134
module/utils/PopoverEventManager.mjs
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import { getTooltipDelay } from "../consts.mjs";
|
||||||
|
import { Logger } from "./Logger.mjs";
|
||||||
|
|
||||||
|
export class PopoverEventManager {
|
||||||
|
#options;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 = {}) {
|
||||||
|
options.locked ??= false;
|
||||||
|
options.lockable ??= true;
|
||||||
|
|
||||||
|
this.#options = options;
|
||||||
|
this.#element = element;
|
||||||
|
this.#class = popoverClass;
|
||||||
|
|
||||||
|
element.addEventListener(`pointerenter`, this.#pointerEnterHandler.bind(this));
|
||||||
|
element.addEventListener(`pointerout`, this.#pointerOutHandler.bind(this));
|
||||||
|
element.addEventListener(`click`, this.#clickHandler.bind(this));
|
||||||
|
|
||||||
|
if (options.lockable) {
|
||||||
|
element.addEventListener(`pointerup`, this.#pointerUpHandler.bind(this));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.close();
|
||||||
|
this.#element.removeEventListener(`pointerenter`, this.#pointerEnterHandler);
|
||||||
|
this.#element.removeEventListener(`pointerout`, this.#pointerOutHandler);
|
||||||
|
this.#element.removeEventListener(`click`, this.#clickHandler);
|
||||||
|
if (this.#options.lockable) {
|
||||||
|
this.#element.removeEventListener(`pointerup`, this.#pointerUpHandler);
|
||||||
|
};
|
||||||
|
this.stopOpen();
|
||||||
|
this.stopClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.#frameless?.close({ force: true });
|
||||||
|
this.#framed?.close({ force: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
stopOpen() {
|
||||||
|
if (this.#openTimeout != null) {
|
||||||
|
clearTimeout(this.#openTimeout);
|
||||||
|
this.#openTimeout = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
stopClose() {
|
||||||
|
if (this.#closeTimeout != null) {
|
||||||
|
clearTimeout(this.#closeTimeout);
|
||||||
|
this.#closeTimeout = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#element;
|
||||||
|
#class;
|
||||||
|
#openTimeout = null;
|
||||||
|
#closeTimeout = null;
|
||||||
|
|
||||||
|
#frameless;
|
||||||
|
#framed;
|
||||||
|
|
||||||
|
#clickHandler() {
|
||||||
|
// Cleanup for the frameless lifecycle
|
||||||
|
this.stopOpen();
|
||||||
|
this.#frameless?.close({ force: true });
|
||||||
|
|
||||||
|
if (!this.#framed) {
|
||||||
|
const app = new this.#class({ popover: { ...this.#options, framed: true } });
|
||||||
|
this.#framed = app;
|
||||||
|
}
|
||||||
|
this.#framed.render({ force: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
#pointerEnterHandler(event) {
|
||||||
|
this.stopClose();
|
||||||
|
|
||||||
|
const pos = event.target.getBoundingClientRect();
|
||||||
|
const x = pos.x + Math.floor(pos.width / 2);
|
||||||
|
const y = pos.y;
|
||||||
|
|
||||||
|
this.#openTimeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
this.#openTimeout = null;
|
||||||
|
|
||||||
|
// When we have the framed version rendered, we might as well just focus
|
||||||
|
// it instead of rendering a new application
|
||||||
|
if (this.#framed?.rendered) {
|
||||||
|
this.#framed.bringToFront();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.#frameless?.rendered) {
|
||||||
|
const { width, height } = this.#frameless.position;
|
||||||
|
this.#frameless.render({ position: { left: x - Math.floor(width / 2), top: y - height }});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#frameless = new this.#class({
|
||||||
|
popover: {
|
||||||
|
...this.#options,
|
||||||
|
framed: false,
|
||||||
|
x, y,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.#frameless.render({ force: true });
|
||||||
|
},
|
||||||
|
getTooltipDelay(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
#pointerOutHandler() {
|
||||||
|
this.stopOpen();
|
||||||
|
|
||||||
|
this.#closeTimeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
this.#closeTimeout = null;
|
||||||
|
this.#frameless?.close();
|
||||||
|
},
|
||||||
|
getTooltipDelay(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
#pointerUpHandler(event) {
|
||||||
|
Logger.debug(event);
|
||||||
|
if (event.button !== 1 || !this.#frameless?.rendered || Tour.tourInProgress) { return };
|
||||||
|
event.preventDefault();
|
||||||
|
this.#frameless.toggleLock();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -6,3 +6,5 @@
|
||||||
@import url("./HeroSummaryCardV1/style.css");
|
@import url("./HeroSummaryCardV1/style.css");
|
||||||
@import url("./HeroSkillsCardV1/style.css");
|
@import url("./HeroSkillsCardV1/style.css");
|
||||||
@import url("./RichEditor/style.css");
|
@import url("./RichEditor/style.css");
|
||||||
|
|
||||||
|
@import url("./popovers/AmmoTracker/style.css");
|
||||||
|
|
|
||||||
4
templates/Apps/popovers/AmmoTracker/style.css
Normal file
4
templates/Apps/popovers/AmmoTracker/style.css
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.ripcrypt--AmmoTracker {
|
||||||
|
color: var(--base-text);
|
||||||
|
background: var(--base-background);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
.ripcrypt.popover {
|
.ripcrypt.popover {
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.frameless {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: var(--z-index-tooltip);
|
z-index: var(--z-index-tooltip);
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.locked {
|
||||||
|
border-color: var(--accent-3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue