GetDocFromElement is now async to work with compendiums. Added GetDocFromElementSync for use in contextMenu condition cases (#420)

This commit is contained in:
WBHarry 2025-07-26 22:36:48 +02:00 committed by GitHub
parent a54f4e3831
commit e98adc55b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 89 additions and 54 deletions

View file

@ -75,7 +75,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings {
* @returns * @returns
*/ */
static async #deleteAdversary(_event, target) { static async #deleteAdversary(_event, target) {
const doc = getDocFromElement(target); const doc = await getDocFromElement(target);
const { category } = target.dataset; const { category } = target.dataset;
const path = `system.potentialAdversaries.${category}.adversaries`; const path = `system.potentialAdversaries.${category}.adversaries`;

View file

@ -4,7 +4,7 @@ import { abilities } from '../../../config/actorConfig.mjs';
import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import { getDocFromElement, itemAbleRollParse } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs';
/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */
@ -258,19 +258,27 @@ export default class CharacterSheet extends DHBaseActorSheet {
{ {
name: 'toLoadout', name: 'toLoadout',
icon: 'fa-solid fa-arrow-up', icon: 'fa-solid fa-arrow-up',
condition: target => getDocFromElement(target).system.inVault, condition: target => {
callback: target => { const doc = getDocFromElementSync(target);
const doc = getDocFromElement(target), return doc && system.inVault;
actorLoadout = doc.actor.system.loadoutSlot; },
if(actorLoadout.available) return doc.update({ 'system.inVault': false }); callback: async target => {
ui.notifications.warn(game.i18n.format('DAGGERHEART.UI.Notifications.loadoutMaxReached', { max: actorLoadout.max })) const doc = await getDocFromElement(target);
const actorLoadout = doc.actor.system.loadoutSlot;
if (actorLoadout.available) return doc.update({ 'system.inVault': false });
ui.notifications.warn(
game.i18n.format('DAGGERHEART.UI.Notifications.loadoutMaxReached', { max: actorLoadout.max })
);
} }
}, },
{ {
name: 'toVault', name: 'toVault',
icon: 'fa-solid fa-arrow-down', icon: 'fa-solid fa-arrow-down',
condition: target => !getDocFromElement(target).system.inVault, condition: target => {
callback: target => getDocFromElement(target).update({ 'system.inVault': true }) const doc = getDocFromElementSync(target);
return doc && !doc.system.inVault;
},
callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true })
} }
].map(option => ({ ].map(option => ({
...option, ...option,
@ -292,13 +300,19 @@ export default class CharacterSheet extends DHBaseActorSheet {
{ {
name: 'equip', name: 'equip',
icon: 'fa-solid fa-hands', icon: 'fa-solid fa-hands',
condition: target => !getDocFromElement(target).system.equipped, condition: target => {
const doc = getDocFromElementSync(target);
return doc && !doc.system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target) callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
}, },
{ {
name: 'unequip', name: 'unequip',
icon: 'fa-solid fa-hands', icon: 'fa-solid fa-hands',
condition: target => getDocFromElement(target).system.equipped, condition: target => {
const doc = getDocFromElementSync(target);
return doc && system.equipped;
},
callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target) callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target)
} }
].map(option => ({ ].map(option => ({
@ -407,11 +421,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @param {HTMLElement} html The container to filter items from. * @param {HTMLElement} html The container to filter items from.
* @protected * @protected
*/ */
_onSearchFilterInventory(event, query, rgx, html) { async _onSearchFilterInventory(_event, query, rgx, html) {
this.#filteredItems.inventory.search.clear(); this.#filteredItems.inventory.search.clear();
for (const li of html.querySelectorAll('.inventory-item')) { for (const li of html.querySelectorAll('.inventory-item')) {
const item = getDocFromElement(li); const item = await getDocFromElement(li);
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.inventory.search.add(item.id); if (matchesSearch) this.#filteredItems.inventory.search.add(item.id);
const { menu } = this.#filteredItems.inventory; const { menu } = this.#filteredItems.inventory;
@ -427,11 +441,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @param {HTMLElement} html The container to filter items from. * @param {HTMLElement} html The container to filter items from.
* @protected * @protected
*/ */
_onSearchFilterCard(event, query, rgx, html) { async _onSearchFilterCard(_event, query, rgx, html) {
this.#filteredItems.loadout.search.clear(); this.#filteredItems.loadout.search.clear();
for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) {
const item = getDocFromElement(li); const item = await getDocFromElement(li);
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
if (matchesSearch) this.#filteredItems.loadout.search.add(item.id); if (matchesSearch) this.#filteredItems.loadout.search.add(item.id);
const { menu } = this.#filteredItems.loadout; const { menu } = this.#filteredItems.loadout;
@ -478,11 +492,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @param {HTMLElement} html * @param {HTMLElement} html
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters
*/ */
_onMenuFilterInventory(event, html, filters) { async _onMenuFilterInventory(_event, html, filters) {
this.#filteredItems.inventory.menu.clear(); this.#filteredItems.inventory.menu.clear();
for (const li of html.querySelectorAll('.inventory-item')) { for (const li of html.querySelectorAll('.inventory-item')) {
const item = getDocFromElement(li); const item = await getDocFromElement(li);
const matchesMenu = const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
@ -499,11 +513,11 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @param {HTMLElement} html * @param {HTMLElement} html
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters
*/ */
_onMenuFilterLoadout(event, html, filters) { async _onMenuFilterLoadout(_event, html, filters) {
this.#filteredItems.loadout.menu.clear(); this.#filteredItems.loadout.menu.clear();
for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) {
const item = getDocFromElement(li); const item = await getDocFromElement(li);
const matchesMenu = const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
@ -519,7 +533,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
async updateItemResource(event) { async updateItemResource(event) {
const item = getDocFromElement(event.currentTarget); const item = await getDocFromElement(event.currentTarget);
if (!item) return; if (!item) return;
const max = event.currentTarget.max ? Number(event.currentTarget.max) : null; const max = event.currentTarget.max ? Number(event.currentTarget.max) : null;
@ -529,7 +543,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
async updateItemQuantity(event) { async updateItemQuantity(event) {
const item = getDocFromElement(event.currentTarget); const item = await getDocFromElement(event.currentTarget);
if (!item) return; if (!item) return;
await item.update({ 'system.quantity': event.currentTarget.value }); await item.update({ 'system.quantity': event.currentTarget.value });
@ -609,7 +623,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleEquipItem(_event, button) { static async #toggleEquipItem(_event, button) {
const item = getDocFromElement(button); const item = await getDocFromElement(button);
if (!item) return; if (!item) return;
if (item.system.equipped) { if (item.system.equipped) {
await item.update({ 'system.equipped': false }); await item.update({ 'system.equipped': false });
@ -664,7 +678,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleVault(_event, button) { static async #toggleVault(_event, button) {
const doc = getDocFromElement(button); const doc = await getDocFromElement(button);
await doc?.update({ 'system.inVault': !doc.system.inVault }); await doc?.update({ 'system.inVault': !doc.system.inVault });
} }
@ -673,7 +687,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleResourceDice(event, target) { static async #toggleResourceDice(event, target) {
const item = getDocFromElement(target); const item = await getDocFromElement(target);
const { dice } = event.target.closest('.item-resource').dataset; const { dice } = event.target.closest('.item-resource').dataset;
const diceState = item.system.resource.diceStates[dice]; const diceState = item.system.resource.diceStates[dice];
@ -688,7 +702,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #handleResourceDice(_, target) { static async #handleResourceDice(_, target) {
const item = getDocFromElement(target); const item = await getDocFromElement(target);
if (!item) return; if (!item) return;
const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document); const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document);
@ -709,7 +723,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
} }
async _onDragStart(event) { async _onDragStart(event) {
const item = getDocFromElement(event.target); const item = await getDocFromElement(event.target);
const dragData = { const dragData = {
type: item.documentName, type: item.documentName,

View file

@ -1,6 +1,5 @@
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
import { getDocFromElement, tagifyElement } from '../../../helpers/utils.mjs'; import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs';
import DHActionConfig from '../../sheets-configs/action-config.mjs';
/** /**
* @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction
@ -259,14 +258,20 @@ export default function DHApplicationMixin(Base) {
{ {
name: 'disableEffect', name: 'disableEffect',
icon: 'fa-solid fa-lightbulb', icon: 'fa-solid fa-lightbulb',
condition: target => !getDocFromElement(target).disabled, condition: target => {
callback: target => getDocFromElement(target).update({ disabled: true }) const doc = getDocFromElementSync(target);
return doc && !doc.disabled;
},
callback: async target => (await getDocFromElement(target)).update({ disabled: true })
}, },
{ {
name: 'enableEffect', name: 'enableEffect',
icon: 'fa-regular fa-lightbulb', icon: 'fa-regular fa-lightbulb',
condition: target => getDocFromElement(target).disabled, condition: target => {
callback: target => getDocFromElement(target).update({ disabled: false }) const doc = getDocFromElementSync(target);
return doc && doc.disabled;
},
callback: async target => (await getDocFromElement(target)).update({ disabled: false })
} }
].map(option => ({ ].map(option => ({
...option, ...option,
@ -299,10 +304,10 @@ export default function DHApplicationMixin(Base) {
name: 'CONTROLS.CommonEdit', name: 'CONTROLS.CommonEdit',
icon: 'fa-solid fa-pen-to-square', icon: 'fa-solid fa-pen-to-square',
condition: target => { condition: target => {
const doc = getDocFromElement(target); const doc = getDocFromElementSync(target);
return !doc.hasOwnProperty('systemPath') || doc.inCollection; return doc && (!doc.hasOwnProperty('systemPath') || doc.inCollection);
}, },
callback: target => getDocFromElement(target).sheet.render({ force: true }) callback: async target => (await getDocFromElement(target)).sheet.render({ force: true })
} }
]; ];
@ -311,25 +316,25 @@ export default function DHApplicationMixin(Base) {
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem',
icon: 'fa-solid fa-burst', icon: 'fa-solid fa-burst',
condition: target => { condition: target => {
const doc = getDocFromElement(target); const doc = getDocFromElementSync(target);
return !(doc.type === 'domainCard' && doc.system.inVault) return doc && !(doc.type === 'domainCard' && doc.system.inVault);
}, },
callback: (target, event) => getDocFromElement(target).use(event) callback: async (target, event) => (await getDocFromElement(target)).use(event)
}); });
if (toChat) if (toChat)
options.unshift({ options.unshift({
name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat',
icon: 'fa-solid fa-message', icon: 'fa-solid fa-message',
callback: target => getDocFromElement(target).toChat(this.document.id) callback: async target => (await getDocFromElement(target)).toChat(this.document.id)
}); });
if (deletable) if (deletable)
options.push({ options.push({
name: 'CONTROLS.CommonDelete', name: 'CONTROLS.CommonDelete',
icon: 'fa-solid fa-trash', icon: 'fa-solid fa-trash',
callback: (target, event) => { callback: async (target, event) => {
const doc = getDocFromElement(target); const doc = await getDocFromElement(target);
if (event.shiftKey) return doc.delete(); if (event.shiftKey) return doc.delete();
else return doc.deleteDialog(); else return doc.deleteDialog();
} }
@ -371,7 +376,7 @@ export default function DHApplicationMixin(Base) {
if (!actionId && !itemUuid) return; if (!actionId && !itemUuid) return;
const doc = itemUuid const doc = itemUuid
? getDocFromElement(extensibleElement) ? await getDocFromElement(extensibleElement)
: this.document.system.attack?.id === actionId : this.document.system.attack?.id === actionId
? this.document.system.attack ? this.document.system.attack
: this.document.system.actions?.get(actionId); : this.document.system.actions?.get(actionId);
@ -429,8 +434,8 @@ export default function DHApplicationMixin(Base) {
* Renders an embedded document. * Renders an embedded document.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static #editDoc(_event, target) { static async #editDoc(_event, target) {
const doc = getDocFromElement(target); const doc = await getDocFromElement(target);
if (doc) return doc.sheet.render({ force: true }); if (doc) return doc.sheet.render({ force: true });
} }
@ -439,7 +444,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #deleteDoc(event, target) { static async #deleteDoc(event, target) {
const doc = getDocFromElement(target); const doc = await getDocFromElement(target);
if (doc) { if (doc) {
if (event.shiftKey) return doc.delete(); if (event.shiftKey) return doc.delete();
else return await doc.deleteDialog(); else return await doc.deleteDialog();
@ -451,7 +456,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toChat(_event, target) { static async #toChat(_event, target) {
let doc = getDocFromElement(target); let doc = await getDocFromElement(target);
return doc.toChat(this.document.id); return doc.toChat(this.document.id);
} }
@ -460,7 +465,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #useItem(event, target) { static async #useItem(event, target) {
let doc = getDocFromElement(target); let doc = await getDocFromElement(target);
await doc.use(event); await doc.use(event);
} }
@ -469,7 +474,7 @@ export default function DHApplicationMixin(Base) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #toggleEffect(_, target) { static async #toggleEffect(_, target) {
const doc = getDocFromElement(target); const doc = await getDocFromElement(target);
await doc.update({ disabled: !doc.disabled }); await doc.update({ disabled: !doc.disabled });
} }
@ -492,7 +497,7 @@ export default function DHApplicationMixin(Base) {
const t = extensible?.classList.toggle('extended'); const t = extensible?.classList.toggle('extended');
const descriptionElement = extensible?.querySelector('.invetory-description'); const descriptionElement = extensible?.querySelector('.invetory-description');
if (t && !!descriptionElement) this.#prepareInventoryDescription(extensible, descriptionElement); if (t && !!descriptionElement) await this.#prepareInventoryDescription(extensible, descriptionElement);
} }
} }

View file

@ -117,7 +117,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
name: 'CONTROLS.CommonDelete', name: 'CONTROLS.CommonDelete',
icon: '<i class="fa-solid fa-trash"></i>', icon: '<i class="fa-solid fa-trash"></i>',
callback: async target => { callback: async target => {
const feature = getDocFromElement(target); const feature = await getDocFromElement(target);
if (!feature) return; if (!feature) return;
const confirmed = await foundry.applications.api.DialogV2.confirm({ const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { window: {
@ -168,7 +168,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
*/ */
static async #deleteFeature(_, element) { static async #deleteFeature(_, element) {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
const feature = getDocFromElement(target); const feature = await getDocFromElement(target);
if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
await this.document.update({ await this.document.update({
'system.features': this.document.system.features 'system.features': this.document.system.features

View file

@ -235,9 +235,25 @@ export const updateActorTokens = async (actor, update) => {
* @param {HTMLElement} element - The DOM element to start the search from. * @param {HTMLElement} element - The DOM element to start the search from.
* @returns {foundry.abstract.Document|null} The resolved document, or null if not found or invalid. * @returns {foundry.abstract.Document|null} The resolved document, or null if not found or invalid.
*/ */
export function getDocFromElement(element) { export async function getDocFromElement(element) {
const target = element.closest('[data-item-uuid]'); const target = element.closest('[data-item-uuid]');
return foundry.utils.fromUuidSync(target.dataset.itemUuid) ?? null; return (await foundry.utils.fromUuid(target.dataset.itemUuid)) ?? null;
}
/**
* Retrieves a Foundry document associated with the nearest ancestor element
* that has a `data-item-uuid` attribute.
* @param {HTMLElement} element - The DOM element to start the search from.
* @returns {foundry.abstract.Document|null} The resolved document, or null if not found, invalid
* or in embedded compendium collection.
*/
export function getDocFromElementSync(element) {
const target = element.closest('[data-item-uuid]');
try {
return foundry.utils.fromUuidSync(target.dataset.itemUuid) ?? null;
} catch (_) {
return null;
}
} }
/** /**