Merge pull request #48 from Eldritch-Oliver/feature/tabbed-doc-sheet-config

Custom Document Sheet Config
This commit is contained in:
Oliver 2025-10-05 11:59:21 -06:00 committed by GitHub
commit f7fee99b44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 303 additions and 139 deletions

View file

@ -13,6 +13,24 @@
}, },
"sheet-names": { "sheet-names": {
"PlayerSheet": "Player Sheet" "PlayerSheet": "Player Sheet"
},
"Apps": {
"TAFDocumentSheetConfig": {
"Sizing": "Sizing",
"Width": {
"label": "Width"
},
"Height": {
"label": "Height"
},
"Resizable": {
"label": "Resizable"
},
"tabs": {
"foundry": "Foundry",
"system": "Text-Based Actors"
}
}
} }
} }
} }

View file

@ -1,7 +1,7 @@
import { __ID__, filePath } from "../consts.mjs"; import { __ID__, filePath } from "../consts.mjs";
import { AttributeManager } from "./AttributeManager.mjs"; import { AttributeManager } from "./AttributeManager.mjs";
import { attributeSorter } from "../utils/attributeSort.mjs"; import { attributeSorter } from "../utils/attributeSort.mjs";
import { ResizeControlManager } from "./ResizeControlManager.mjs"; import { TAFDocumentSheetConfig } from "./TAFDocumentSheetConfig.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api; const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ActorSheetV2 } = foundry.applications.sheets; const { ActorSheetV2 } = foundry.applications.sheets;
@ -28,7 +28,7 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}, },
actions: { actions: {
manageAttributes: this.#manageAttributes, manageAttributes: this.#manageAttributes,
sizeSettings: this.#configureSizeSettings, configureSheet: this.#configureSheet,
}, },
}; };
@ -78,15 +78,6 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return isGM || (allowPlayerEdits && editable); return isGM || (allowPlayerEdits && editable);
}, },
}); });
controls.push({
icon: `fa-solid fa-crop-simple`,
label: `Configure Size`,
action: `sizeSettings`,
visible: () => {
const isGM = game.user.isGM;
return isGM;
},
});
return controls; return controls;
}; };
@ -156,15 +147,18 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}; };
}; };
#sizeSettings = null; static async #configureSheet(event) {
/** @this {PlayerSheet} */ event.stopPropagation();
static async #configureSizeSettings() { if ( event.detail > 1 ) { return }
this.#sizeSettings ??= new ResizeControlManager({ document: this.actor });
if (this.#sizeSettings.rendered) { // const docSheetConfigWidth = TAFDocumentSheetConfig.DEFAULT_OPTIONS.position.width;
await this.#sizeSettings.bringToFront(); new TAFDocumentSheetConfig({
} else { document: this.document,
await this.#sizeSettings.render({ force: true }); position: {
}; top: this.position.top + 40,
left: this.position.left + ((this.position.width - 60) / 2),
},
}).render({ force: true });
}; };
// #endregion Actions // #endregion Actions
}; };

View file

@ -1,61 +0,0 @@
import { __ID__, filePath } from "../consts.mjs";
const { HandlebarsApplicationMixin, DocumentSheetV2 } = foundry.applications.api;
const { getProperty } = foundry.utils;
export class ResizeControlManager extends HandlebarsApplicationMixin(DocumentSheetV2) {
// #region Options
static DEFAULT_OPTIONS = {
classes: [
__ID__,
`ResizeControlManager`,
],
position: {
width: 400,
height: `auto`,
},
window: {
resizable: true,
},
form: {
submitOnChange: false,
closeOnSubmit: true,
},
actions: {},
};
static PARTS = {
settings: { template: filePath(`templates/ResizeControlManager/settings.hbs`) },
controls: { template: filePath(`templates/ResizeControlManager/controls.hbs`) },
};
// #endregion Options
// #region Instance Data
get title() {
return `Sizing Settings For : ${this.document.name}`;
};
// #endregion Instance Data
// #region Data Prep
async _prepareContext() {
const sizing = getProperty(this.document, `flags.${__ID__}.PlayerSheet.size`) ?? {};
const ctx = {
meta: {
idp: this.id,
},
width: sizing.width,
height: sizing.height,
resizable: sizing.resizable,
resizeOptions: [
{ label: `Default`, value: `` },
{ label: `Resizable`, value: `true` },
{ label: `No Resizing`, value: `false` },
],
};
return ctx;
};
// #endregion Data Prep
};

View file

