diff --git a/README.md b/README.md index 2095ca0..ba27310 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,55 @@ Once this library is active (it should be activated at the start of the `init` h - Playlist - Scene - User -- ~~Folder~~ (excluded for complexity) +- Folder + +### Hooks + +This library provides two hooks: +```js +Hooks.on("preDocumentSheetRegistrarInit", (settings) => {}); +Hooks.on("documentSheetRegistrarInit", (documentTypes) => {}); +``` + +The `preDocumentSheetRegistrarInit` hook passes an object of boolean "settings", you must set the setting corresponding to the document type that you wish to register a sheet for to `true`. If you do not do this, the registration method will not be created which will produce an error when you call it. + +The `documentSheetRegistrarInit` hook indicates that the initialization process has been completed, and it is now safe to register your sheets. This hook also passes an object of data about any documents for which this library has been enabled. + +### Settings + +From the `preDocumentSheetRegistrarInit` hook you can choose to enable this library for particular document types. The hook passes a `settings` parameter which is an object like so: + +```js +{ + Actor: false + Folder: false + Item: false + JournalEntry: false + Macro: false + Playlist: false + RollTable: false + Scene: false + User: false +} +``` + +You need only to set the appropriate value to `true` for the document type you wish to register a sheet for. + +Example: + +```js +Hooks.on("preDocumentSheetRegistrarInit", (settings) => { + settings["JournalEntry"] = true; +}); +``` + +This will enable `Journal.registerSheet`. ### `DocType.registerSheet` -Register a sheet class as a candidate which can be used to display Journal Entries. +Register a sheet class as a candidate which can be used to display this document. + +You must enable your chosen document type in the `preDocumentSheetRegistrarInit` hook for this method to be available. #### Parameters @@ -44,7 +88,7 @@ Register a sheet class as a candidate which can be used to display Journal Entri | sheetClass | `Application` | A defined Application class used to render the sheet |   | | options | `Object` | Additional options used for sheet registration |   | | options.label | `string` | A human readable label for the sheet name, which will be localized | *Optional* | -| options.types | `Array.` | An array of entity types for which this sheet should be used | *Optional* | +| options.types | `Array.` | An array of entity types for which this sheet should be used. When not specified, all types will be used. That does *not* include artificial types, if you are using artificial types you must specify them here. | *Optional* | | options.makeDefault | `boolean` | Whether to make this sheet the default for provided types | *Optional* | #### Examples @@ -58,8 +102,11 @@ Journal.registerSheet?.("myModule", SheetApplicationClass, { ``` ### `DocType.unregisterSheet` + Unregister a Journal Entry sheet class, removing it from the list of available Applications to use for Journal Entries. +You must enable your chosen document type in the `preDocumentSheetRegistrarInit` hook for this method to be available. + #### Parameters | Name | Type | Description | | @@ -86,3 +133,44 @@ It can be set in [any of the ways a flag can be set](https://foundryvtt.wiki/en/ ```js someJournalEntry.setFlag('core', 'sheetClass', 'my-module.MyModuleSheetClassName'); ``` +### Types + +This library introduces the ability to give documents "types" even for documents that did not support types before. The `object.type` property is supported on many documents in Core such as Actor (character, npc, vehicle), Item, Macro (script, chat), and others. This allows a document to have type-specific sheets such as NPC sheets vs. character sheets. With Document Sheet Registrar, we can add "artificial" types to any of the following nine do documents: + +- Actor +- Item +- JournalEntry +- RollTable +- Macro +- Playlist +- Scene +- User +- Folder + +There are two steps to adding a new artificial type. First, you must register a new sheet and pass your custom type as part of the `types` array: + +```javascript +DocType.registerSheet?.("myModule", SheetApplicationClass, { + types: ["my-type"], + makeDefault: false, + label: "My document sheet" +}); +``` + +When you register a sheet in this way, the sheet will only be available to documents with the specified type. Since the `object.data.type` property is part of the official schema of the document, we can not add this property to documents that don't already support it, or give it a custom value. Instead, DSR uses a `type` flag in the `_document-sheet-registrar` scope to specify the artificial type. + +```js +document.setFlag("_document-sheet-registrar", "type", "my-type") +``` + +This will cause the `object.type` getter on the document to return the value stored in this flag, resulting in a different selection of sheets which are specific to that `type`. + +Note that if no sheet is registered to handle a given document and type, an error will occur. When this happens with the library enabled, a UI warning is displayed. When the library is disabled, the default sheet for that document will render. + +## The Sheet Config Dialog + +In order to give the user control of how their documents are rendered, documents that have multiple registered sheets will now have a "⚙ sheet" button in the header of their sheet application. This button opens the same sheet dialog that is used by Actor and Item. + +If for some reason you need to prevent users from modifying this, you can hide the button with CSS by targeting the `.configure-sheet` class on the element. + +If it is important that documents created for your module only be rendered using sheets provided by your module, you may also want to restrict which sheets are available by setting a particular `type` for those sheets. You can specify an existing type for documents that support it, e.g. "script" Macros, or you can use the artificial types system discussed in the API section above. The sheet config dialog will only give the user the option to select a sheet that is valid for the type of the document being configured. diff --git a/module.json b/module.json index 4c37144..5f2bbe7 100644 --- a/module.json +++ b/module.json @@ -2,11 +2,11 @@ "name": "_document-sheet-registrar", "title": "Lib: Document Sheet Registrar", "description": "A library module which enables the registration of alternative document sheets for all document types that don't normally have this capacity.", - "version": "0.5.0", + "version": "0.6.1", "library": true, "manifestPlusVersion": "1.1.0", "minimumCoreVersion": "0.8.8", - "compatibleCoreVersion": "0.8.8", + "compatibleCoreVersion": "0.8.9", "languages": [ { "lang": "en", diff --git a/scripts/document-sheet-registrar.js b/scripts/document-sheet-registrar.js index f8cde67..e2655ca 100644 --- a/scripts/document-sheet-registrar.js +++ b/scripts/document-sheet-registrar.js @@ -31,6 +31,44 @@ export default class DocumentSheetRegistrar { */ static get name() { return "_document-sheet-registrar"; } + + /** + * A function to filter the CONFIG...Document object for only + * documents that have either the sheetClass or sheetClasses property + * and have a collection. + * + * Since these properties can be getters, it can be dangerous to run the + * getters this early in the init process. Instead, we use + * Object.getOwnPropertyDescriptors to check if the properties exist. + * + * @static + * @param {[string, object]} [key, config] - The key and config object for the document type + * @return {boolean} True if the document fits the criteria, false otherwise + * @memberof DocumentSheetRegistrar + */ + static filterDocs([key, config]) { + return ( + Object.getOwnPropertyDescriptor(config, "sheetClass") || + Object.getOwnPropertyDescriptor(config, "sheetClasses") + ) && config.collection; + } + + + /** + * A list of booleans for each document type that indicates whether + * or not the sheet registration is enabled. + * + * @type {object} + * + * @static + * @memberof DocumentSheetRegistrar + */ + static settings = Object.fromEntries( + Object.entries(CONFIG) + .filter(this.filterDocs) + .map(([key, config]) => [key, false]) + ); + /** * @typedef {object} DocumentMap A map of document name, class, and collection * @property {string} name - The name of the document @@ -42,13 +80,14 @@ export default class DocumentSheetRegistrar { */ static get documentTypes() { return Object.entries(CONFIG) - .filter(([key, config]) => config.sheetClass && config.collection) + .filter(this.filterDocs) .map(([key, config]) => { /** @return {DocumentMap} */ return { name: key, class: config.documentClass, - collection: config.collection + collection: config.collection, + enabled: this.settings[key] } }); } @@ -89,20 +128,28 @@ export default class DocumentSheetRegistrar { /** - * Initialize all of the document sheet registrars. + * Handles the init hook + * + * Initializes all of the document sheet registrars, + * then sets up some wrapper functions. + * + * Calls a pre-init hook to allow modules to request certain + * sheet registration options. + * + * Finally calls a post-init hook to alert modules that the + * document sheet registrar has been initialized. * * @static * @memberof DocumentSheetRegistrar */ - static initializeDocumentSheets() { - console.log(game.i18n.localize("_document-sheet-registrar.console.log.init")); + static init() { + console.log(game.i18n.localize("Document Sheet Registrar: initializing...")); - for (let doc of this.documentTypes) { - // Skip any collection that already has a sheet registration method - if (doc.collection.registerSheet) continue; + // Call settings hook for this module + Hooks.callAll("preDocumentSheetRegistrarInit", this.settings); - this.initializeDocumentSheet(doc); - } + // Initialize all of the document sheet registrars + this.initializeDocumentSheets(); // Add a sheet config event handler for header buttons on DocumentSheet DocumentSheet.prototype._onConfigureSheet = this._onConfigureSheet; @@ -115,6 +162,27 @@ export default class DocumentSheetRegistrar { this.object.data.type = this.object.type; return wrapped(...args); }, "WRAPPER"); + + console.log(game.i18n.localize("Document Sheet Registrar: ...ready!")); + + // Call the init hook to alert modules that the registrar is ready + Hooks.callAll("documentSheetRegistrarInit", Object.fromEntries( + this.documentTypes.filter(doc => doc.enabled).map(doc => [doc.name, doc]) + )); + } + + + /** + * Initialize all of the document sheet registrars. + * + * @static + * @memberof DocumentSheetRegistrar + */ + static initializeDocumentSheets() { + for (let doc of this.documentTypes) { + // Skip documents that aren't enabled + if (doc.enabled) this.initializeDocumentSheet(doc); + } } @@ -170,7 +238,8 @@ export default class DocumentSheetRegistrar { * @memberof DocumentSheetRegistrar */ static configureSheetClasses(doc) { - CONFIG[doc.name].sheetClasses = { }; + if (!CONFIG[doc.name]?.sheetClasses) + CONFIG[doc.name].sheetClasses = { }; if (doc.class.metadata.types.length) { for (let type of doc.class.metadata.types) { @@ -193,6 +262,9 @@ export default class DocumentSheetRegistrar { * @memberof DocumentSheetRegistrar */ static configureSheetClassessByType(doc, type) { + // If this config already exists, do nothing + if (CONFIG[doc.name].sheetClasses[type]) return; + CONFIG[doc.name].sheetClasses[type] = { [doc.name]: { // Register the default sheet id: doc.name, @@ -240,7 +312,7 @@ export default class DocumentSheetRegistrar { * @param {Application} sheetClass A defined Application class used to render the sheet * @param {Object} options Additional options used for sheet registration * @param {string} [options.label] A human readable label for the sheet name, which will be localized - * @param {string[]} [options.types] An array of entity types for which this sheet should be used + * @param {string[]} [options.types] An array of entity types for which this sheet should be used. When not specified, all types will be used. That does *not* include artificial types, if you are using artificial types you must specify them here. * @param {boolean} [options.makeDefault] Whether to make this sheet the default for provided types * * @example @@ -283,8 +355,18 @@ export default class DocumentSheetRegistrar { } - /*********************************************************************************************/ - + /********************************************************************************************* + * This section contains code copied from the Foundry core software and modified for + * this library. + * + * Foundry Virtual Tabletop © Copyright 2021, Foundry Gaming, LLC. + * + * This code is used in accordance with the Foundry Virtual Tabletop + * LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT. + * + * https://foundryvtt.com/article/license/ + * + *********************************************************************************************/ /** * Retrieve the sheet class for the document. @see Actor._getSheetClass @@ -326,10 +408,7 @@ export default class DocumentSheetRegistrar { */ static updateDefaultSheets(setting = {}) { if (!Object.keys(setting).length) return; - const documents = [ - "Actor", "Item", - ...DocumentSheetRegistrar.documentTypes.map(doc => doc.name) - ]; + const documents = DocumentSheetRegistrar.documentTypes.filter(doc => doc.enabled).map(doc => doc.name); for (let documentName of documents) { const cfg = CONFIG[documentName]; const classes = cfg.sheetClasses; @@ -351,10 +430,17 @@ export default class DocumentSheetRegistrar { } } } + + /********************************************************************************************* + * + * END OF SECTION COPIED FROM FOUNDRY CORE SOFTWARE + * + *********************************************************************************************/ } + // On init, create the nessesary configs and methods to enable the sheet config API -Hooks.once("init", DocumentSheetRegistrar.initializeDocumentSheets.bind(DocumentSheetRegistrar)); +Hooks.once("init", DocumentSheetRegistrar.init.bind(DocumentSheetRegistrar)); // When a doc sheet is rendered, add a header button for sheet configuration Hooks.on("getDocumentSheetHeaderButtons", DocumentSheetRegistrar.getDocumentSheetHeaderButtons.bind(DocumentSheetRegistrar)); \ No newline at end of file