dh-attribution-sources/scripts/dh-attribution-sources.mjs
2026-05-30 23:03:18 +02:00

233 lines
7.9 KiB
JavaScript

const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
let registeredKeys = new Set();
/**
* Apply the custom attribution sources to the system CONFIG object.
*/
export function applyAttributionSources() {
if (!CONFIG.DH?.GENERAL?.attributionSources) return;
// Remove previously registered custom sources
for (const key of registeredKeys) {
delete CONFIG.DH.GENERAL.attributionSources[key];
}
registeredKeys.clear();
// Get current custom sources from setting
const customSources = game.settings.get("dh-attribution-sources", "customSources") || [];
// Inject custom sources
for (const source of customSources) {
if (!source.id) continue;
const key = `custom_${source.id}`;
CONFIG.DH.GENERAL.attributionSources[key] = {
label: source.label || source.id,
values: (source.values || []).map(val => ({
label: val.label || "",
hint: val.hint || ""
}))
};
registeredKeys.add(key);
}
}
/**
* Custom application sheet to add, update, and remove custom attribution sources.
*/
export class CustomAttributionSourcesForm extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(options = {}) {
super(options);
const saved = game.settings.get("dh-attribution-sources", "customSources") || [];
this.sources = foundry.utils.deepClone(saved);
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'dh-attribution-sources-settings',
classes: ['dh-attribution-sources', 'dialog', 'settings-app'],
position: { width: 620, height: 650 },
window: {
title: "Custom Attribution Sources",
icon: 'fa-solid fa-signature',
resizable: true
},
actions: {
addSource: CustomAttributionSourcesForm.#onAddSource,
removeSource: CustomAttributionSourcesForm.#onRemoveSource,
addValue: CustomAttributionSourcesForm.#onAddValue,
removeValue: CustomAttributionSourcesForm.#onRemoveValue,
reset: CustomAttributionSourcesForm.#onReset,
save: CustomAttributionSourcesForm.#onSave
},
form: {
handler: CustomAttributionSourcesForm.#onSubmit,
submitOnChange: false,
closeOnSubmit: false
}
};
static PARTS = {
main: {
template: 'modules/dh-attribution-sources/templates/settings.hbs',
scrollable: ['.sources-list']
},
footer: {
template: 'modules/dh-attribution-sources/templates/footer.hbs'
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.sources = this.sources;
return context;
}
static #serializeForm(target) {
const form = target.closest('form');
const formData = new foundry.applications.ux.FormDataExtended(form);
const data = foundry.utils.expandObject(formData.object);
// Convert dot-notation structures back to nested arrays
const sources = Object.values(data.sources || {}).map(src => {
const values = Object.values(src.values || {}).map(val => ({
label: val.label || "",
hint: val.hint || ""
}));
return {
id: src.id || "",
label: src.label || "",
values: values
};
});
return sources;
}
static async #onAddSource(event, target) {
const sources = CustomAttributionSourcesForm.#serializeForm(target);
sources.push({
id: `group-${foundry.utils.randomID(4)}`,
label: "New Source Group",
values: [{ label: "", hint: "" }]
});
this.sources = sources;
this.render();
}
static async #onRemoveSource(event, target) {
const sources = CustomAttributionSourcesForm.#serializeForm(target);
const index = parseInt(target.dataset.sourceIndex);
if (!isNaN(index)) {
sources.splice(index, 1);
}
this.sources = sources;
this.render();
}
static async #onAddValue(event, target) {
const sources = CustomAttributionSourcesForm.#serializeForm(target);
const index = parseInt(target.dataset.sourceIndex);
if (!isNaN(index)) {
sources[index].values.push({ label: "", hint: "" });
}
this.sources = sources;
this.render();
}
static async #onRemoveValue(event, target) {
const sources = CustomAttributionSourcesForm.#serializeForm(target);
const sourceIndex = parseInt(target.dataset.sourceIndex);
const valueIndex = parseInt(target.dataset.valueIndex);
if (!isNaN(sourceIndex) && !isNaN(valueIndex)) {
sources[sourceIndex].values.splice(valueIndex, 1);
}
this.sources = sources;
this.render();
}
static async #onReset(event, target) {
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: {
title: "Reset Custom Attribution Sources"
},
content: "Are you sure you want to discard your unsaved changes and reload settings?"
});
if (!confirmed) return;
const saved = game.settings.get("dh-attribution-sources", "customSources") || [];
this.sources = foundry.utils.deepClone(saved);
this.render();
}
static async #onSave(event, target) {
const sources = CustomAttributionSourcesForm.#serializeForm(target);
// Validation
const ids = new Set();
for (const src of sources) {
const cleanId = src.id.trim();
if (!cleanId) {
ui.notifications.error("All attribution source groups must have a non-empty ID.");
return;
}
if (ids.has(cleanId)) {
ui.notifications.error(`Duplicate ID found: "${cleanId}". Each attribution source group must have a unique ID.`);
return;
}
ids.add(cleanId);
src.id = cleanId;
src.label = src.label.trim() || cleanId;
if (src.values.length === 0) {
ui.notifications.error(`Group "${src.label}" must contain at least one attribution value.`);
return;
}
for (const val of src.values) {
val.label = val.label.trim();
val.hint = val.hint.trim();
if (!val.label) {
ui.notifications.error(`Group "${src.label}" has an empty attribution label.`);
return;
}
}
}
await game.settings.set("dh-attribution-sources", "customSources", sources);
applyAttributionSources();
ui.notifications.info("Custom attribution sources saved successfully.");
this.close();
}
static async #onSubmit(event, form, formData) {
// Form submit falls through; handled by onSave button click
}
}
// Hook into initialization to register the settings and configuration panel
Hooks.once('init', () => {
game.settings.register("dh-attribution-sources", "customSources", {
name: "Custom Attribution Sources List",
scope: "world",
config: false,
type: Array,
default: []
});
game.settings.registerMenu("dh-attribution-sources", "manageSources", {
name: "Custom Attribution Sources",
label: "Manage Custom Sources",
hint: "Configure your custom item attribution sources.",
icon: "fas fa-signature",
type: CustomAttributionSourcesForm,
restricted: true
});
});
// Setup hook to run once the system has registered its configuration
Hooks.on('setup', () => {
applyAttributionSources();
});