Skip to content

Commit c66b66d

Browse files
UnarekinShourn
andauthored
Invocation Selection Application Template Redesign (#568)
* feat:Overhaul invocation selection application template * feat: Table renderer improvements - allow more flexibility for FUTableRenderer users by introducing a "custom" preset that requires configuration through the new "advancedConfig" key - existing "item" and "effect" presets continue to work and take precedence over "advancedConfig" * feat:Added customization for name header * feat:Removed unnecessary debug output, added tooltip to send-to-chat button * fix:Localize header name --------- Co-authored-by: Sören Jahns <jahns.simon@web.de>
1 parent 9a3fbd8 commit c66b66d

File tree

7 files changed

+230
-104
lines changed

7 files changed

+230
-104
lines changed

module/documents/items/classFeature/invoker/invocation-selection-application.mjs

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { systemTemplatePath } from '../../../../helpers/system-utils.mjs';
22
import FUApplication from '../../../../ui/application.mjs';
3+
import { InvocationTableRenderer } from '../../../../helpers/tables/invocation-table-renderer.mjs';
34
import { WELLSPRINGS } from './invoker-integration.mjs';
4-
import { TextEditor } from '../../../../helpers/text-editor.mjs';
55

66
export class InvocationSelectionApplication extends FUApplication {
77
static DEFAULT_OPTIONS = {
@@ -10,21 +10,28 @@ export class InvocationSelectionApplication extends FUApplication {
1010
title: 'FU.ClassFeatureInvocationsSelectDialogTitle',
1111
},
1212
position: {
13-
width: 350,
13+
width: 500,
1414
height: 'auto',
1515
},
1616
actions: {
1717
useInvocation: InvocationSelectionApplication.UseInvocation,
18+
// The item name template from the table renderer we're using
19+
// hard codes the data-action on its icon to `roll`
20+
roll: InvocationSelectionApplication.UseInvocation,
1821
},
1922
};
2023

2124
static PARTS = {
2225
app: {
2326
template: systemTemplatePath('feature/invoker/invocations-selection-application'),
2427
},
28+
footer: {
29+
template: `templates/generic/form-footer.hbs`,
30+
},
2531
};
2632

2733
#model;
34+
#invocationTable = new InvocationTableRenderer();
2835

2936
constructor(model) {
3037
super();
@@ -36,54 +43,34 @@ export class InvocationSelectionApplication extends FUApplication {
3643
async _prepareContext(options = {}) {
3744
const activeWellsprings = Object.entries(this.#model.actor.wellspringManager.activeWellsprings)
3845
.filter(([, value]) => value)
39-
.reduce((agg, [key, value]) => (agg[key] = value) && agg, {});
40-
41-
const availableInvocations = {
42-
basic: ['basic'],
43-
advanced: ['basic', 'advanced'],
44-
superior: ['basic', 'advanced', 'superior1', 'superior2'],
45-
}[this.#model.level];
46-
47-
const data = {};
48-
for (const [element] of Object.entries(activeWellsprings)) {
49-
const modelElement = this.#model[element];
50-
const invocations = {};
51-
for (const invocation of availableInvocations) {
52-
const modelInvocation = modelElement[invocation];
53-
invocations[invocation] = {
54-
name: modelInvocation.name,
55-
description: await TextEditor.enrichHTML(modelInvocation.description),
56-
};
57-
}
58-
data[element] = {
59-
name: WELLSPRINGS[element].name,
60-
icon: WELLSPRINGS[element].icon,
61-
invocations,
62-
};
63-
}
46+
.map(([key]) => key);
6447

65-
return { wellsprings: data };
48+
return {
49+
buttons: [{ type: 'submit', icon: 'fa-solid fa-times', label: 'Close' }],
50+
wellsprings: await Promise.all(
51+
activeWellsprings.map(async (element) => {
52+
return {
53+
wellspring: WELLSPRINGS[element],
54+
table: await this.#invocationTable.renderTable(this.#model, { wellspring: element }),
55+
};
56+
}),
57+
),
58+
};
6659
}
6760

6861
static UseInvocation(event, elem) {
62+
const invocation = elem.closest(`[data-invocation]`).dataset.invocation;
63+
const element = elem.closest(`[data-element]`).dataset.element;
6964
this.close({
7065
use: {
71-
element: elem.dataset.element,
72-
invocation: elem.dataset.invocation,
66+
element,
67+
invocation,
7368
},
7469
});
7570
}
7671

77-
_onRender(context, options) {
78-
// Set width
79-
const activeWellsprings = Object.values(this.#model.actor.wellspringManager.activeWellsprings).filter((value) => value).length;
80-
foundry.utils.mergeObject(options, {
81-
position: {
82-
width: activeWellsprings * InvocationSelectionApplication.DEFAULT_OPTIONS.position.width,
83-
},
84-
});
85-
86-
return super._onRender(context, options);
72+
async _onFirstRender(context, options) {
73+
this.#invocationTable.activateListeners(this);
8774
}
8875

8976
useInvocation(event) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { FUTableRenderer } from './table-renderer.mjs';
2+
import { systemTemplatePath } from '../system-utils.mjs';
3+
4+
export class InvocationTableRenderer extends FUTableRenderer {
5+
/** @type TableConfig */
6+
static TABLE_CONFIG = {
7+
cssClass: 'invocations-table',
8+
tablePreset: 'custom',
9+
getItems: (invocationsDataModel, { wellspring }) => {
10+
const availableInvocations = {
11+
basic: ['basic'],
12+
advanced: ['basic', 'advanced'],
13+
superior: ['basic', 'advanced', 'superior1', 'superior2'],
14+
}[invocationsDataModel.level];
15+
16+
return availableInvocations.map((invocation) => ({
17+
key: `${wellspring}:${invocation}`,
18+
img: invocationsDataModel.item.img,
19+
wellspring,
20+
invocation,
21+
name: invocationsDataModel[wellspring][invocation].name,
22+
description: invocationsDataModel[wellspring][invocation].description,
23+
}));
24+
},
25+
renderDescription: async (item) => {
26+
const enriched = await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description);
27+
return `<div class="description-with-tags" style="pointer-events: none">${enriched}</div>`;
28+
},
29+
columns: {
30+
name: {
31+
headerAlignment: 'start',
32+
renderHeader: InvocationTableRenderer.#renderNameHeader,
33+
renderCell: InvocationTableRenderer.#renderName,
34+
},
35+
controls: {
36+
headerAlignment: 'end',
37+
renderHeader: InvocationTableRenderer.#renderControlsHeader,
38+
renderCell: InvocationTableRenderer.#renderControls,
39+
},
40+
},
41+
advancedConfig: {
42+
getKey: (invocationItem) => invocationItem.key,
43+
additionalRowAttributes: [
44+
{ attributeName: 'data-element', getAttributeValue: (invocationItem) => invocationItem.wellspring },
45+
{ attributeName: 'data-invocation', getAttributeValue: (invocationItem) => invocationItem.invocation },
46+
],
47+
},
48+
};
49+
50+
static #renderControlsHeader() {
51+
return `<div class="header-item-controls"></div>`;
52+
}
53+
54+
static #renderControls(invocation) {
55+
return `<div class="cell-item-controls"><a class="cell-item-controls__control" data-action="roll" data-tooltip="${game.i18n.localize('FU.ChatMessageSendHint')}"><i class="fa-solid fa-share"></i></a></div>`;
56+
}
57+
58+
static #renderNameHeader() {
59+
return game.i18n.localize('FU.Name');
60+
}
61+
62+
static async #renderName(invocation) {
63+
return foundry.applications.handlebars.renderTemplate(systemTemplatePath('table/cell/cell-invocation-name'), invocation);
64+
}
65+
}

module/helpers/tables/table-renderer.mjs

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { PseudoItem } from '../../documents/items/pseudo-item.mjs';
77
* @template {Object} D the document of the sheet being rendered
88
* @template {Object} T the type of the items in the table
99
* @property {string, (() => string)} cssClass
10-
* @property {"item", "effect"} [tablePreset="item"]
10+
* @property {"item", "effect", "custom"} [tablePreset="item"]
1111
* @property {(document: D, options: FUTableRendererRenderOptions) => T[]} getItems
1212
* @property {boolean, ((a: D, b: D) => number)} [sort=true] sorting function to determine the order of entries, true means sort using foundry sort order, false means don't sort
1313
* @property {(element: HTMLElement) => void} activateListeners
@@ -17,6 +17,7 @@ import { PseudoItem } from '../../documents/items/pseudo-item.mjs';
1717
* @property {Record<string, ColumnConfig<T>>} columns
1818
* @property {Record<string, ((event: PointerEvent, target: HTMLElement) => void)>} actions
1919
* @property {DragDropConfiguration[]} [dragDrop]
20+
* @property {AdvancedTableConfig<T>} [advancedConfig]
2021
*/
2122

2223
/**
@@ -29,6 +30,24 @@ import { PseudoItem } from '../../documents/items/pseudo-item.mjs';
2930
* @property {string, ((T) => string | Promise<string>)} renderCell
3031
*/
3132

33+
/**
34+
* @typedef AdvancedTableConfig
35+
* @template T
36+
* @property {(T) => string | number} getKey
37+
* @property {string} [keyDataAttribute] the data attribute representing each rows key, defaults to 'data-key'
38+
* @property {AdditionalRowAttribute<T>[]} additionalRowAttributes
39+
* @property {string} tableClass
40+
* @property {string} rowClass
41+
* @property {boolean} draggable defaults to false
42+
*/
43+
44+
/**
45+
* @template T
46+
* @typedef AdditionalRowAttribute
47+
* @property {string} attributeName
48+
* @property {(T) => string} getAttributeValue
49+
*/
50+
3251
export class FUTableRenderer {
3352
/**
3453
* @type TableConfig
@@ -92,6 +111,38 @@ export class FUTableRenderer {
92111

93112
return new foundry.applications.ux.DragDrop.implementation(dragDropConfig);
94113
});
114+
config.tablePreset ??= 'item';
115+
if (config.tablePreset === 'item') {
116+
config.advancedConfig = {
117+
getKey: (item) => item.uuid,
118+
keyDataAttribute: 'data-uuid',
119+
additionalRowAttributes: [{ attributeName: 'data-item-id', getAttributeValue: (item) => item.id }],
120+
tableClass: 'item-list',
121+
rowClass: 'item',
122+
draggable: true,
123+
};
124+
} else if (config.tablePreset === 'effect') {
125+
config.advancedConfig = {
126+
getKey: (effect) => effect.uuid,
127+
keyDataAttribute: 'data-uuid',
128+
additionalRowAttributes: [{ attributeName: 'data-effect-id', getAttributeValue: (item) => item.id }],
129+
tableClass: '',
130+
rowClass: '',
131+
draggable: false,
132+
};
133+
} else {
134+
const advancedConfig = config.advancedConfig;
135+
advancedConfig.getKey = advancedConfig.getKey.bind(this);
136+
advancedConfig.keyDataAttribute ??= 'data-key';
137+
if (!advancedConfig.keyDataAttribute.startsWith('data-')) {
138+
advancedConfig.keyDataAttribute = `data-${advancedConfig.keyDataAttribute}`;
139+
}
140+
advancedConfig.additionalRowAttributes ??= [];
141+
advancedConfig.additionalRowAttributes.forEach((value) => (value.getAttributeValue = value.getAttributeValue.bind(this)));
142+
advancedConfig.tableClass ??= '';
143+
advancedConfig.rowClass ??= '';
144+
advancedConfig.draggable ??= false;
145+
}
95146

96147
this.initializeOptions(config);
97148

@@ -133,7 +184,7 @@ export class FUTableRenderer {
133184
const descriptions = {};
134185
const rowCssClasses = {};
135186
const rowTooltips = {};
136-
const { getItems, tablePreset, sort, columns: columnConfigs = {}, cssClass, renderDescription, renderRowCaption, hideIfEmpty: configHideIfEmpty } = this.tableConfig;
187+
const { getItems, tablePreset, sort, columns: columnConfigs = {}, cssClass, renderDescription, renderRowCaption, hideIfEmpty: configHideIfEmpty, advancedConfig } = this.tableConfig;
137188

138189
const items = getItems(document, options);
139190

@@ -163,32 +214,42 @@ export class FUTableRenderer {
163214
};
164215
}
165216

217+
const rows = [];
166218
for (let item of items) {
167-
const uuid = item.uuid;
219+
const rowKey = advancedConfig.getKey(item);
168220

169-
if (document !== item.parent && document !== item.parentDocument) {
170-
let directParentItem = item.parent;
171-
while (!(directParentItem instanceof FUItem || directParentItem instanceof PseudoItem)) {
172-
directParentItem = directParentItem.parent;
173-
}
174-
let parentItem = directParentItem;
175-
let parentage = [];
176-
while (!(parentItem instanceof Actor || parentItem == null)) {
177-
if (parentItem instanceof FUItem || parentItem instanceof PseudoItem) {
178-
parentage.unshift(parentItem);
221+
if (tablePreset !== 'custom') {
222+
if (document !== item.parent && document !== item.parentDocument) {
223+
let directParentItem = item.parent;
224+
while (!(directParentItem instanceof FUItem || directParentItem instanceof PseudoItem)) {
225+
directParentItem = directParentItem.parent;
226+
}
227+
let parentItem = directParentItem;
228+
let parentage = [];
229+
while (!(parentItem instanceof Actor || parentItem == null)) {
230+
if (parentItem instanceof FUItem || parentItem instanceof PseudoItem) {
231+
parentage.unshift(parentItem);
232+
}
233+
parentItem = parentItem.parent;
179234
}
180-
parentItem = parentItem.parent;
235+
parentage = parentage.map((item) => item.name).join(' → ');
236+
rowCssClasses[rowKey] = 'fu-table__row--deeply-nested';
237+
rowTooltips[rowKey] = game.i18n.format('FU.ItemDeeplyNested', { parent: parentage });
181238
}
182-
parentage = parentage.map((item) => item.name).join(' → ');
183-
rowCssClasses[uuid] = 'fu-table__row--deeply-nested';
184-
rowTooltips[uuid] = game.i18n.format('FU.ItemDeeplyNested', { parent: parentage });
185239
}
186240

187241
for (let [columnKey, columnConfig] of Object.entries(columnConfigs)) {
188-
columns[columnKey].cells[uuid] = columnConfig.renderCell instanceof Function ? columnConfig.renderCell(item) : columnConfig.renderCell;
242+
columns[columnKey].cells[rowKey] = columnConfig.renderCell instanceof Function ? columnConfig.renderCell(item) : columnConfig.renderCell;
189243
}
190-
rowCaptions[uuid] = rowCaptionRenderer(item);
191-
descriptions[uuid] = descriptionRenderer(item);
244+
rowCaptions[rowKey] = rowCaptionRenderer(item);
245+
descriptions[rowKey] = descriptionRenderer(item);
246+
247+
const additionalAttributes = {};
248+
for (const { attributeName, getAttributeValue } of advancedConfig.additionalRowAttributes) {
249+
additionalAttributes[attributeName] = getAttributeValue(item);
250+
}
251+
252+
rows.push({ key: rowKey, item, additionalAttributes });
192253
}
193254

194255
for (const column of Object.values(columns)) {
@@ -206,27 +267,10 @@ export class FUTableRenderer {
206267
descriptions[key] = await value;
207268
}
208269

209-
let presets;
210-
if (tablePreset === 'effect') {
211-
presets = {
212-
dataTypeId: 'data-effect-id',
213-
tableClass: '',
214-
rowClass: '',
215-
draggable: false,
216-
};
217-
} else {
218-
presets = {
219-
dataTypeId: 'data-item-id',
220-
tableClass: 'item-list',
221-
rowClass: 'item',
222-
draggable: true,
223-
};
224-
}
225-
226270
return foundry.applications.handlebars.renderTemplate('systems/projectfu/templates/table/fu-table.hbs', {
227271
tableId: this.#tableId,
228-
presets,
229-
items,
272+
config: advancedConfig,
273+
items: rows,
230274
cssClass: cssClass instanceof Function ? cssClass() : cssClass,
231275
columns,
232276
rowCssClasses,
@@ -265,14 +309,15 @@ export class FUTableRenderer {
265309
#onClick(event) {
266310
const table = event.target.closest(`[data-table-id="${this.#tableId}"]`);
267311
if (table) {
268-
const row = event.target.closest(`.fu-table__row-container[data-uuid]`);
312+
const keyDataAttribute = this.tableConfig.advancedConfig.keyDataAttribute;
313+
const row = event.target.closest(`.fu-table__row-container[${keyDataAttribute}]`);
269314
const actionElement = event.target.closest('[data-action]');
270315
const contextMenuTrigger = event.target.closest(`[data-context-menu]`);
271316
if (event.button === 0 && row && !actionElement && !contextMenuTrigger) {
272-
const uuid = row.dataset.uuid;
317+
const rowKey = row.dataset[this.#convertToDatasetKey(keyDataAttribute)];
273318
const expand = row.querySelector('.fu-table__row-expand');
274319
if (expand) {
275-
this.#expandedItems[uuid] = expand.classList.toggle('fu-table__row-expand--visible');
320+
this.#expandedItems[rowKey] = expand.classList.toggle('fu-table__row-expand--visible');
276321
}
277322
return;
278323
}
@@ -285,4 +330,12 @@ export class FUTableRenderer {
285330
}
286331
}
287332
}
333+
334+
#convertToDatasetKey(keyDataAttribute) {
335+
return keyDataAttribute
336+
.substring(5) //strip 'data-' prefix
337+
.split('-') // split at dashes
338+
.map((value, index) => (index > 0 ? value.capitalize() : value)) // capitalize parts beyond first
339+
.join(''); // join parts
340+
}
288341
}

0 commit comments

Comments
 (0)