|
|
- /**
- * Represents a single chat command.
- */
- class ChatCommand {
- /**
- * Creates a new chat command from the given data.
- * @param {object} data The data of the command.
- * @param {string} data.name The name that is used to invoke the command.
- * @param {string} data.module The ID of the module that registered the command.
- * @param {string[]} data.aliases Aliases that can be used instead of the name. Defaults to an empty array.
- * @param {string?} data.description The human readable description of the command.
- * @param {string?} data.icon An HTML string containing the icon of the command.
- * @param {string=} data.requiredRole A minimum role that is required to invoke the command. Defaults to "NONE" (everyone).
- * @param {Function?} data.callback An optional function that is called with the chat application that triggered
- * the command, its parameters and the original message data. The function can return null to apply FoundryVTT
- * core handling, an empty object to omit the message or a message data object that will be sent to the chat.
- * @param {Function?} data.autocompleteCallback An optional function that is called when a user is typing the
- * command with the menu that triggered the completion, the command's alias and its parameters. The function can
- * return an array of @see HTMLElement or an @see HTMLCollection that will be displayed in the autocomplete menu.
- * It may also return an asynchronous generator yielding element arrays.
- * @param {boolean=} data.closeOnComplete Indicates that the menu should be closed after an entry is selected.
- * Defaults to true.
- */
- constructor(data) {
- Object.assign(this, data);
- this.name = data.name.toLowerCase();
- this.aliases = (data.aliases ?? []).map(a => a.toLowerCase());
- this.moduleName = data.module === "core" ? "FoundryVTT" : (game.modules.get(data.module)?.title ?? data.module);
- this.requiredRole ??= "NONE";
- }
-
- /**
- * Returns every name that this command can be invoked with (including aliases).
- */
- get names() {
- return [this.name].concat(this.aliases);
- }
-
- /**
- * Describes the command with predefined elements.
- * @param {string} alias The alias that should be described.
- * @param {boolean} footer Indicates whether a footer with other aliases and the module name will be displayed.
- * @returns {string} An HTML string containing a detailed command description.
- */
- getDescription(alias = this.name, footer = true) {
- let content = "<span class='command-title'>";
- if (this.icon) content += this.icon;
- content += alias + "</span>";
-
- // Add additional information.
- if (this.description) content += " - " + this.description;
- if (footer) {
- content += `<div class="command-footer"><span class="notes">${this.moduleName}</span>`;
- if (this.aliases.length) {
- const aliases = this.names.filter(a => a !== alias);
- content += `<span class="notes">${aliases.join(", ")}</span>`;
- }
- content += "</div>";
- }
-
- return content;
- }
-
- /**
- * Removes an alias from this command. Note that this should only be called before registering the command since it
- * does not remove the alias from the command registry.
- * @param {string} name The alias to remove.
- */
- removeAlias(name) {
- const index = this.aliases.indexOf(name);
- if (index !== -1) this.aliases.splice(index, 1);
- }
-
- /**
- * Checks if the command can be invoked by the current user.
- * @returns {boolean} True if the command can be invoked by the current user, false otherwise.
- */
- canInvoke() {
- return this.requiredRole === "NONE" || game.user.hasRole(this.requiredRole);
- }
-
- /**
- * Invokes the callback of the command.
- * @param {ChatLog} chat The chat application that the command is being invoked from.
- * @param {string} parameters The parameters of the command (if any).
- * @param {object} messageData The data of the chat message invoking the command.
- * @returns {?object|Promise} A chat message data object containing a new message. If omitted, no message will be
- * sent. Alternatively, this may return a promise representing the result if the command's callback is asynchronous.
- */
- invoke(chat, parameters, messageData) {
- if (!this.callback) return;
- return this.callback(chat, parameters, messageData);
- }
-
- /**
- * Invokes the autocomplete callback of the command.
- * @param {AutocompleteMenu} menu The menu that the autocompletion is being invoked from.
- * @param {string} alias The alias that was used to initiate the autocomplete.
- * @param {string} parameters The parameters of the command (if any).
- * @returns {?string[]} A list of HTML strings or an HTMLCollection containing entries to complete the command or
- * null when the command has no autocomplete callback.
- */
- autocomplete(menu, alias, parameters) {
- if (!this.autocompleteCallback) return null;
- const result = this.autocompleteCallback(menu, alias, parameters);
- if (!result) return [];
- if (result instanceof HTMLCollection || Array.isArray(result)) return result;
- if (result.next instanceof Function) {
- // Callback returned an iterator.
- this.abortAutocomplete();
- result.menu = menu;
- result.remaining = menu.maxEntries;
- this.currentAutocomplete = result;
- setTimeout(() => this.resumeAutocomplete(result), 400);
- return [game.chatCommands.createLoadingElement()];
- }
- }
-
- /**
- * Calls the next iteration of the given generator if it still belongs to the current autocomplete process. The
- * result is displayed using the callback passed to @see autocomplete.
- * @param {AsyncGenerator} generator The generator to fetch new entries with.
- */
- async resumeAutocomplete(generator) {
- if (!generator || generator !== this.currentAutocomplete) return;
- let result;
- if (generator.remaining <= 0) {
- // Stop iterating after reaching the maximum entries.
- generator.return([]);
- result = { value: [null], done: true };
- } else {
- result = await generator.next(); // Get the next set of results.
- if (generator !== this.currentAutocomplete) return; // Make sure that we're still current after waiting.
- }
-
- const value = result.value ?? [];
- generator.remaining -= value.length;
- generator.menu.display(value, true, result.done);
- if (result.done) this.currentAutocomplete = null;
- else setTimeout(() => this.resumeAutocomplete(generator));
- }
-
- /**
- * Aborts the current autocomplete process (if there is one) without displaying the result.
- */
- abortAutocomplete() {
- this.currentAutocomplete?.return([]);
- }
- }
-
- export default ChatCommand;
|