@ -0,0 +1,171 @@
import { __ID__, filePath } from "../consts.mjs";
import { getDefaultSizing } from "../utils/getSizing.mjs";
const { diffObject, expandObject, flattenObject } = foundry.utils;
const { DocumentSheetConfig } = foundry.applications.apps;
const { CONST } = foundry;
export class TAFDocumentSheetConfig extends DocumentSheetConfig {
// #region Options
static DEFAULT_OPTIONS = {
classes: [`taf`],
form: {
handler: this.#onSubmit,
},
};
static get PARTS() {
const { form, footer } = super.PARTS;
return {
tabs: { template: `templates/generic/tab-navigation.hbs` },
foundryTab: {
...form,
template: filePath(`templates/TAFDocumentSheetConfig/foundry.hbs`),
templates: [ `templates/sheets/document-sheet-config.hbs` ],
},
systemTab: {
template: filePath(`templates/TAFDocumentSheetConfig/system.hbs`),
classes: [`standard-form`],
},
footer,
};
};
static TABS = {
main: {
initial: `system`,
labelPrefix: `taf.Apps.TAFDocumentSheetConfig.tabs`,
tabs: [
{ id: `system` },
{ id: `foundry` },
],
},
};
// #endregion Options
// #region Data Prep
async _preparePartContext(partID, context, options) {
this._prepareTabs(`main`);
context.meta = {
idp: this.id,
};
switch (partID) {
case `foundryTab`: {
await this._prepareFormContext(context, options);
break;
};
case `systemTab`: {
await this._prepareSystemSettingsContext(context, options);
break;
};
case `footer`: {
await this._prepareFooterContext(context, options);
break;
};
};
return context;
};
async _prepareSystemSettingsContext(context, _options) {
// Inherited values for placeholders
const defaults = getDefaultSizing();
context.placeholders = {
...defaults,
resizable: defaults.resizable ? `Resizable` : `Not Resizable`,
};
// Custom values from document itself
const sheetConfig = this.document.getFlag(__ID__, `PlayerSheet`) ?? {};
const sizing = sheetConfig.size ?? {};
context.values = {
width: sizing.width,
height: sizing.height,
resizable: sizing.resizable ?? ``,
};
// Static prep
context.resizeOptions = [
{ label: `Default (${context.placeholders.resizable})`, value: `` },
{ label: `Resizable`, value: `true` },
{ label: `No Resizing`, value: `false` },
];
};
// #endregion Data Prep
// #region Actions
/** @this {TAFDocumentSheetConfig} */
static async #onSubmit(event, form, formData) {
const foundryReopen = await TAFDocumentSheetConfig.#submitFoundry.call(this, event, form, formData);
const systemReopen = await TAFDocumentSheetConfig.#submitSystem.call(this, event, form, formData);
if (foundryReopen || systemReopen) {
this.document._onSheetChange({ sheetOpen: true });
};
};
/**
* This method is mostly the form submission handler that foundry uses in
* DocumentSheetConfig, however because we clobber that in order to save our
* own config stuff as well, we need to duplicate Foundry's handling and tweak
* it a bit to make it work nicely with our custom saving.
*
* @this {TAFDocumentSheetConfig}
*/
static async #submitFoundry(_event, _form, formData) {
const { object } = formData;
const { documentName, type = CONST.BASE_DOCUMENT_TYPE } = this.document;
// Update themes.
const themes = game.settings.get(`core`, `sheetThemes`);
const defaultTheme = foundry.utils.getProperty(themes, `defaults.${documentName}.${type}`);
const documentTheme = themes.documents?.[this.document.uuid];
const themeChanged = (object.defaultTheme !== defaultTheme) || (object.theme !== documentTheme);
if (themeChanged) {
foundry.utils.setProperty(themes, `defaults.${documentName}.${type}`, object.defaultTheme);
themes.documents ??= {};
themes.documents[this.document.uuid] = object.theme;
await game.settings.set(`core`, `sheetThemes`, themes);
}
// Update sheets.
const { defaultClass } = this.constructor.getSheetClassesForSubType(documentName, type);
const sheetClass = this.document.getFlag(`core`, `sheetClass`) ?? ``;
const defaultSheetChanged = object.defaultClass !== defaultClass;
const documentSheetChanged = object.sheetClass !== sheetClass;
if (themeChanged || (game.user.isGM && defaultSheetChanged)) {
if (game.user.isGM && defaultSheetChanged) {
const setting = game.settings.get(`core`, `sheetClasses`);
foundry.utils.setProperty(setting, `${documentName}.${type}`, object.defaultClass);
await game.settings.set(`core`, `sheetClasses`, setting);
}
// This causes us to manually rerender the sheet due to the theme or default
// sheet class changing resulting in no update making it to the client-document's
// _onUpdate handling
if (!documentSheetChanged) {
return true;
}
}
// Update the document-specific override.
if (documentSheetChanged) {
this.document.setFlag(`core`, `sheetClass`, object.sheetClass);
};
return false;
};
/** @this {TAFDocumentSheetConfig} */
static async #submitSystem(_event, _form, formData) {
const { FLAGS: flags } = expandObject(formData.object);
const diff = flattenObject(diffObject(this.document.flags, flags));
const hasChanges = Object.keys(diff).length > 0;
if (hasChanges) {
await this.document.update({ flags });
};
return hasChanges;
};
// #endregion Actions
};

View file

