From 58c3e5e129f59b37c0d87ccf847f9ef9c20eebb9 Mon Sep 17 00:00:00 2001 From: Matt Potts Date: Wed, 23 Nov 2022 11:32:03 +0000 Subject: [PATCH] Chore: Reusable app window components (#64) --- package.json | 2 +- src/module.json | 2 +- .../apps/CraftingSystemManagerApp.ts | 19 +- .../interface/apps/EditComponentDialog.ts | 238 ++++++++------ .../apps/EditCraftingSystemDetailDialog.ts | 187 ++++++----- .../interface/apps/EditEssenceDialog.ts | 182 ++++++----- .../apps/FabricateFormApplication.ts | 100 ------ .../interface/apps/core/Applications.ts | 296 ++++++++++++++++++ src/templates/edit-crafting-system-detail.hbs | 8 +- 9 files changed, 676 insertions(+), 358 deletions(-) delete mode 100644 src/scripts/interface/apps/FabricateFormApplication.ts create mode 100644 src/scripts/interface/apps/core/Applications.ts diff --git a/package.json b/package.json index cf9e67c3..38aeb827 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fabricate", - "version": "0.7.0", + "version": "0.7.1", "description": "A system-agnostic, flexible crafting module for FoundryVT", "main": "index.js", "scripts": { diff --git a/src/module.json b/src/module.json index 74cd5562..6cf56119 100644 --- a/src/module.json +++ b/src/module.json @@ -1,7 +1,7 @@ { "id": "fabricate", "title": "Fabricate", - "version": "0.7.0", + "version": "0.7.1", "description": "A system-agnostic, flexible crafting module for FoundryVTT", "authors": [ { diff --git a/src/scripts/interface/apps/CraftingSystemManagerApp.ts b/src/scripts/interface/apps/CraftingSystemManagerApp.ts index 0d60da1c..3af3658c 100644 --- a/src/scripts/interface/apps/CraftingSystemManagerApp.ts +++ b/src/scripts/interface/apps/CraftingSystemManagerApp.ts @@ -1,8 +1,5 @@ import Properties from "../../Properties"; import {GameProvider} from "../../foundry/GameProvider"; -import {EditCraftingSystemDetailDialog} from "./EditCraftingSystemDetailDialog"; -import {EditComponentDialog} from "./EditComponentDialog"; -import {EditEssenceDialog} from "./EditEssenceDialog"; import {CraftingSystem} from "../../system/CraftingSystem"; import {CraftingComponent} from "../../common/CraftingComponent"; import {Combination} from "../../common/Combination"; @@ -11,6 +8,9 @@ import {DefaultDocumentManager} from "../../foundry/DocumentManager"; import FabricateApplication from "../FabricateApplication"; import {Recipe} from "../../crafting/Recipe"; import {PartDictionary} from "../../system/PartDictionary"; +import EditComponentDialogFactory from "./EditComponentDialog"; +import EditEssenceDialogFactory from "./EditEssenceDialog"; +import EditCraftingSystemDetailDialogFactory from "./EditCraftingSystemDetailDialog"; class CraftingSystemManagerApp extends FormApplication { @@ -57,7 +57,7 @@ class CraftingSystemManagerApp extends FormApplication { async getData(): Promise { const craftingSystems = await this.systemRegistry.getAllCraftingSystems(); if (!this._selectedSystem && craftingSystems.size > 0) { - [this._selectedSystem] = craftingSystems.values(); + this._selectedSystem = Array.from(craftingSystems.values())[0]; } await this._selectedSystem?.loadPartDictionary(); return { @@ -139,13 +139,13 @@ class CraftingSystemManagerApp extends FormApplication { private async handleUserAction(systemId: string, action: string, event: any) { switch (action) { case "editDetails": - new EditCraftingSystemDetailDialog(this._selectedSystem).render(); + EditCraftingSystemDetailDialogFactory.make(this._selectedSystem).render(); break; case "importCraftingSystem": console.log(event); break; case "createCraftingSystem": - new EditCraftingSystemDetailDialog().render(); + EditCraftingSystemDetailDialogFactory.make().render(); break; case "toggleSystemEnabled": const checked = event.target.checked; @@ -179,8 +179,7 @@ class CraftingSystemManagerApp extends FormApplication { if (!componentToEdit) { throw new Error(`Cannot edit component. Component with ID "${componentIdToEdit}" not found. `); } - new EditComponentDialog(componentToEdit, this._selectedSystem) - .render(); + EditComponentDialogFactory.make(componentToEdit, this._selectedSystem).render(); break; case "createComponent": try { @@ -207,7 +206,7 @@ class CraftingSystemManagerApp extends FormApplication { } break; case "createEssence": - new EditEssenceDialog(this._selectedSystem).render(); + EditEssenceDialogFactory.make(this._selectedSystem).render(); break; case "editEssence": const essenceIdToEdit = event?.target?.dataset?.essenceId; @@ -215,7 +214,7 @@ class CraftingSystemManagerApp extends FormApplication { if (!essenceToEdit) { throw new Error(`Essence with ID "${essenceIdToEdit}" does not exist.`); } - new EditEssenceDialog(this._selectedSystem, essenceToEdit).render(); + EditEssenceDialogFactory.make(this._selectedSystem, essenceToEdit).render(); break; case "deleteEssence": const essenceIdToDelete = event?.target?.dataset?.essenceId; diff --git a/src/scripts/interface/apps/EditComponentDialog.ts b/src/scripts/interface/apps/EditComponentDialog.ts index d7d88a9d..ac0425cd 100644 --- a/src/scripts/interface/apps/EditComponentDialog.ts +++ b/src/scripts/interface/apps/EditComponentDialog.ts @@ -5,118 +5,134 @@ import {CombinableString, CraftingComponent} from "../../common/CraftingComponen import {Essence} from "../../common/Essence"; import {Combination, Unit} from "../../common/Combination"; import FabricateApplication from "../FabricateApplication"; +import {ApplicationWindow, Click, DefaultClickHandler, StateManager} from "./core/Applications"; -class EditComponentDialog extends FormApplication { +interface EditComponentDialogView { + system: { + id: string, + essences: Essence[], + components: CraftingComponent[] + }, + component: { + id: string, + name: string, + imageUrl: string, + essences: { essence: Essence; amount: number }[], + salvage: { component: CraftingComponent; amount: number }[] + } +} + +class EditComponentDialogModel { + private readonly _system: CraftingSystem; private readonly _component: CraftingComponent; - private readonly _craftingSystem: CraftingSystem; - constructor(component: CraftingComponent, selectedSystem: CraftingSystem) { - super(null); + constructor({ + system, + component + }: { + system: CraftingSystem, + component: CraftingComponent + }) { + this._system = system; this._component = component; - this._craftingSystem = selectedSystem; } - static get defaultOptions() { - const GAME = new GameProvider().globalGameObject(); - return { - ...super.defaultOptions, - title: GAME.i18n.localize(`${Properties.module.id}.EditComponentDialog.title`), - id: `${Properties.module.id}-component-manager`, - template: Properties.module.templates.ComponentManagerApp, - width: 500, - }; + get system(): CraftingSystem { + return this._system; + } + + get component(): CraftingComponent { + return this._component; + } + + public incrementEssence(essenceId: string): EditComponentDialogModel { + const essenceDelta = new Unit(new CombinableString(essenceId), 1); + this._component.essences = this._component.essences.add(essenceDelta); + return this; + } + + public decrementEssence(essenceId: string): EditComponentDialogModel { + const essenceDelta = new Unit(new CombinableString(essenceId), 1); + this._component.essences = this._component.essences.minus(essenceDelta); + return this; + } + + public incrementSalvage(salvageId: string): EditComponentDialogModel { + const salvageDelta = new Unit(new CombinableString(salvageId), 1); + this._component.salvage = this._component.salvage.add(salvageDelta); + return this; + } + + public decrementSalvage(salvageId: string): EditComponentDialogModel { + const salvageDelta = new Unit(new CombinableString(salvageId), 1); + this._component.salvage = this._component.salvage.minus(salvageDelta); + return this; + } + +} + +class ComponentStateManager implements StateManager { + + private readonly _model: EditComponentDialogModel; + + constructor({ + component, + system + }: { + component: CraftingComponent, + system: CraftingSystem + }) { + this._model = new EditComponentDialogModel({ + component, + system + }); + } + + getModelState(): EditComponentDialogModel { + return this._model; } - protected _updateObject(_event: Event, _formData: object | undefined): Promise { - console.log("Update object"); - this.render(); - return undefined; - } - - render(force: boolean = true) { - super.render(force); - } - - activateListeners(html: JQuery) { - super.activateListeners(html); - const rootElement = html[0]; - rootElement.addEventListener("click", this._onClick.bind(this)); - } - - async _onClick(event: any) { - const dataKeys = ["action", "componentId", "essenceId", "salvageId"]; - const data = new Map(dataKeys.map(key => [key, this.getClosestElementDataForKey(key, event)])); - const shiftPressed: boolean = event.shiftKey; - return this.handleUserAction(data, shiftPressed); - } - - getClosestElementDataForKey(key: string, event: any): string { - let element = event?.target; - let value = element?.dataset[key]; - while (element && !value) { - value = element?.dataset[key]; - element = element.parentElement; - } - return value; - }; - - async handleUserAction(data: Map, shiftPressed: boolean) { - switch (data.get("action")) { - case "editComponentEssence": - const essenceId = data.get("essenceId"); - const essenceDelta = new Unit(new CombinableString(essenceId), 1); - let essenceResult: Combination; - if (shiftPressed) { - essenceResult = this._component.essences.minus(essenceDelta); - } else { - essenceResult = this._component.essences.add(essenceDelta); - } - this._component.essences = essenceResult; - await FabricateApplication.systemRegistry.saveCraftingSystem(this._craftingSystem); - await this.render(true); - break; - case "editComponentSalvage": - const salvageId = data.get("salvageId"); - const salvageDelta = new Unit(new CombinableString(salvageId), 1); - let salvageResult: Combination; - if (shiftPressed) { - salvageResult = this._component.salvage.minus(salvageDelta); - } else { - salvageResult = this._component.salvage.add(salvageDelta); - } - this._component.salvage = salvageResult; - await FabricateApplication.systemRegistry.saveCraftingSystem(this._craftingSystem); - await this.render(true); - break; - default: - return; - } - } - - async getData(): Promise { + getViewData(): EditComponentDialogView { return { system: { - id: this._craftingSystem.id, - essences: this._craftingSystem.essences, - components: this._craftingSystem.components.filter(component => component.id !== this._component.id) + id: this.system.id, + essences: this.system.essences, + components: this.system.components.filter(component => component.id !== this.component.id) }, component: { - id: this._component.id, - name: this._component.name, - imageUrl: this._component.imageUrl, - essences: this.prepareEssenceData(this._craftingSystem.essences, this._component.essences), - salvage: this.prepareSalvageData(this._component.id, this._craftingSystem.components, this._component.salvage) + id: this.component.id, + name: this.component.name, + imageUrl: this.component.imageUrl, + essences: this.prepareEssenceData(this.system.essences, this.component.essences), + salvage: this.prepareSalvageData(this.component.id, this.system.components, this.component.salvage) } }; } + async load(): Promise { + return this.getModelState(); + } + + async save(model: EditComponentDialogModel): Promise { + await FabricateApplication.systemRegistry.saveCraftingSystem(model.system); + return this.getModelState(); + } + + get component(): CraftingComponent { + return this._model.component; + } + + get system(): CraftingSystem { + return this._model.system; + } + private prepareEssenceData(all: Essence[], includedAmounts: Combination): { essence: Essence; amount: number }[] { return all.map(essence => { return { essence, amount: includedAmounts.amountFor(new CombinableString(essence.id)) - }}); + }}); } private prepareSalvageData(thisComponentId: string, all: CraftingComponent[], includedAmounts: Combination): { component: CraftingComponent; amount: number }[] { @@ -125,8 +141,46 @@ class EditComponentDialog extends FormApplication { return { component, amount: includedAmounts.amountFor(new CombinableString(component.id)) - }}); + }}); + } + +} + +class EditComponentDialogFactory { + + make(component: CraftingComponent, system: CraftingSystem): ApplicationWindow { + return new ApplicationWindow({ + clickHandler: new DefaultClickHandler({ + dataKeys: ["componentId", "essenceId", "salvageId"], + actions: new Map([ + ["editComponentEssence", async (click: Click, currentState: EditComponentDialogModel) => { + const essenceId = click.data.get("essenceId"); + if (click.keys.shift) { + return currentState.decrementEssence(essenceId); + } else { + return currentState.incrementEssence(essenceId); + } + }], + ["editComponentSalvage", async (click: Click, currentState: EditComponentDialogModel) => { + const salvageId = click.data.get("salvageId"); + if (click.keys.shift) { + return currentState.decrementSalvage(salvageId); + } else { + return currentState.incrementSalvage(salvageId); + } + }] + ]) + }), + stateManager: new ComponentStateManager({component, system}), + options: { + title: new GameProvider().globalGameObject().i18n.localize(`${Properties.module.id}.EditComponentDialog.title`), + id: `${Properties.module.id}-component-manager`, + template: Properties.module.templates.ComponentManagerApp, + width: 500, + } + }); } + } -export { EditComponentDialog } \ No newline at end of file +export default new EditComponentDialogFactory(); \ No newline at end of file diff --git a/src/scripts/interface/apps/EditCraftingSystemDetailDialog.ts b/src/scripts/interface/apps/EditCraftingSystemDetailDialog.ts index e25d3087..c3ecb259 100644 --- a/src/scripts/interface/apps/EditCraftingSystemDetailDialog.ts +++ b/src/scripts/interface/apps/EditCraftingSystemDetailDialog.ts @@ -1,81 +1,87 @@ +import {FormApplicationWindow, FormError, StateManager, SubmissionHandler} from "./core/Applications"; +import {CraftingSystem, CraftingSystemJson} from "../../system/CraftingSystem"; import Properties from "../../Properties"; import {GameProvider} from "../../foundry/GameProvider"; -import {CraftingSystem, CraftingSystemJson} from "../../system/CraftingSystem"; import FabricateApplication from "../FabricateApplication"; -import {CraftingSystemDetails} from "../../system/CraftingSystemDetails"; +import {CraftingSystemDetails, CraftingSystemDetailsJson} from "../../system/CraftingSystemDetails"; +import {CraftingSystemFactory} from "../../system/CraftingSystemFactory"; -class EditCraftingSystemDetailDialog extends FormApplication { +class CraftingSystemModel { - private readonly _craftingSystem: CraftingSystem; + private readonly _factory: CraftingSystemFactory; + private _system: CraftingSystem; - constructor(craftingSystem?: CraftingSystem) { - super(null); - this._craftingSystem = craftingSystem; + constructor({ + system, + factory + }: { + system: CraftingSystem; + factory: CraftingSystemFactory; + }) { + this._system = system; + this._factory = factory; } - static get defaultOptions() { - const GAME = new GameProvider().globalGameObject(); - return { - ...super.defaultOptions, - title: GAME.i18n.localize(`${Properties.module.id}.EditCraftingSystemDetailDialog.title`), - id: `${Properties.module.id}-create-crafting-system-dialog`, - template: Properties.module.templates.EditCraftingSystemDetailDialog, - width: 400, - }; + get system(): CraftingSystem { + return this._system; } - protected async _updateObject(_event: Event, _formData: object | undefined): Promise { - console.log("Update object"); - this.render(); - return undefined; + set system(value: CraftingSystem) { + this._system = value; } - render(force: boolean = true) { - super.render(force); + public editDetails(value: CraftingSystemDetails): CraftingSystemModel { + this._system.setDetails(value); + return this; } - async getData(): Promise { - const GAME = new GameProvider().globalGameObject(); - return { - name: this._craftingSystem?.name ?? "", - summary: this._craftingSystem?.summary ?? "", - description: this._craftingSystem?.description ?? "", - author: this._craftingSystem?.author ?? GAME.user.name - }; + public async createWithDetails(details: CraftingSystemDetailsJson): Promise { + this._system = await this._factory.make({ + id: randomID(), + details, + locked: false, + enabled: true, + parts: { + essences: {}, + recipes: {}, + components: {} + } + }); + return this; } - async _onSubmit(event: Event, options: FormApplication.OnSubmitOptions): Promise { - event.preventDefault(); - const formData = foundry.utils.expandObject(this._getSubmitData(options?.updateData)); - const errors = this.validate(formData); - if (errors.length > 0) { - ui.notifications.error(this.formatErrorMessage(errors)); - return; - } - if (!this._craftingSystem) { - await this.createCraftingSystem(formData); - } else { - await this.editCraftingSystemDetails(formData); - } - await this.close(); - return formData; +} + +class CraftingSystemStateManager implements StateManager { + + private readonly _model: CraftingSystemModel; + + constructor({ + system, + factory + }: { + system: CraftingSystem; + factory: CraftingSystemFactory + }) { + this._model = new CraftingSystemModel({system, factory}); } - private async editCraftingSystemDetails(editedDetails: { name: string, summary: string, description: string, author: string }) { - this._craftingSystem.setDetails(new CraftingSystemDetails(editedDetails)); - await FabricateApplication.systemRegistry.saveCraftingSystem(this._craftingSystem); + getModelState(): CraftingSystemModel { + return this._model; } - private async createCraftingSystem({ name, summary, description, author}: {name: string, summary: string, description: string, author: string}) { - const gameProvider = new GameProvider(); - // todo: add more detail;s for checks and alchemy as those are defined in the UI with macros - const systemDefinition: CraftingSystemJson = { + getViewData(): CraftingSystemJson { + if (this._model.system) { + return this._model.system.toJson(); + } + const GAME = new GameProvider().globalGameObject(); + return { id: randomID(), details: { - name, - summary, - description, - author: author ?? gameProvider.globalGameObject().user.name, + name: "", + summary: "", + description: "", + author: GAME.user.name }, locked: false, enabled: true, @@ -84,33 +90,70 @@ class EditCraftingSystemDetailDialog extends FormApplication { components: {}, recipes: {} } - }; - await FabricateApplication.systemRegistry.createCraftingSystem(systemDefinition); + } } - validate(formData: any): Array { - const GAME = new GameProvider().globalGameObject(); - const errors: Array = []; + load(): Promise { + return Promise.resolve(undefined); + } + + async save(model: CraftingSystemModel): Promise { + this._model.system = await FabricateApplication.systemRegistry.saveCraftingSystem(model.system); + return this._model; + } + +} + +class CraftingSystemDetailSubmissionHandler implements SubmissionHandler { + + async process(formData: CraftingSystemDetailsJson, currentState: CraftingSystemModel): Promise { + if (currentState.system) { + return currentState.editDetails(new CraftingSystemDetails({ + name: formData.name, + description: formData.description, + author: formData.author, + summary: formData.summary + })); + } + return currentState.createWithDetails(formData); + } + + validate(formData: CraftingSystemDetailsJson): FormError[] { + const GAME = new GameProvider().globalGameObject(); + const errors: Array = []; if (!formData.name || formData.name.length === 0) { - errors.push(GAME.i18n.localize(`${Properties.module.id}.EditCraftingSystemDetailDialog.errors.nameRequired`)) + errors.push({ + detail: GAME.i18n.localize(`${Properties.module.id}.EditCraftingSystemDetailDialog.errors.nameRequired`), + field: "Name" + }) } if (!formData.summary || formData.summary.length === 0) { - errors.push(GAME.i18n.localize(`${Properties.module.id}.EditCraftingSystemDetailDialog.errors.summaryRequired`)) + errors.push({ + detail: GAME.i18n.localize(`${Properties.module.id}.EditCraftingSystemDetailDialog.errors.summaryRequired`), + field: "Summary" + }) } return errors; } - formatErrorMessage(errors: Array): string { - if (errors.length === 1) { - return `${Properties.module.label} | ${errors[0]}`; - } - const errorDetail = errors.map((error, index) => `${index + 1}) ${error}`) - .join(", "); +} + +class EditCraftingSystemDetailDialogFactory { + + make(system?: CraftingSystem): FormApplicationWindow { const GAME = new GameProvider().globalGameObject(); - const localizationPath = `${Properties.module.id}.ui.notifications.submissionError.prefix` - return `${Properties.module.label} | ${GAME.i18n.localize(localizationPath)}: ${errorDetail}` + return new FormApplicationWindow({ + options: { + title: GAME.i18n.localize(`${Properties.module.id}.EditCraftingSystemDetailDialog.title`), + id: `${Properties.module.id}-create-crafting-system-dialog`, + template: Properties.module.templates.EditCraftingSystemDetailDialog, + width: 400, + }, + stateManager: new CraftingSystemStateManager({system, factory: new CraftingSystemFactory({})}), + submissionHandler: new CraftingSystemDetailSubmissionHandler() + }); } } -export { EditCraftingSystemDetailDialog } \ No newline at end of file +export default new EditCraftingSystemDetailDialogFactory(); \ No newline at end of file diff --git a/src/scripts/interface/apps/EditEssenceDialog.ts b/src/scripts/interface/apps/EditEssenceDialog.ts index a414969e..d52969a2 100644 --- a/src/scripts/interface/apps/EditEssenceDialog.ts +++ b/src/scripts/interface/apps/EditEssenceDialog.ts @@ -1,110 +1,136 @@ -import {GameProvider} from "../../foundry/GameProvider"; -import Properties from "../../Properties"; -import {Essence} from "../../common/Essence"; +import {FormApplicationWindow, FormError, StateManager, SubmissionHandler} from "./core/Applications"; import {CraftingSystem} from "../../system/CraftingSystem"; +import {Essence} from "../../common/Essence"; +import Properties from "../../Properties"; +import {GameProvider} from "../../foundry/GameProvider"; import FabricateApplication from "../FabricateApplication"; -class EditEssenceDialog extends FormApplication { +interface EssenceView { - private readonly _craftingSystem: CraftingSystem; - private readonly _essence?: Essence; + name: string; + description: string; + iconCode: string; + tooltip: string; - constructor(craftingSystem: CraftingSystem, essence?: Essence) { - super(null); - this._craftingSystem = craftingSystem; +} + +class EssenceModel { + + private readonly _system: CraftingSystem; + private _essence?: Essence; + + constructor({ + essence, + system + }: { + system: CraftingSystem, + essence: Essence + }) { + this._system = system; this._essence = essence; } - static get defaultOptions() { - const GAME = new GameProvider().globalGameObject(); - return { - ...super.defaultOptions, - title: GAME.i18n.localize(`${Properties.module.id}.EditEssenceDialog.title`), - id: `${Properties.module.id}-essence-manager`, - template: Properties.module.templates.EssenceManagerApp, - width: 380, - }; + get system(): CraftingSystem { + return this._system; } - protected async _updateObject(_event: Event, _formData: object | undefined): Promise { - console.log("Update object"); - await this.render(); - return undefined; + get essence(): Essence { + return this._essence; } - async render(force: boolean = true) { - await this._craftingSystem.loadPartDictionary(); - super.render(force); + public editEssence(value: Essence): EssenceModel { + this._essence = value; + this._system.partDictionary.editEssence(value); + return this; } - async _onSubmit(event: Event, options: FormApplication.OnSubmitOptions): Promise { - event.preventDefault(); - const formData = foundry.utils.expandObject(this._getSubmitData(options?.updateData)); +} - const errors = this.validate(formData); - if (errors.length > 0) { - ui.notifications.error(this.formatErrorMessage(errors)); - return; - } - await this.createOrUpdateEssence(formData); - await this.close(); - return formData; - } +class EssenceStateManager implements StateManager { - async createOrUpdateEssence({ - name, - description, - iconCode, - tooltip + private _model: EssenceModel; + + constructor({ + essence, + system }: { - name: string, - description?: string, - iconCode: string, - tooltip?: string - }): Promise { - if (!this._craftingSystem) { - throw new Error(`The crafting system with ID "${this._craftingSystem?.id}" does not exist.`) - } - const essence = new Essence({ + essence?: Essence, + system: CraftingSystem}) { + this._model = new EssenceModel({essence, system}); + } + + getModelState(): EssenceModel { + return this._model; + } + + getViewData(): EssenceView { + const name: string = this.essence?.name ?? ""; + return { name, - description, - tooltip, - id: this._essence?.id ?? randomID(), - iconCode: iconCode ? iconCode : Properties.ui.defaults.essenceIconCode + description: this.essence?.description ?? "", + iconCode: this.essence?.iconCode ? this.essence.iconCode : Properties.ui.defaults.essenceIconCode, + tooltip: this.essence?.tooltip ? this.essence.tooltip : name + }; + } + + get essence(): Essence { + return this._model.essence; + } + + load(): Promise { + return Promise.resolve(undefined); + } + + async save(): Promise { + const system = await FabricateApplication.systemRegistry.saveCraftingSystem(this._model.system); + this._model = new EssenceModel({system, essence: system.getEssenceById(this._model.essence.id)}); + return this._model; + } + +} + +class EssenceSubmissionHandler implements SubmissionHandler { + + async process(formData: EssenceView, currentState: EssenceModel): Promise { + const modifiedEssence = new Essence({ + name: formData.name, + description: formData.description, + tooltip: formData.tooltip ? formData.tooltip : formData.name, + id: currentState.essence?.id ?? randomID(), + iconCode: formData.iconCode ? formData.iconCode : Properties.ui.defaults.essenceIconCode }); - this._craftingSystem.partDictionary.editEssence(essence); - return FabricateApplication.systemRegistry.saveCraftingSystem(this._craftingSystem); + return currentState.editEssence(modifiedEssence); } - validate(formData: any): Array { + validate(formData: EssenceView): FormError[] { const GAME = new GameProvider().globalGameObject(); - const errors: Array = []; + const errors: Array = []; if (!formData.name || formData.name.length === 0) { - errors.push(GAME.i18n.localize(`${Properties.module.id}.EditEssenceDialog.errors.nameRequired`)) + errors.push({ + field: "name", + detail: GAME.i18n.localize(`${Properties.module.id}.EditEssenceDialog.errors.nameRequired`) + }) } return errors; } - formatErrorMessage(errors: Array): string { - if (errors.length === 1) { - return `${Properties.module.label} | ${errors[0]}`; - } - const errorDetail = errors.map((error, index) => `${index + 1}) ${error}`) - .join(", "); - const GAME = new GameProvider().globalGameObject(); - const localizationPath = `${Properties.module.id}.ui.notifications.submissionError.prefix` - return `${Properties.module.label} | ${GAME.i18n.localize(localizationPath)}: ${errorDetail}` - } +} - async getData(): Promise { - return { - name: this._essence?.name ?? "", - description: this._essence?.description ?? "", - iconCode: this._essence?.iconCode ?? "", - tooltip: this._essence?.tooltip ?? "" - }; +class EditEssenceDialogFactory { + + make(system: CraftingSystem, essence?: Essence): FormApplicationWindow { + return new FormApplicationWindow({ + options: { + title: new GameProvider().globalGameObject().i18n.localize(`${Properties.module.id}.EditEssenceDialog.title`), + id: `${Properties.module.id}-essence-manager`, + template: Properties.module.templates.EssenceManagerApp, + width: 380, + }, + stateManager: new EssenceStateManager({essence, system}), + submissionHandler: new EssenceSubmissionHandler() + }); } } -export { EditEssenceDialog } \ No newline at end of file +export default new EditEssenceDialogFactory(); \ No newline at end of file diff --git a/src/scripts/interface/apps/FabricateFormApplication.ts b/src/scripts/interface/apps/FabricateFormApplication.ts deleted file mode 100644 index 8f51a8b0..00000000 --- a/src/scripts/interface/apps/FabricateFormApplication.ts +++ /dev/null @@ -1,100 +0,0 @@ -interface FormAction { - - do(model: M): Promise; - -} - -interface FormInteractionHandler { - - click(event: any): Promise; - - register(actionType: string, action: FormAction): void; - -} - -interface FormStateManager { - - load(filteredClickData: Map): Promise; - - save(viewData: V): Promise; - -} - -class DefaultFormInteractionHandler implements FormInteractionHandler { - - private readonly _actionDataKey: string = "action"; - - private readonly _dataKeys: string[]; - private readonly _formActionsByType: Map>; - private readonly _formStateManager: FormStateManager; - - constructor({ - dataKeys, - formActionsByType, - formStateManager - }: { - dataKeys: string[]; - formActionsByType: Map>; - formStateManager: FormStateManager; - }) { - this._dataKeys = dataKeys; - this._formActionsByType = formActionsByType; - this._formStateManager = formStateManager; - } - - async click(event: any): Promise { - const keys = [...this._dataKeys, this._actionDataKey]; - const data = new Map(keys.map(key => [key, this.getClosestElementDataForKey(key, event)])); - if (!data.has(this._actionDataKey)) { - throw new Error("An unknown action was triggered on a Fabricate Form Application. "); - } - const action = data.get(this._actionDataKey); - if (!this._formActionsByType.has(action)) { - throw new Error(`The action "${action}" was triggered, but no form action was registered to handle it! `); - } - const filteredClickData = new Map(Array.from(data.entries()).filter(entry => entry[0] && entry[1])); - await this._formActionsByType.get(action).do(await this._formStateManager.load(filteredClickData)); - return; - } - - private getClosestElementDataForKey(key: string, event: any): string { - let element = event?.target; - let value = element?.dataset[key]; - while (element && !value) { - value = element?.dataset[key]; - element = element.parentElement; - } - return value; - }; - - register(actionType: string, action: FormAction): void { - this._formActionsByType.set(actionType, action); - } - -} - -// todo: finish tinkering as this will be really useful -class FabricateFormApplication extends FormApplication { - - private readonly _formInteractionHandler: FormInteractionHandler; - - constructor(args: any, formInteractionHandler: FormInteractionHandler) { - super(args); - this._formInteractionHandler = formInteractionHandler; - } - - protected _updateObject(_event: Event, _formData: object | undefined): Promise { - return Promise.resolve(undefined); - } - - override activateListeners(html: JQuery): void { - super.activateListeners(html); - const rootElement = html[0]; - rootElement.addEventListener("click", this._formInteractionHandler.click.bind(this)); - } - - override getData(options?: Partial): Promise> | FormApplication.Data<{}, FormApplicationOptions> { - return super.getData(options); - } - -} \ No newline at end of file diff --git a/src/scripts/interface/apps/core/Applications.ts b/src/scripts/interface/apps/core/Applications.ts new file mode 100644 index 00000000..41a4a1c8 --- /dev/null +++ b/src/scripts/interface/apps/core/Applications.ts @@ -0,0 +1,296 @@ +import Properties from "../../../Properties"; +import {GameProvider} from "../../../foundry/GameProvider"; + +interface Click { + action: string; + keys: { + shift: boolean; + alt: boolean; + ctrl: boolean; + }; + data: Map; + event: any; +} + +interface ClickHandler { + + readonly dataKeys: string[]; + + handle(clickEvent: any, stateManager: StateManager): Promise; + +} + +class DefaultClickHandler> implements ClickHandler { + + private readonly _dataKeys: string[]; + private readonly _actions: Map>; + + constructor({ + dataKeys = [], + actions = new Map() + }: { + dataKeys?: string[]; + actions?: Map>; + }) { + this._dataKeys = [...dataKeys, "action"]; + this._actions = actions; + } + + get actions(): Map> { + return this._actions; + } + + get dataKeys(): string[] { + return this._dataKeys; + } + + getClosestElementDataForKey(key: string, event: any): string { + let element = event?.target; + let value = element?.dataset[key]; + while (element && !value) { + value = element?.dataset[key]; + element = element.parentElement; + } + return value; + }; + + getClickData(event: any): Click { + const data = new Map(this._dataKeys.map(key => [key, this.getClosestElementDataForKey(key, event)])); + if (event.shiftKey) { + data.set("shiftPressed", String(event.shiftKey)); + } + return { + action: data.get("action"), + data, + event, + keys: { + shift: event.shiftKey, + alt: event.altKey, + ctrl: event.ctrlKey, + } + }; + } + + async handle(clickEvent: any, stateManager: S): Promise { + const click = this.getClickData(clickEvent); + if (this._actions.has(click.action)) { + const model = await this._actions.get(click.action)(click, stateManager.getModelState()); + await stateManager.save(model); + return stateManager.getModelState(); + } + if (click.action) { + throw new Error(`Could not determine action for click event ${click.action}`); + } + } + +} + +interface StateManager { + + getModelState(): M; + + getViewData(): V; + + save(model: M): Promise; + + load(): Promise; + +} + +class NoStateManager implements StateManager { + + private static readonly _INSTANCE = new NoStateManager(); + + private constructor() {} + + public static getInstance(): NoStateManager { + return NoStateManager._INSTANCE; + } + + getModelState(): any { + return {}; + } + + getViewData(): any { + return {}; + } + + async load(): Promise { + return {}; + } + + async save(): Promise { + return {}; + } + +} + +interface FormError { + + field: string; + detail: string; + +} + +interface SubmissionHandler { + + validate(formData: F): FormError[]; + + process(formData: F, currentState: M): Promise; + +} + +type ApplicationAction = (click: Click, currentState: M) => Promise; + +class ApplicationWindow extends Application { + + private readonly _clickHandler: ClickHandler; + private readonly _stateManager: StateManager; + + constructor({ + clickHandler = new DefaultClickHandler({}), + options = {}, + stateManager = NoStateManager.getInstance() + }: { + clickHandler?: ClickHandler; + options?: Partial; + stateManager?: StateManager; + }) { + super(options); + this._clickHandler = clickHandler; + this._stateManager = stateManager; + } + + render(force: boolean = true): void { + super.render(force); + } + + async getData(): Promise { + return this._stateManager.getViewData(); + } + + activateListeners(html: JQuery): void { + super.activateListeners(html); + const rootElement = html[0]; + rootElement.addEventListener("click", this.onClick.bind(this)); + } + + private async onClick(event: any): Promise { + await this._clickHandler.handle(event, this._stateManager); + await this.render(true); + return; + } + +} + +class NoSubmissionHandler implements SubmissionHandler { + + private static readonly _INSTANCE = new NoSubmissionHandler(); + + private constructor() {} + + public static getInstance(): NoSubmissionHandler { + return NoSubmissionHandler._INSTANCE; + } + + formatErrors(): string { + return ""; + } + + async process(): Promise { + return {} + } + + validate(): FormError[] { + return []; + } + +} + +class FormApplicationWindow extends FormApplication { + + private readonly _clickHandler: ClickHandler; + private readonly _stateManager: StateManager; + private readonly _submissionHandler: SubmissionHandler; + + constructor({ + clickHandler = new DefaultClickHandler({}), + options = {}, + stateManager = NoStateManager.getInstance(), + submissionHandler = NoSubmissionHandler.getInstance() + }: { + clickHandler?: ClickHandler; + options?: Partial; + stateManager?: StateManager; + submissionHandler?: SubmissionHandler; + }) { + super({}, options); // todo: figure out what the value of concrete object *should* be when we're not dealing with a document (if not "{}") + this._clickHandler = clickHandler; + this._stateManager = stateManager; + this._submissionHandler = submissionHandler; + } + + protected _updateObject(_event: Event, _formData: object | undefined): Promise { + console.log("Update object"); + this.render(); + return undefined; + } + + render(force: boolean = true) { + super.render(force); + } + + activateListeners(html: JQuery) { + super.activateListeners(html); + const rootElement = html[0]; + rootElement.addEventListener("click", this.onClick.bind(this)); + } + + private async onClick(event: any): Promise { + return this._clickHandler.handle(event, this._stateManager); + } + + override async getData(): Promise { + return this._stateManager.getViewData(); + } + + protected async _onSubmit(event: Event, options: FormApplication.OnSubmitOptions): Promise { + event.preventDefault(); + const formData = foundry.utils.expandObject(this._getSubmitData(options?.updateData)); + const errors = this._submissionHandler.validate(formData); + if (errors.length > 0) { + ui.notifications.error(this.formatErrors(errors)); + return; + } + const model = await this._submissionHandler.process(formData, this._stateManager.getModelState()); + await this._stateManager.save(model); + await this.close(); + return formData; + } + + private formatErrors(errors: FormError[]): string { + if (errors.length === 1) { + return `${Properties.module.label} | ${errors[0].detail}`; + } + const errorDetail = errors + .map((error, index) => `${index + 1}) ${error.detail}`) + .join(", "); + const GAME = new GameProvider().globalGameObject(); + const localizationPath = `${Properties.module.id}.ui.notifications.submissionError.prefix` + return `${Properties.module.label} | ${GAME.i18n.localize(localizationPath)}: ${errorDetail}` + } + +} + +export { + ApplicationWindow, + FormApplicationWindow, + StateManager, + NoStateManager, + ApplicationAction, + SubmissionHandler, + FormError, + Click, + ClickHandler, + DefaultClickHandler +} \ No newline at end of file diff --git a/src/templates/edit-crafting-system-detail.hbs b/src/templates/edit-crafting-system-detail.hbs index 67d44ff6..2fac6394 100644 --- a/src/templates/edit-crafting-system-detail.hbs +++ b/src/templates/edit-crafting-system-detail.hbs @@ -3,28 +3,28 @@
- +
- +
- +
- +