|
/**
|
|
* 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;
|