Ammo Tracking
- Adds the sum of all ammo into the skills sheet - Initializes a Popover system so that we can have rerenderable popovers in the system that can be rendered like tooltips, or popped out into their own window without the need for separate templating for each method. The "tooltip"-presentation is just a frameless application. - Utilizes the Popover system to create an ammo popover that lists each ammo independently and allows the user to "star" up to three ammos that will be visible on the sheet at all times. - Bumps the verified from from `13`->`13.339`
This commit is contained in:
commit
86ddac1aa4
35 changed files with 831 additions and 25 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
Oliver Akins:
|
Oliver Akins:
|
||||||
- geist-silhouette.v2.svg : All rights reserved.
|
- geist-silhouette.v2.svg : All rights reserved.
|
||||||
- caster-silhouette.v1.svg : All rights reserved.
|
- caster-silhouette.v1.svg : All rights reserved.
|
||||||
|
- icons/star-empty.svg : Modified from https://thenounproject.com/icon/star-7711815/ by Llisole
|
||||||
|
- icons/star.svg : Modified from https://thenounproject.com/icon/star-7711815/ by Llisole
|
||||||
|
|
||||||
Kýnan Antos (Gritsilk Games):
|
Kýnan Antos (Gritsilk Games):
|
||||||
- hero-silhouette.svg : Licensed to Distribute and Modify within the bounds of the "Foundry-RipCrypt" system.
|
- hero-silhouette.svg : Licensed to Distribute and Modify within the bounds of the "Foundry-RipCrypt" system.
|
||||||
|
|
|
||||||
1
assets/icons/star-empty.svg
Normal file
1
assets/icons/star-empty.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M93.824 44.383 80.058 61.379a6.3 6.3 0 0 0-1.398 4.3l1.144 21.84c.2 3.802-3.578 6.548-7.133 5.18l-20.418-7.84a6.3 6.3 0 0 0-4.52 0L27.317 92.7c-3.551 1.364-7.332-1.382-7.133-5.18l1.145-21.84a6.3 6.3 0 0 0-1.399-4.3L6.163 44.383C3.77 41.426 5.21 36.985 8.886 36l21.125-5.66a6.3 6.3 0 0 0 3.656-2.656L45.577 9.34c2.07-3.192 6.742-3.192 8.816 0l11.91 18.344a6.3 6.3 0 0 0 3.657 2.656L91.085 36c3.675.985 5.128 5.43 2.734 8.387z" style="stroke-width: 8px; stroke: black; fill: transparent;"/></svg>
|
||||||
|
After Width: | Height: | Size: 565 B |
1
assets/icons/star.svg
Normal file
1
assets/icons/star.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="M93.824 44.383 80.058 61.379a6.3 6.3 0 0 0-1.398 4.3l1.144 21.84c.2 3.802-3.578 6.548-7.133 5.18l-20.418-7.84a6.3 6.3 0 0 0-4.52 0L27.317 92.7c-3.551 1.364-7.332-1.382-7.133-5.18l1.145-21.84a6.3 6.3 0 0 0-1.399-4.3L6.163 44.383C3.77 41.426 5.21 36.985 8.886 36l21.125-5.66a6.3 6.3 0 0 0 3.656-2.656L45.577 9.34c2.07-3.192 6.742-3.192 8.816 0l11.91 18.344a6.3 6.3 0 0 0 3.657 2.656L91.085 36c3.675.985 5.128 5.43 2.734 8.387z"/></svg>
|
||||||
|
After Width: | Height: | Size: 504 B |
|
|
@ -22,9 +22,7 @@ export default [
|
||||||
Hooks: `readonly`,
|
Hooks: `readonly`,
|
||||||
ui: `readonly`,
|
ui: `readonly`,
|
||||||
Actor: `readonly`,
|
Actor: `readonly`,
|
||||||
Actors: `readonly`,
|
|
||||||
Item: `readonly`,
|
Item: `readonly`,
|
||||||
Items: `readonly`,
|
|
||||||
foundry: `readonly`,
|
foundry: `readonly`,
|
||||||
ChatMessage: `readonly`,
|
ChatMessage: `readonly`,
|
||||||
ActiveEffect: `readonly`,
|
ActiveEffect: `readonly`,
|
||||||
|
|
@ -36,6 +34,7 @@ export default [
|
||||||
Combatant: `readonly`,
|
Combatant: `readonly`,
|
||||||
canvas: `readonly`,
|
canvas: `readonly`,
|
||||||
Token: `readonly`,
|
Token: `readonly`,
|
||||||
|
Tour: `readonly`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@
|
||||||
"HeroCraftCardV1": "Hero Craft Card",
|
"HeroCraftCardV1": "Hero Craft Card",
|
||||||
"HeroSkillsCardV1": "Hero Skill Card"
|
"HeroSkillsCardV1": "Hero Skill Card"
|
||||||
},
|
},
|
||||||
|
"app-titles": {
|
||||||
|
"AmmoTracker": "Ammo Tracker"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"grit": "Grit",
|
"grit": "Grit",
|
||||||
|
|
@ -172,12 +175,21 @@
|
||||||
"numberOfDice": "# of Dice",
|
"numberOfDice": "# of Dice",
|
||||||
"rollTarget": "Target",
|
"rollTarget": "Target",
|
||||||
"difficulty": "(DC: {dc})",
|
"difficulty": "(DC: {dc})",
|
||||||
"RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost."
|
"RichEditor-no-collaborative": "Warning: This editor is not collaborative, that means that if you and someone else are editing it at the same time, you won't see that someone else is making changes until they save, and then your changes will be lost.",
|
||||||
|
"starred-ammo-placeholder": "Starred Ammo Slot",
|
||||||
|
"AmmoTracker": {
|
||||||
|
"no-ammo": "You don't have any ammo!",
|
||||||
|
"star-button": "Add {name} as a starred ammo",
|
||||||
|
"star-button-tooltip": "Add Star",
|
||||||
|
"unstar-button": "Remove {name} as a starred ammo",
|
||||||
|
"unstar-button-tooltip": "Remove Star"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"notifs": {
|
"notifs": {
|
||||||
"error": {
|
"error": {
|
||||||
"cannot-equip": "Cannot equip the {itemType}, see console for more details.",
|
"cannot-equip": "Cannot equip the {itemType}, see console for more details.",
|
||||||
"invalid-delta": "The delta for \"{name}\" is not a number, cannot finish processing the action."
|
"invalid-delta": "The delta for \"{name}\" is not a number, cannot finish processing the action.",
|
||||||
|
"at-favourite-limit": "Cannot favourite more than three items, unfavourite one to make space."
|
||||||
},
|
},
|
||||||
"warn": {
|
"warn": {
|
||||||
"cannot-go-negative": "\"{name}\" is unable to be a negative number."
|
"cannot-go-negative": "\"{name}\" is unable to be a negative number."
|
||||||
|
|
@ -197,5 +209,8 @@
|
||||||
"heavy": "The distance of your aura when using Heavycraft"
|
"heavy": "The distance of your aura when using Heavycraft"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"USER": {
|
||||||
|
"GM": "Keeper"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
|
import { deleteItemFromElement, editItemFromElement } from "../utils.mjs";
|
||||||
import { documentSorter, filePath } 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 { ItemFlags } from "../../flags/item.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 { 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;
|
||||||
|
|
@ -43,6 +46,7 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
|
||||||
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);
|
||||||
|
HeroSkillsCardV1._createPopoverListeners.bind(this)();
|
||||||
};
|
};
|
||||||
|
|
||||||
static async _onRender(_context, options) {
|
static async _onRender(_context, options) {
|
||||||
|
|
@ -75,6 +79,18 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @this {HeroSkillsCardV1} */
|
||||||
|
static async _createPopoverListeners() {
|
||||||
|
const ammoInfoIcon = this.element.querySelector(`.ammo-info-icon`);
|
||||||
|
const idPrefix = this.actor.uuid;
|
||||||
|
|
||||||
|
const manager = new PopoverEventManager(`${idPrefix}.ammo-info-icon`, ammoInfoIcon, AmmoTracker);
|
||||||
|
this._popoverManagers.set(`.ammo-info-icon`, manager);
|
||||||
|
this._hookIDs.set(Hooks.on(`prepare${manager.id}Context`, (ctx) => {
|
||||||
|
ctx.ammos = this.actor.itemTypes.ammo;
|
||||||
|
}), `prepare${manager.id}Context`);
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -120,7 +136,24 @@ export class HeroSkillsCardV1 extends GenericAppMixin(HandlebarsApplicationMixin
|
||||||
};
|
};
|
||||||
|
|
||||||
static async prepareAmmo(ctx) {
|
static async prepareAmmo(ctx) {
|
||||||
ctx.ammo = 0;
|
let total = 0;
|
||||||
|
let favouriteCount = 0;
|
||||||
|
ctx.favouriteAmmo = new Array(3).fill(null);
|
||||||
|
|
||||||
|
for (const ammo of ctx.actor.itemTypes.ammo) {
|
||||||
|
total += ammo.system.quantity;
|
||||||
|
|
||||||
|
if (favouriteCount < 3 && ammo.getFlag(game.system.id, ItemFlags.FAVOURITE)) {
|
||||||
|
ctx.favouriteAmmo[favouriteCount] = {
|
||||||
|
uuid: ammo.uuid,
|
||||||
|
name: ammo.name,
|
||||||
|
quantity: ammo.system.quantity,
|
||||||
|
};
|
||||||
|
favouriteCount++;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ammo = total;
|
||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createItemFromElement, deleteItemFromElement, editItemFromElement } from "./utils.mjs";
|
import { createItemFromElement, deleteItemFromElement, editItemFromElement, updateForeignDocumentFromEvent } from "./utils.mjs";
|
||||||
import { DicePool } from "./DicePool.mjs";
|
import { DicePool } from "./DicePool.mjs";
|
||||||
import { RichEditor } from "./RichEditor.mjs";
|
import { RichEditor } from "./RichEditor.mjs";
|
||||||
import { toBoolean } from "../consts.mjs";
|
import { toBoolean } from "../consts.mjs";
|
||||||
|
|
@ -31,6 +31,13 @@ export function GenericAppMixin(HandlebarsApp) {
|
||||||
};
|
};
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
// #region Instance Data
|
||||||
|
/** @type {Map<string, PopoverEventManager>} */
|
||||||
|
_popoverManagers = new Map();
|
||||||
|
/** @type {Map<number, string>} */
|
||||||
|
_hookIDs = new Map();
|
||||||
|
// #endregion
|
||||||
|
|
||||||
// #region Lifecycle
|
// #region Lifecycle
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
|
|
@ -38,13 +45,43 @@ export function GenericAppMixin(HandlebarsApp) {
|
||||||
* top after being re-rendered as normal
|
* top after being re-rendered as normal
|
||||||
*/
|
*/
|
||||||
async render(options = {}, _options = {}) {
|
async render(options = {}, _options = {}) {
|
||||||
super.render(options, _options);
|
await super.render(options, _options);
|
||||||
const instance = foundry.applications.instances.get(this.id);
|
const instance = foundry.applications.instances.get(this.id);
|
||||||
if (instance !== undefined && options.orBringToFront) {
|
if (instance !== undefined && options.orBringToFront) {
|
||||||
instance.bringToFront();
|
instance.bringToFront();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
async _onRender(...args) {
|
||||||
|
await super._onRender(...args);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Rendering each of the popover managers associated with this app allows us
|
||||||
|
to have them be dynamic and update when their parent application is rerendered,
|
||||||
|
this could eventually be something we can move into the Document's apps
|
||||||
|
collection so Foundry auto-rerenders it, but because it isn't actually
|
||||||
|
associated with the Document (as it's dependendant on the Application), I
|
||||||
|
decided that it would be best to do my own handling for it.
|
||||||
|
*/
|
||||||
|
for (const manager of this._popoverManagers.values()) {
|
||||||
|
manager.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 _preparePartContext(partId, ctx, opts) {
|
async _preparePartContext(partId, ctx, opts) {
|
||||||
ctx = await super._preparePartContext(partId, ctx, opts);
|
ctx = await super._preparePartContext(partId, ctx, opts);
|
||||||
delete ctx.document;
|
delete ctx.document;
|
||||||
|
|
@ -60,6 +97,22 @@ export function GenericAppMixin(HandlebarsApp) {
|
||||||
|
|
||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_tearDown(options) {
|
||||||
|
// Clear all popovers associated with the app
|
||||||
|
for (const manager of this._popoverManagers.values()) {
|
||||||
|
manager.destroy();
|
||||||
|
};
|
||||||
|
this._popoverManagers.clear();
|
||||||
|
|
||||||
|
// Remove any hooks added for this app
|
||||||
|
for (const [id, hook] of this._hookIDs.entries()) {
|
||||||
|
Hooks.off(hook, id);
|
||||||
|
};
|
||||||
|
this._hookIDs.clear();
|
||||||
|
|
||||||
|
super._tearDown(options);
|
||||||
|
};
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Actions
|
// #region Actions
|
||||||
|
|
|
||||||
96
module/Apps/popovers/AmmoTracker.mjs
Normal file
96
module/Apps/popovers/AmmoTracker.mjs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { filePath } from "../../consts.mjs";
|
||||||
|
import { GenericPopoverMixin } from "./GenericPopoverMixin.mjs";
|
||||||
|
import { ItemFlags } from "../../flags/item.mjs";
|
||||||
|
import { localizer } from "../../utils/Localizer.mjs";
|
||||||
|
import { Logger } from "../../utils/Logger.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
export class AmmoTracker extends GenericPopoverMixin(HandlebarsApplicationMixin(ApplicationV2)) {
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: [
|
||||||
|
`ripcrypt`,
|
||||||
|
],
|
||||||
|
window: {
|
||||||
|
title: `RipCrypt.app-titles.AmmoTracker`,
|
||||||
|
contentClasses: [
|
||||||
|
`ripcrypt--AmmoTracker`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
favourite: this.#favourite,
|
||||||
|
unfavourite: this.#unfavourite,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
ammoList: {
|
||||||
|
template: filePath(`templates/Apps/popovers/AmmoTracker/ammoList.hbs`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Instance Data
|
||||||
|
_favouriteCount = 0;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Lifecycle
|
||||||
|
async _preparePartContext(partId, data) {
|
||||||
|
const ctx = {
|
||||||
|
meta: { idp: this.id },
|
||||||
|
partId,
|
||||||
|
};
|
||||||
|
|
||||||
|
let favouriteCount = 0;
|
||||||
|
ctx.ammos = data.ammos.map(ammo => {
|
||||||
|
const favourite = ammo.getFlag(game.system.id, ItemFlags.FAVOURITE) ?? false;
|
||||||
|
if (favourite) { favouriteCount++ };
|
||||||
|
|
||||||
|
return {
|
||||||
|
ammo,
|
||||||
|
favourite,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this._favouriteCount = favouriteCount;
|
||||||
|
ctx.atFavouriteLimit = favouriteCount >= 3;
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Actions
|
||||||
|
static async #favourite(_, el) {
|
||||||
|
const targetEl = el.closest(`[data-item-id]`);
|
||||||
|
if (!targetEl) {
|
||||||
|
Logger.warn(`Cannot find a parent element with data-item-id`);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._favouriteCount > 3) {
|
||||||
|
ui.notifications.error(localizer(`RipCrypt.notifs.error.at-favourite-limit`));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = targetEl.dataset;
|
||||||
|
const item = await fromUuid(data.itemId);
|
||||||
|
if (!item) { return };
|
||||||
|
|
||||||
|
item.setFlag(game.system.id, ItemFlags.FAVOURITE, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
static async #unfavourite(_, el) {
|
||||||
|
const targetEl = el.closest(`[data-item-id]`);
|
||||||
|
if (!targetEl) {
|
||||||
|
Logger.warn(`Cannot find a parent element with data-item-id`);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = targetEl.dataset;
|
||||||
|
const item = await fromUuid(data.itemId);
|
||||||
|
if (!item) { return };
|
||||||
|
|
||||||
|
item.unsetFlag(game.system.id, ItemFlags.FAVOURITE);
|
||||||
|
};
|
||||||
|
// #endregion
|
||||||
|
};
|
||||||
188
module/Apps/popovers/GenericPopoverMixin.mjs
Normal file
188
module/Apps/popovers/GenericPopoverMixin.mjs
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
@ -42,3 +42,24 @@ export async function deleteItemFromElement(target) {
|
||||||
const item = await fromUuid(itemId);
|
const item = await fromUuid(itemId);
|
||||||
item.delete();
|
item.delete();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a document using the UUID, expects there to be the following
|
||||||
|
* dataset attributes:
|
||||||
|
* - "data-foreign-uuid" : The UUID of the document to update
|
||||||
|
* - "data-foreign-name" : The dot-separated path of the value to update
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
export async function updateForeignDocumentFromEvent(event) {
|
||||||
|
const target = event.currentTarget;
|
||||||
|
const data = target.dataset;
|
||||||
|
const document = await fromUuid(data.foreignUuid);
|
||||||
|
|
||||||
|
let value = target.value;
|
||||||
|
switch (target.type) {
|
||||||
|
case `checkbox`: value = target.checked; break;
|
||||||
|
};
|
||||||
|
|
||||||
|
await document?.update({ [data.foreignName]: value });
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// App imports
|
// App imports
|
||||||
|
import { AmmoTracker } from "./Apps/popovers/AmmoTracker.mjs";
|
||||||
import { CombinedHeroSheet } from "./Apps/ActorSheets/CombinedHeroSheet.mjs";
|
import { CombinedHeroSheet } from "./Apps/ActorSheets/CombinedHeroSheet.mjs";
|
||||||
import { DicePool } from "./Apps/DicePool.mjs";
|
import { DicePool } from "./Apps/DicePool.mjs";
|
||||||
import { HeroSkillsCardV1 } from "./Apps/ActorSheets/HeroSkillsCardV1.mjs";
|
import { HeroSkillsCardV1 } from "./Apps/ActorSheets/HeroSkillsCardV1.mjs";
|
||||||
|
|
@ -10,6 +11,9 @@ 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";
|
||||||
|
|
||||||
|
// Misc Imports
|
||||||
|
import { ItemFlags } from "./flags/item.mjs";
|
||||||
|
|
||||||
const { deepFreeze } = foundry.utils;
|
const { deepFreeze } = foundry.utils;
|
||||||
|
|
||||||
Object.defineProperty(
|
Object.defineProperty(
|
||||||
|
|
@ -18,6 +22,7 @@ Object.defineProperty(
|
||||||
{
|
{
|
||||||
value: deepFreeze({
|
value: deepFreeze({
|
||||||
Apps: {
|
Apps: {
|
||||||
|
AmmoTracker,
|
||||||
DicePool,
|
DicePool,
|
||||||
CombinedHeroSheet,
|
CombinedHeroSheet,
|
||||||
HeroSummaryCardV1,
|
HeroSummaryCardV1,
|
||||||
|
|
@ -31,6 +36,7 @@ Object.defineProperty(
|
||||||
previousFate,
|
previousFate,
|
||||||
rankToInteger,
|
rankToInteger,
|
||||||
},
|
},
|
||||||
|
ItemFlags,
|
||||||
}),
|
}),
|
||||||
writable: false,
|
writable: false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 game.tooltip.constructor.TOOLTIP_ACTIVATION_MS;
|
||||||
|
};
|
||||||
|
|
|
||||||
4
module/flags/item.mjs
Normal file
4
module/flags/item.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const ItemFlags = Object.freeze({
|
||||||
|
/** The boolean value to indicate if an item is considered favourited/starred or not */
|
||||||
|
FAVOURITE: `favourited`,
|
||||||
|
});
|
||||||
|
|
@ -37,6 +37,7 @@ export const gameTerms = Object.preventExtensions({
|
||||||
}),
|
}),
|
||||||
/** The types of items that contribute to the gear limit */
|
/** The types of items that contribute to the gear limit */
|
||||||
gearItemTypes: new Set([
|
gearItemTypes: new Set([
|
||||||
|
`ammo`,
|
||||||
`armour`,
|
`armour`,
|
||||||
`weapon`,
|
`weapon`,
|
||||||
`shield`,
|
`shield`,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ import { registerMetaSettings } from "../settings/metaSettings.mjs";
|
||||||
import { registerUserSettings } from "../settings/userSettings.mjs";
|
import { registerUserSettings } from "../settings/userSettings.mjs";
|
||||||
import { registerWorldSettings } from "../settings/worldSettings.mjs";
|
import { registerWorldSettings } from "../settings/worldSettings.mjs";
|
||||||
|
|
||||||
|
const { Items, Actors } = foundry.documents.collections;
|
||||||
|
const { ItemSheet, ActorSheet } = foundry.appv1.sheets;
|
||||||
|
|
||||||
Hooks.once(`init`, () => {
|
Hooks.once(`init`, () => {
|
||||||
Logger.log(`Initializing`);
|
Logger.log(`Initializing`);
|
||||||
|
|
||||||
|
|
@ -70,10 +73,8 @@ Hooks.once(`init`, () => {
|
||||||
|
|
||||||
// #region Sheets
|
// #region Sheets
|
||||||
// Unregister core sheets
|
// Unregister core sheets
|
||||||
/* eslint-disable no-undef */
|
|
||||||
Items.unregisterSheet(`core`, ItemSheet);
|
Items.unregisterSheet(`core`, ItemSheet);
|
||||||
Actors.unregisterSheet(`core`, ActorSheet);
|
Actors.unregisterSheet(`core`, ActorSheet);
|
||||||
/* eslint-enabled no-undef */
|
|
||||||
|
|
||||||
// #region Actors
|
// #region Actors
|
||||||
Actors.registerSheet(game.system.id, CombinedHeroSheet, {
|
Actors.registerSheet(game.system.id, CombinedHeroSheet, {
|
||||||
|
|
|
||||||
184
module/utils/PopoverEventManager.mjs
Normal file
184
module/utils/PopoverEventManager.mjs
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { getTooltipDelay } from "../consts.mjs";
|
||||||
|
import { Logger } from "./Logger.mjs";
|
||||||
|
|
||||||
|
export class PopoverEventManager {
|
||||||
|
#options;
|
||||||
|
#id;
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.#id;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {Map<string, PopoverEventManager>} */
|
||||||
|
static #existing = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element The element to attach the listeners to.
|
||||||
|
* @param {GenericPopoverMixin} popoverClass The class reference that represents the popover app
|
||||||
|
*/
|
||||||
|
constructor(id, element, popoverClass, options = {}) {
|
||||||
|
id = `${id}-${popoverClass.name}`;
|
||||||
|
this.#id = id;
|
||||||
|
|
||||||
|
if (PopoverEventManager.#existing.has(id)) {
|
||||||
|
const manager = PopoverEventManager.#existing.get(id);
|
||||||
|
manager.#addListeners(element);
|
||||||
|
return manager;
|
||||||
|
};
|
||||||
|
|
||||||
|
options.managerId = id;
|
||||||
|
options.locked ??= false;
|
||||||
|
options.lockable ??= true;
|
||||||
|
|
||||||
|
this.#options = options;
|
||||||
|
this.#element = element;
|
||||||
|
this.#class = popoverClass;
|
||||||
|
|
||||||
|
this.#addListeners(element);
|
||||||
|
PopoverEventManager.#existing.set(id, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
|
#addListeners(element) {
|
||||||
|
element.addEventListener(`pointerenter`, this.#pointerEnterHandler.bind(this));
|
||||||
|
element.addEventListener(`pointerout`, this.#pointerOutHandler.bind(this));
|
||||||
|
element.addEventListener(`click`, this.#clickHandler.bind(this));
|
||||||
|
|
||||||
|
if (this.#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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get rendered() {
|
||||||
|
return Boolean(this.#frameless?.rendered || this.#framed?.rendered);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(options) {
|
||||||
|
if (this.#framed?.rendered) {
|
||||||
|
this.#framed.render(options);
|
||||||
|
};
|
||||||
|
if (this.#frameless?.rendered) {
|
||||||
|
this.#frameless.render(options);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#element;
|
||||||
|
#class;
|
||||||
|
#openTimeout = null;
|
||||||
|
#closeTimeout = null;
|
||||||
|
|
||||||
|
#frameless;
|
||||||
|
#framed;
|
||||||
|
|
||||||
|
#construct(options) {
|
||||||
|
options.popover ??= {};
|
||||||
|
options.popover.managerId = this.#id;
|
||||||
|
|
||||||
|
return new this.#class(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
#clickHandler() {
|
||||||
|
Logger.debug(`click event handler`);
|
||||||
|
// Cleanup for the frameless lifecycle
|
||||||
|
this.#stopOpen();
|
||||||
|
this.#stopClose();
|
||||||
|
this.#frameless?.close({ force: true });
|
||||||
|
|
||||||
|
if (!this.#framed) {
|
||||||
|
this.#framed = this.#construct({ popover: { ...this.#options, framed: true } });
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// When the frameless is already rendered, we should just move it to the
|
||||||
|
// new location instead of spawning a new one
|
||||||
|
if (this.#frameless?.rendered) {
|
||||||
|
const { width, height } = this.#frameless.element.getBoundingClientRect();
|
||||||
|
const top = y - height;
|
||||||
|
const left = x - Math.floor(width / 2);
|
||||||
|
this.#frameless.setPosition({ left, top });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#frameless = this.#construct({
|
||||||
|
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) {
|
||||||
|
if (event.button !== 1 || !this.#frameless?.rendered || Tour.tourInProgress) { return };
|
||||||
|
event.preventDefault();
|
||||||
|
this.#frameless.toggleLock();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": 13,
|
"minimum": 13,
|
||||||
"verified": 13,
|
"verified": 13.339,
|
||||||
"maximum": 13
|
"maximum": 13
|
||||||
},
|
},
|
||||||
"authors": [
|
"authors": [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<div>
|
<div>
|
||||||
{{!-- This is here to prevent height collapsing --}}
|
|
||||||
​
|
|
||||||
|
|
||||||
{{#if meta.editable}}
|
{{#if meta.editable}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -18,5 +15,8 @@
|
||||||
var:fill="currentColor"
|
var:fill="currentColor"
|
||||||
></rc-icon>
|
></rc-icon>
|
||||||
</button>
|
</button>
|
||||||
|
{{else}}
|
||||||
|
{{!-- This is here to prevent height collapsing --}}
|
||||||
|
​
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<div>
|
<div>
|
||||||
{{!-- This is here to prevent height collapsing --}}
|
|
||||||
​
|
|
||||||
|
|
||||||
{{#if meta.editable}}
|
{{#if meta.editable}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -18,6 +15,9 @@
|
||||||
var:fill="currentColor"
|
var:fill="currentColor"
|
||||||
></rc-icon>
|
></rc-icon>
|
||||||
</button>
|
</button>
|
||||||
|
{{else}}
|
||||||
|
{{!-- This is here to prevent height collapsing --}}
|
||||||
|
​
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,13 @@
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div class="ammo half-pill">
|
<div class="ammo 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>
|
||||||
|
|
@ -112,10 +118,35 @@
|
||||||
{{ ammo }}
|
{{ ammo }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{#each favouriteAmmo as | data |}}
|
||||||
|
{{#if data}}
|
||||||
|
<div
|
||||||
|
class="pill fav-ammo"
|
||||||
|
data-item-id="{{data.uuid}}"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{{data.name}}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{@root.meta.idp}}-{{data.uuid}}-quantity"
|
||||||
|
value="{{data.quantity}}"
|
||||||
|
aria-label="Quantity of {{data.name}}"
|
||||||
|
data-foreign-update-on="change,blur"
|
||||||
|
data-foreign-uuid="{{data.uuid}}"
|
||||||
|
data-foreign-name="system.quantity"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="pill" style="opacity: 0.5; background: var(--alt-row-background);">
|
||||||
|
{{ rc-i18n "RipCrypt.Apps.starred-ammo-placeholder" }}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
{{!-- * Currencies --}}
|
{{!-- * Currencies --}}
|
||||||
<div class="currencies">
|
<div class="currencies">
|
||||||
<div class="currency half-pill">
|
<div class="currency pill">
|
||||||
<label for="{{meta.idp}}-gold" >
|
<label for="{{meta.idp}}-gold" >
|
||||||
{{ rc-i18n "RipCrypt.common.currency.gold"}}
|
{{ rc-i18n "RipCrypt.common.currency.gold"}}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -126,7 +157,7 @@
|
||||||
value="0"
|
value="0"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="currency half-pill">
|
<div class="currency pill">
|
||||||
<label for="{{meta.idp}}-silver" >
|
<label for="{{meta.idp}}-silver" >
|
||||||
{{ rc-i18n "RipCrypt.common.currency.silver"}}
|
{{ rc-i18n "RipCrypt.common.currency.silver"}}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -137,7 +168,7 @@
|
||||||
value="0"
|
value="0"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="currency half-pill">
|
<div class="currency pill">
|
||||||
<label for="{{meta.idp}}-copper" >
|
<label for="{{meta.idp}}-copper" >
|
||||||
{{ rc-i18n "RipCrypt.common.currency.copper"}}
|
{{ rc-i18n "RipCrypt.common.currency.copper"}}
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
grid-template-rows: repeat(13, minmax(0, 1fr));
|
grid-template-rows: repeat(13, minmax(0, 1fr));
|
||||||
column-gap: var(--col-gap);
|
column-gap: var(--col-gap);
|
||||||
|
row-gap: var(--row-gap);
|
||||||
|
|
||||||
background: var(--base-background);
|
background: var(--base-background);
|
||||||
color: var(--base-text);
|
color: var(--base-text);
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
.skill-list {
|
.skill-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -106,18 +108,30 @@
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.half-pill {
|
.pill {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--section-header-background);
|
background: var(--section-header-background);
|
||||||
border-radius: 0 999px 999px 0;
|
border-radius: 999px;
|
||||||
color: var(--section-header-text);
|
color: var(--section-header-text);
|
||||||
|
padding: 2px 0 2px 4px;
|
||||||
--input-background: var(--base-background);
|
--input-background: var(--base-background);
|
||||||
--input-text: var(--base-text);
|
--input-text: var(--base-text);
|
||||||
|
|
||||||
.input {
|
&.with-icon {
|
||||||
margin: 2px;
|
grid-template-columns: min-content minmax(0, 1.5fr) minmax(0, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label, .label {
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, .input {
|
||||||
|
margin: 0 2px 0 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
53
templates/Apps/popovers/AmmoTracker/ammoList.hbs
Normal file
53
templates/Apps/popovers/AmmoTracker/ammoList.hbs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<div data-tooltip-direction="RIGHT">
|
||||||
|
{{log @root}}
|
||||||
|
{{#if ammos}}
|
||||||
|
<ul>
|
||||||
|
{{#each ammos as | data |}}
|
||||||
|
<li class="ammo" data-item-id="{{data.ammo.uuid}}">
|
||||||
|
<span class="name">{{ data.ammo.name }}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="{{@root.meta.idp}}-{{data.ammo.uuid}}-quantity"
|
||||||
|
class="value"
|
||||||
|
value="{{ data.ammo.system.quantity }}"
|
||||||
|
data-foreign-update-on="change,blur"
|
||||||
|
data-foreign-uuid="{{data.ammo.uuid}}"
|
||||||
|
data-foreign-name="system.quantity"
|
||||||
|
>
|
||||||
|
{{#if data.favourite}}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon"
|
||||||
|
data-action="unfavourite"
|
||||||
|
aria-label="{{ rc-i18n "RipCrypt.Apps.AmmoTracker.unstar-button" name=data.ammo.name }}"
|
||||||
|
data-tooltip="{{ rc-i18n "RipCrypt.Apps.AmmoTracker.unstar-button-tooltip" name=data.ammo.name }}"
|
||||||
|
>
|
||||||
|
<rc-icon
|
||||||
|
name="icons/star"
|
||||||
|
var:size="1rem"
|
||||||
|
></rc-icon>
|
||||||
|
</button>
|
||||||
|
{{else}}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon"
|
||||||
|
{{ disabled @root.atFavouriteLimit }}
|
||||||
|
data-action="favourite"
|
||||||
|
aria-label="{{ rc-i18n "RipCrypt.Apps.AmmoTracker.star-button" name=data.ammo.name }}"
|
||||||
|
data-tooltip="{{ rc-i18n "RipCrypt.Apps.AmmoTracker.star-button-tooltip" name=data.ammo.name }}"
|
||||||
|
>
|
||||||
|
<rc-icon
|
||||||
|
name="icons/star-empty"
|
||||||
|
var:size="1rem"
|
||||||
|
></rc-icon>
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{else}}
|
||||||
|
<span class="placeholder">
|
||||||
|
{{ rc-i18n "RipCrypt.Apps.no-ammo" }}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
47
templates/Apps/popovers/AmmoTracker/style.css
Normal file
47
templates/Apps/popovers/AmmoTracker/style.css
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
.ripcrypt--AmmoTracker.ripcrypt--AmmoTracker.ripcrypt--AmmoTracker {
|
||||||
|
color: var(--popover-text);
|
||||||
|
background: var(--popover-background);
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
--row-gap: 4px;
|
||||||
|
--button-text: var(--header-text);
|
||||||
|
--button-background: var(--header-background);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: var(--row-gap);
|
||||||
|
|
||||||
|
> li {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
color: var(--popover-alt-row-text);
|
||||||
|
background: var(--popover-alt-row-background);
|
||||||
|
--input-text: var(--popover-text);
|
||||||
|
--input-background: var(--popover-background);
|
||||||
|
--button-text: unset;
|
||||||
|
--button-background: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ammo {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 130px 50px min-content;
|
||||||
|
grid-template-rows: min-content;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-self: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
@import url("./vars.css");
|
@import url("./vars.css");
|
||||||
|
|
||||||
|
@import url("./popover.css");
|
||||||
|
|
||||||
@import url("./elements/button.css");
|
@import url("./elements/button.css");
|
||||||
@import url("./elements/input.css");
|
@import url("./elements/input.css");
|
||||||
@import url("./elements/lists.css");
|
@import url("./elements/lists.css");
|
||||||
|
|
@ -27,6 +29,7 @@
|
||||||
/* height: 270px; */
|
/* height: 270px; */
|
||||||
width: 680px;
|
width: 680px;
|
||||||
--col-gap: 2px;
|
--col-gap: 2px;
|
||||||
|
--row-gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
label, input, select {
|
label, input, select {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
.ripcrypt.hud button,
|
.ripcrypt:where(.popover.frameless, .hud) button,
|
||||||
.ripcrypt > .window-content button {
|
.ripcrypt > .window-content button {
|
||||||
all: revert;
|
all: revert;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
.ripcrypt.popover.frameless,
|
||||||
.ripcrypt.hud,
|
.ripcrypt.hud,
|
||||||
.ripcrypt > .window-content {
|
.ripcrypt > .window-content {
|
||||||
input, .input {
|
input, .input {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
.ripcrypt.popover.frameless,
|
||||||
.ripcrypt.hud,
|
.ripcrypt.hud,
|
||||||
.ripcrypt > .window-content {
|
.ripcrypt > .window-content {
|
||||||
ol {
|
ol {
|
||||||
|
|
@ -35,6 +36,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
> li {
|
> li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
.ripcrypt.popover.frameless p,
|
||||||
.ripcrypt.hud p,
|
.ripcrypt.hud p,
|
||||||
.ripcrypt > .window-content p {
|
.ripcrypt > .window-content p {
|
||||||
&.warning {
|
&.warning {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
.ripcrypt.popover.frameless .pill-bar,
|
||||||
.ripcrypt.hud .pill-bar,
|
.ripcrypt.hud .pill-bar,
|
||||||
.ripcrypt > .window-content .pill-bar {
|
.ripcrypt > .window-content .pill-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
.ripcrypt.popover.frameless select,
|
||||||
.ripcrypt.hud select,
|
.ripcrypt.hud select,
|
||||||
.ripcrypt > .window-content select {
|
.ripcrypt > .window-content select {
|
||||||
all: revert;
|
all: revert;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
.ripcrypt.popover.frameless span,
|
||||||
.ripcrypt.hud span,
|
.ripcrypt.hud span,
|
||||||
.ripcrypt > .window-content span {
|
.ripcrypt > .window-content span {
|
||||||
&.small {
|
&.small {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
.ripcrypt.popover.frameless table,
|
||||||
.ripcrypt.hud table,
|
.ripcrypt.hud table,
|
||||||
.ripcrypt > .window-content table {
|
.ripcrypt > .window-content table {
|
||||||
all: revert;
|
all: revert;
|
||||||
|
|
|
||||||
17
templates/css/popover.css
Normal file
17
templates/css/popover.css
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
.ripcrypt.popover {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.frameless {
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: calc(var(--z-index-tooltip) - 5);
|
||||||
|
transform-origin: top left;
|
||||||
|
|
||||||
|
&.locked {
|
||||||
|
border-color: var(--accent-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,16 @@
|
||||||
--col-gap: 2px;
|
--col-gap: 2px;
|
||||||
--row-gap: 0px;
|
--row-gap: 0px;
|
||||||
|
|
||||||
|
/* Popover Variables */
|
||||||
|
--popover-text: var(--base-text);
|
||||||
|
--popover-background: var(--base-background);
|
||||||
|
|
||||||
|
--popover-alt-row-text: var(--alt-row-text);
|
||||||
|
--popover-alt-row-background: var(--alt-row-background);
|
||||||
|
|
||||||
|
--popover-header-text: var(--header-text);
|
||||||
|
--popover-header-background: var(--header-background);
|
||||||
|
|
||||||
/* Additional Variables */
|
/* Additional Variables */
|
||||||
--string-tags-border: inherit;
|
--string-tags-border: inherit;
|
||||||
--string-tags-tag-background: inherit;
|
--string-tags-tag-background: inherit;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue