Getting the popover Application working on the most superficial level, and creating a generic popover mixin
This commit is contained in:
parent
af5cf4acd5
commit
8e8202f8a6
7 changed files with 244 additions and 12 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
|
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 { 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";
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
const { ActorSheetV2 } = foundry.applications.sheets;
|
const { ActorSheetV2 } = foundry.applications.sheets;
|
||||||
|
|
@ -39,16 +40,19 @@ 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 _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();
|
||||||
const ammo = this.element.querySelector(`.ammo`);
|
|
||||||
|
|
||||||
ammo.addEventListener(`mouseenter`, () => {console.log(`mouseenter-ing`)});
|
|
||||||
|
|
||||||
ammo.addEventListener(`contextmenu`, () => {console.log(`right-clicking`)});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static async _onRender(_context, options) {
|
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) {
|
async _preparePartContext(partId, ctx, opts) {
|
||||||
ctx = await super._preparePartContext(partId, ctx, opts);
|
ctx = await super._preparePartContext(partId, ctx, opts);
|
||||||
ctx.actor = this.document;
|
ctx.actor = this.document;
|
||||||
|
|
@ -177,6 +188,44 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
|
||||||
};
|
};
|
||||||
// #endregion
|
// #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
|
// #region Actions
|
||||||
// #endregion
|
// #endregion
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { filePath } from "../../consts.mjs";
|
import { filePath } from "../../consts.mjs";
|
||||||
import { GenericAppMixin } from "../GenericApp.mjs";
|
import { GenericPopoverMixin } from "./GenericPopoverMixin.mjs";
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(ApplicationV2)) {
|
export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin(ApplicationV2)) {
|
||||||
// #region Options
|
// #region Options
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
classes: [
|
classes: [
|
||||||
|
|
@ -30,13 +30,163 @@ export class AmmoTracker extends GenericAppMixin(HandlebarsApplicationMixin(Appl
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Instance Data
|
// #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
|
// #endregion
|
||||||
|
|
||||||
// #region Lifecycle
|
// #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) {
|
async _onFirstRender(context, options) {
|
||||||
await super._onFirstRender(context, options);
|
await super._onFirstRender(context, options);
|
||||||
const ammoContainer = this.element.querySelector(`.ammo`);
|
|
||||||
console.dir(ammoContainer);
|
|
||||||
};
|
};
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
|
|
||||||
4
module/Apps/popovers/GenericPopoverMixin.mjs
Normal file
4
module/Apps/popovers/GenericPopoverMixin.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export function GenericPopoverMixin(HandlebarsApp) {
|
||||||
|
class GenericRipCryptPopover extends HandlebarsApp {};
|
||||||
|
return GenericRipCryptPopover;
|
||||||
|
};
|
||||||
|
|
@ -9,6 +9,7 @@ import { RichEditor } from "./Apps/RichEditor.mjs";
|
||||||
import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs";
|
import { distanceBetweenFates, nextFate, previousFate } from "./utils/fates.mjs";
|
||||||
import { documentSorter } from "./consts.mjs";
|
import { documentSorter } from "./consts.mjs";
|
||||||
import { rankToInteger } from "./utils/rank.mjs";
|
import { rankToInteger } from "./utils/rank.mjs";
|
||||||
|
import { AmmoTracker } from "./Apps/popovers/AmmoTracker.mjs";
|
||||||
|
|
||||||
const { deepFreeze } = foundry.utils;
|
const { deepFreeze } = foundry.utils;
|
||||||
|
|
||||||
|
|
@ -18,6 +19,7 @@ Object.defineProperty(
|
||||||
{
|
{
|
||||||
value: deepFreeze({
|
value: deepFreeze({
|
||||||
Apps: {
|
Apps: {
|
||||||
|
AmmoTracker,
|
||||||
DicePool,
|
DicePool,
|
||||||
CombinedHeroSheet,
|
CombinedHeroSheet,
|
||||||
HeroSummaryCardV1,
|
HeroSummaryCardV1,
|
||||||
|
|
|
||||||
|
|
@ -54,3 +54,14 @@ export function documentSorter(a, b) {
|
||||||
};
|
};
|
||||||
return Math.sign(a.name.localeCompare(b.name));
|
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;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,13 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div class="ammo half-pill">
|
<div class="ammo half-pill with-icon">
|
||||||
|
<rc-icon
|
||||||
|
class="ammo-info-icon"
|
||||||
|
name="icons/info-circle"
|
||||||
|
var:size="16px"
|
||||||
|
var:fill="currentColor"
|
||||||
|
></rc-icon>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
{{ rc-i18n "RipCrypt.common.ammo"}}
|
{{ rc-i18n "RipCrypt.common.ammo"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,16 @@
|
||||||
--input-background: var(--base-background);
|
--input-background: var(--base-background);
|
||||||
--input-text: var(--base-text);
|
--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 {
|
.input {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue