Initial
This commit is contained in:
364
resources/app/client/apps/forms/sheet-config.js
Normal file
364
resources/app/client/apps/forms/sheet-config.js
Normal file
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* Document Sheet Configuration Application
|
||||
*/
|
||||
class DocumentSheetConfig extends FormApplication {
|
||||
|
||||
/** @inheritdoc */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ["form", "sheet-config"],
|
||||
template: "templates/sheets/sheet-config.html",
|
||||
width: 400
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of pending sheet assignments which are submitted before other elements of the framework are ready.
|
||||
* @type {object[]}
|
||||
* @private
|
||||
*/
|
||||
static #pending = [];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
get title() {
|
||||
const name = this.object.name ?? game.i18n.localize(this.object.constructor.metadata.label);
|
||||
return `${name}: Sheet Configuration`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
getData(options={}) {
|
||||
const {sheetClasses, defaultClasses, defaultClass} = this.constructor.getSheetClassesForSubType(
|
||||
this.object.documentName,
|
||||
this.object.type || CONST.BASE_DOCUMENT_TYPE
|
||||
);
|
||||
|
||||
// Return data
|
||||
return {
|
||||
isGM: game.user.isGM,
|
||||
object: this.object.toObject(),
|
||||
options: this.options,
|
||||
sheetClass: this.object.getFlag("core", "sheetClass") ?? "",
|
||||
blankLabel: game.i18n.localize("SHEETS.DefaultSheet"),
|
||||
sheetClasses, defaultClass, defaultClasses
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
async _updateObject(event, formData) {
|
||||
event.preventDefault();
|
||||
const original = this.getData({});
|
||||
const defaultSheetChanged = formData.defaultClass !== original.defaultClass;
|
||||
const documentSheetChanged = formData.sheetClass !== original.sheetClass;
|
||||
|
||||
// Update world settings
|
||||
if ( game.user.isGM && defaultSheetChanged ) {
|
||||
const setting = game.settings.get("core", "sheetClasses") || {};
|
||||
const type = this.object.type || CONST.BASE_DOCUMENT_TYPE;
|
||||
foundry.utils.mergeObject(setting, {[`${this.object.documentName}.${type}`]: formData.defaultClass});
|
||||
await game.settings.set("core", "sheetClasses", setting);
|
||||
|
||||
// Trigger a sheet change manually if it wouldn't be triggered by the normal ClientDocument#_onUpdate workflow.
|
||||
if ( !documentSheetChanged ) return this.object._onSheetChange({ sheetOpen: true });
|
||||
}
|
||||
|
||||
// Update the document-specific override
|
||||
if ( documentSheetChanged ) return this.object.setFlag("core", "sheetClass", formData.sheetClass);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Marshal information on the available sheet classes for a given document type and sub-type, and format it for
|
||||
* display.
|
||||
* @param {string} documentName The Document type.
|
||||
* @param {string} subType The Document sub-type.
|
||||
* @returns {{sheetClasses: object, defaultClasses: object, defaultClass: string}}
|
||||
*/
|
||||
static getSheetClassesForSubType(documentName, subType) {
|
||||
const config = CONFIG[documentName];
|
||||
const defaultClasses = {};
|
||||
let defaultClass = null;
|
||||
const sheetClasses = Object.values(config.sheetClasses[subType]).reduce((obj, cfg) => {
|
||||
if ( cfg.canConfigure ) obj[cfg.id] = cfg.label;
|
||||
if ( cfg.default && !defaultClass ) defaultClass = cfg.id;
|
||||
if ( cfg.canConfigure && cfg.canBeDefault ) defaultClasses[cfg.id] = cfg.label;
|
||||
return obj;
|
||||
}, {});
|
||||
return {sheetClasses, defaultClasses, defaultClass};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Configuration Methods
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize the configured Sheet preferences for Documents which support dynamic Sheet assignment
|
||||
* Create the configuration structure for supported documents
|
||||
* Process any pending sheet registrations
|
||||
* Update the default values from settings data
|
||||
*/
|
||||
static initializeSheets() {
|
||||
for ( let cls of Object.values(foundry.documents) ) {
|
||||
const types = this._getDocumentTypes(cls);
|
||||
CONFIG[cls.documentName].sheetClasses = types.reduce((obj, type) => {
|
||||
obj[type] = {};
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Register any pending sheets
|
||||
this.#pending.forEach(p => {
|
||||
if ( p.action === "register" ) this.#registerSheet(p);
|
||||
else if ( p.action === "unregister" ) this.#unregisterSheet(p);
|
||||
});
|
||||
this.#pending = [];
|
||||
|
||||
// Update default sheet preferences
|
||||
const defaults = game.settings.get("core", "sheetClasses");
|
||||
this.updateDefaultSheets(defaults);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static _getDocumentTypes(cls, types=[]) {
|
||||
if ( types.length ) return types;
|
||||
return game.documentTypes[cls.documentName];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Register a sheet class as a candidate which can be used to display documents of a given type
|
||||
* @param {typeof ClientDocument} documentClass The Document class for which to register a new Sheet option
|
||||
* @param {string} scope Provide a unique namespace scope for this sheet
|
||||
* @param {typeof DocumentSheet} sheetClass A defined Application class used to render the sheet
|
||||
* @param {object} [config] Additional options used for sheet registration
|
||||
* @param {string|Function} [config.label] A human-readable label for the sheet name, which will be localized
|
||||
* @param {string[]} [config.types] An array of document types for which this sheet should be used
|
||||
* @param {boolean} [config.makeDefault=false] Whether to make this sheet the default for provided types
|
||||
* @param {boolean} [config.canBeDefault=true] Whether this sheet is available to be selected as a default sheet for
|
||||
* all Documents of that type.
|
||||
* @param {boolean} [config.canConfigure=true] Whether this sheet appears in the sheet configuration UI for users.
|
||||
*/
|
||||
static registerSheet(documentClass, scope, sheetClass, {
|
||||
label, types, makeDefault=false, canBeDefault=true, canConfigure=true
|
||||
}={}) {
|
||||
const id = `${scope}.${sheetClass.name}`;
|
||||
const config = {documentClass, id, label, sheetClass, types, makeDefault, canBeDefault, canConfigure};
|
||||
if ( game.ready ) this.#registerSheet(config);
|
||||
else {
|
||||
config.action = "register";
|
||||
this.#pending.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the sheet registration.
|
||||
* @param {object} config Configuration for how the sheet should be registered
|
||||
* @param {typeof ClientDocument} config.documentClass The Document class being registered
|
||||
* @param {string} config.id The sheet ID being registered
|
||||
* @param {string} config.label The human-readable sheet label
|
||||
* @param {typeof DocumentSheet} config.sheetClass The sheet class definition being registered
|
||||
* @param {object[]} config.types An array of types for which this sheet is added
|
||||
* @param {boolean} config.makeDefault Make this sheet the default for provided types?
|
||||
* @param {boolean} config.canBeDefault Whether this sheet is available to be selected as a default
|
||||
* sheet for all Documents of that type.
|
||||
* @param {boolean} config.canConfigure Whether the sheet appears in the sheet configuration UI for
|
||||
* users.
|
||||
*/
|
||||
static #registerSheet({documentClass, id, label, sheetClass, types, makeDefault, canBeDefault, canConfigure}={}) {
|
||||
types = this._getDocumentTypes(documentClass, types);
|
||||
const classes = CONFIG[documentClass.documentName]?.sheetClasses;
|
||||
const defaults = game.ready ? game.settings.get("core", "sheetClasses") : {};
|
||||
if ( typeof classes !== "object" ) return;
|
||||
for ( const t of types ) {
|
||||
classes[t] ||= {};
|
||||
const existingDefault = defaults[documentClass.documentName]?.[t];
|
||||
const isDefault = existingDefault ? (existingDefault === id) : makeDefault;
|
||||
if ( isDefault ) Object.values(classes[t]).forEach(s => s.default = false);
|
||||
if ( label instanceof Function ) label = label();
|
||||
else if ( label ) label = game.i18n.localize(label);
|
||||
else label = id;
|
||||
classes[t][id] = {
|
||||
id, label, canBeDefault, canConfigure,
|
||||
cls: sheetClass,
|
||||
default: isDefault
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Unregister a sheet class, removing it from the list of available Applications to use for a Document type
|
||||
* @param {typeof ClientDocument} documentClass The Document class for which to register a new Sheet option
|
||||
* @param {string} scope Provide a unique namespace scope for this sheet
|
||||
* @param {typeof DocumentSheet} sheetClass A defined DocumentSheet subclass used to render the sheet
|
||||
* @param {object} [config]
|
||||
* @param {object[]} [config.types] An Array of types for which this sheet should be removed
|
||||
*/
|
||||
static unregisterSheet(documentClass, scope, sheetClass, {types}={}) {
|
||||
const id = `${scope}.${sheetClass.name}`;
|
||||
const config = {documentClass, id, types};
|
||||
if ( game.ready ) this.#unregisterSheet(config);
|
||||
else {
|
||||
config.action = "unregister";
|
||||
this.#pending.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the sheet de-registration.
|
||||
* @param {object} config Configuration for how the sheet should be un-registered
|
||||
* @param {typeof ClientDocument} config.documentClass The Document class being unregistered
|
||||
* @param {string} config.id The sheet ID being unregistered
|
||||
* @param {object[]} config.types An array of types for which this sheet is removed
|
||||
*/
|
||||
static #unregisterSheet({documentClass, id, types}={}) {
|
||||
types = this._getDocumentTypes(documentClass, types);
|
||||
const classes = CONFIG[documentClass.documentName]?.sheetClasses;
|
||||
if ( typeof classes !== "object" ) return;
|
||||
for ( let t of types ) {
|
||||
delete classes[t][id];
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the current default Sheets using a new core world setting.
|
||||
* @param {object} setting
|
||||
*/
|
||||
static updateDefaultSheets(setting={}) {
|
||||
if ( !Object.keys(setting).length ) return;
|
||||
for ( let cls of Object.values(foundry.documents) ) {
|
||||
const documentName = cls.documentName;
|
||||
const cfg = CONFIG[documentName];
|
||||
const classes = cfg.sheetClasses;
|
||||
const collection = cfg.collection?.instance ?? [];
|
||||
const defaults = setting[documentName];
|
||||
if ( !defaults ) continue;
|
||||
|
||||
// Update default preference for registered sheets
|
||||
for ( let [type, sheetId] of Object.entries(defaults) ) {
|
||||
const sheets = Object.values(classes[type] || {});
|
||||
let requested = sheets.find(s => s.id === sheetId);
|
||||
if ( requested ) sheets.forEach(s => s.default = s.id === sheetId);
|
||||
}
|
||||
|
||||
// Close and de-register any existing sheets
|
||||
for ( let document of collection ) {
|
||||
for ( const [id, app] of Object.entries(document.apps) ) {
|
||||
app.close();
|
||||
delete document.apps[id];
|
||||
}
|
||||
document._sheet = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Initialize default sheet configurations for all document types.
|
||||
* @private
|
||||
*/
|
||||
static _registerDefaultSheets() {
|
||||
const defaultSheets = {
|
||||
// Documents
|
||||
Actor: ActorSheet,
|
||||
Adventure: AdventureImporter,
|
||||
Folder: FolderConfig,
|
||||
Item: ItemSheet,
|
||||
JournalEntry: JournalSheet,
|
||||
Macro: MacroConfig,
|
||||
Playlist: PlaylistConfig,
|
||||
RollTable: RollTableConfig,
|
||||
Scene: SceneConfig,
|
||||
User: foundry.applications.sheets.UserConfig,
|
||||
// Embedded Documents
|
||||
ActiveEffect: ActiveEffectConfig,
|
||||
AmbientLight: foundry.applications.sheets.AmbientLightConfig,
|
||||
AmbientSound: foundry.applications.sheets.AmbientSoundConfig,
|
||||
Card: CardConfig,
|
||||
Combatant: CombatantConfig,
|
||||
Drawing: DrawingConfig,
|
||||
MeasuredTemplate: MeasuredTemplateConfig,
|
||||
Note: NoteConfig,
|
||||
PlaylistSound: PlaylistSoundConfig,
|
||||
Region: foundry.applications.sheets.RegionConfig,
|
||||
RegionBehavior: foundry.applications.sheets.RegionBehaviorConfig,
|
||||
Tile: TileConfig,
|
||||
Token: TokenConfig,
|
||||
Wall: WallConfig
|
||||
};
|
||||
|
||||
Object.values(foundry.documents).forEach(base => {
|
||||
const type = base.documentName;
|
||||
const cfg = CONFIG[type];
|
||||
cfg.sheetClasses = {};
|
||||
const defaultSheet = defaultSheets[type];
|
||||
if ( !defaultSheet ) return;
|
||||
DocumentSheetConfig.registerSheet(cfg.documentClass, "core", defaultSheet, {
|
||||
makeDefault: true,
|
||||
label: () => game.i18n.format("SHEETS.DefaultDocumentSheet", {document: game.i18n.localize(`DOCUMENT.${type}`)})
|
||||
});
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(Cards, "core", CardsConfig, {
|
||||
label: "CARDS.CardsDeck",
|
||||
types: ["deck"],
|
||||
makeDefault: true
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(Cards, "core", CardsHand, {
|
||||
label: "CARDS.CardsHand",
|
||||
types: ["hand"],
|
||||
makeDefault: true
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(Cards, "core", CardsPile, {
|
||||
label: "CARDS.CardsPile",
|
||||
types: ["pile"],
|
||||
makeDefault: true
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalTextTinyMCESheet, {
|
||||
types: ["text"],
|
||||
label: () => game.i18n.localize("EDITOR.TinyMCE")
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalImagePageSheet, {
|
||||
types: ["image"],
|
||||
makeDefault: true,
|
||||
label: () =>
|
||||
game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypeImage")})
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalVideoPageSheet, {
|
||||
types: ["video"],
|
||||
makeDefault: true,
|
||||
label: () =>
|
||||
game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypeVideo")})
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalPDFPageSheet, {
|
||||
types: ["pdf"],
|
||||
makeDefault: true,
|
||||
label: () =>
|
||||
game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypePDF")})
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalTextPageSheet, {
|
||||
types: ["text"],
|
||||
makeDefault: true,
|
||||
label: () => {
|
||||
return game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {
|
||||
page: game.i18n.localize("JOURNALENTRYPAGE.TypeText")
|
||||
});
|
||||
}
|
||||
});
|
||||
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", MarkdownJournalPageSheet, {
|
||||
types: ["text"],
|
||||
label: () => game.i18n.localize("EDITOR.Markdown")
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user