@ -0,0 +1,32 @@
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
/**
* @typedef SheetSizing
* @property {number} width The initial width of the application
* @property {number} height The initial height of the application
* @property {boolean} resizable Whether or not the application
* is able to be resized with a drag handle.
*/
/**
* Retrieves the computed default sizing data based on world settings
* and the sheet class' DEFAULT_OPTIONS
* @returns {SheetSizing}
*/
export function getDefaultSizing() {
/** @type {SheetSizing} */
const sizing = {
width: undefined,
height: undefined,
resizable: undefined,
};
// TODO: defaults from world settings
// Defaults from the sheet class itself
sizing.height ||= PlayerSheet.DEFAULT_OPTIONS.position.height;
sizing.width ||= PlayerSheet.DEFAULT_OPTIONS.position.width;
sizing.resizable ||= PlayerSheet.DEFAULT_OPTIONS.window.resizable;
return sizing;
};

View file

@ -1,20 +0,0 @@
.taf.ResizeControlManager {
fieldset {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
align-items: center;
gap: 8px;
border: 1px solid rebeccapurple;
border-radius: 4px;
}
.controls {
display: flex;
flex-direction: row;
gap: 8px;
button {
flex-grow: 1;
}
}
}

View file

@ -0,0 +1,15 @@
.taf.sheet-config {
section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.tab {
display: none;
}
.tab.active {
display: unset;
}
}

View file

@ -20,4 +20,4 @@
@import url("./Apps/Ask.css") layer(apps); @import url("./Apps/Ask.css") layer(apps);
@import url("./Apps/AttributeManager.css") layer(apps); @import url("./Apps/AttributeManager.css") layer(apps);
@import url("./Apps/PlayerSheet.css") layer(apps); @import url("./Apps/PlayerSheet.css") layer(apps);
@import url("./Apps/ResizeControlManager.css") layer(apps); @import url("./Apps/TAFDocumentSheetConfig.css") layer(apps);

View file

@ -44,7 +44,7 @@
"flags": { "flags": {
"hotReload": { "hotReload": {
"extensions": ["css", "hbs", "json", "js", "mjs", "svg"], "extensions": ["css", "hbs", "json", "js", "mjs", "svg"],
"paths": ["templates", "langs", ".styles", "module", "assets"] "paths": ["templates", "langs", "styles", "module", "assets"]
} }
} }
} }

View file

@ -1,7 +0,0 @@
<div class="controls">
<button
type="submit"
>
Save and Close
</button>
</div>

View file

@ -1,29 +0,0 @@
<div class="settings">
<p>
Changes to these settings will only take effect after a reload of Foundry.
</p>
<fieldset>
<legend>Sizing</legend>
<label for="{{ meta.idp }}-width">Width</label>
<input
type="number"
id="{{ meta.idp }}-width"
value="{{ width }}"
name="flags.taf.PlayerSheet.size.width"
>
<label for="{{ meta.idp }}-height">Height</label>
<input
type="number"
id="{{ meta.idp }}-height"
value="{{ height }}"
name="flags.taf.PlayerSheet.size.height"
>
<label for="{{ meta.idp }}-resizable">Resizable?</label>
<select
id="{{ meta.idp }}-resizable"
name="flags.taf.PlayerSheet.size.resizable"
>
{{ taf-options resizable resizeOptions }}
</select>
</fieldset>
</div>

View file

@ -0,0 +1,3 @@
<div class="foundry tab {{tabs.foundry.cssClass}}" data-group="main" data-tab="foundry">
{{> "templates/sheets/document-sheet-config.hbs" }}
</div>

View file

@ -0,0 +1,48 @@
<div class="system tab {{tabs.system.cssClass}}" data-group="main" data-tab="system">
<fieldset>
<legend>
{{ localize "taf.Apps.TAFDocumentSheetConfig.Sizing" }}
</legend>
<div class="form-group">
<label for="{{meta.idp}}-width">
{{ localize "taf.Apps.TAFDocumentSheetConfig.Width.label" }}
</label>
<div class="form-fields">
<input
type="number"
name="FLAGS.taf.PlayerSheet.size.width"
id="{{meta.idp}}-width"
value="{{values.width}}"
placeholder="{{placeholders.width}}"
>
</div>
</div>
<div class="form-group">
<label for="{{meta.idp}}-height">
{{ localize "taf.Apps.TAFDocumentSheetConfig.Height.label" }}
</label>
<div class="form-fields">
<input
type="number"
name="FLAGS.taf.PlayerSheet.size.height"
id="{{meta.idp}}-height"
value="{{values.height}}"
placeholder="{{placeholders.height}}"
>
</div>
</div>
<div class="form-group">
<label for="{{meta.idp}}-resize">
{{ localize "taf.Apps.TAFDocumentSheetConfig.Resizable.label" }}
</label>
<div class="form-fields">
<select
name="FLAGS.taf.PlayerSheet.size.resizable"
id="{{meta.idp}}-resize"
>
{{ taf-options values.resizable resizeOptions }}
</select>
</div>
</div>
</fieldset>
</div>