All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

150 lines
7.1 KiB

  1. /**
  2. * Represents a single chat command.
  3. */
  4. class ChatCommand {
  5. /**
  6. * Creates a new chat command from the given data.
  7. * @param {object} data The data of the command.
  8. * @param {string} data.name The name that is used to invoke the command.
  9. * @param {string} data.module The ID of the module that registered the command.
  10. * @param {string[]} data.aliases Aliases that can be used instead of the name. Defaults to an empty array.
  11. * @param {string?} data.description The human readable description of the command.
  12. * @param {string?} data.icon An HTML string containing the icon of the command.
  13. * @param {string=} data.requiredRole A minimum role that is required to invoke the command. Defaults to "NONE" (everyone).
  14. * @param {Function?} data.callback An optional function that is called with the chat application that triggered
  15. * the command, its parameters and the original message data. The function can return null to apply FoundryVTT
  16. * core handling, an empty object to omit the message or a message data object that will be sent to the chat.
  17. * @param {Function?} data.autocompleteCallback An optional function that is called when a user is typing the
  18. * command with the menu that triggered the completion, the command's alias and its parameters. The function can
  19. * return an array of @see HTMLElement or an @see HTMLCollection that will be displayed in the autocomplete menu.
  20. * It may also return an asynchronous generator yielding element arrays.
  21. * @param {boolean=} data.closeOnComplete Indicates that the menu should be closed after an entry is selected.
  22. * Defaults to true.
  23. */
  24. constructor(data) {
  25. Object.assign(this, data);
  26. this.name = data.name.toLowerCase();
  27. this.aliases = (data.aliases ?? []).map(a => a.toLowerCase());
  28. this.moduleName = data.module === "core" ? "FoundryVTT" : (game.modules.get(data.module)?.title ?? data.module);
  29. this.requiredRole ??= "NONE";
  30. }
  31. /**
  32. * Returns every name that this command can be invoked with (including aliases).
  33. */
  34. get names() {
  35. return [this.name].concat(this.aliases);
  36. }
  37. /**
  38. * Describes the command with predefined elements.
  39. * @param {string} alias The alias that should be described.
  40. * @param {boolean} footer Indicates whether a footer with other aliases and the module name will be displayed.
  41. * @returns {string} An HTML string containing a detailed command description.
  42. */
  43. getDescription(alias = this.name, footer = true) {
  44. let content = "<span class='command-title'>";
  45. if (this.icon) content += this.icon;
  46. content += alias + "</span>";
  47. // Add additional information.
  48. if (this.description) content += " - " + this.description;
  49. if (footer) {
  50. content += `<div class="command-footer"><span class="notes">${this.moduleName}</span>`;
  51. if (this.aliases.length) {
  52. const aliases = this.names.filter(a => a !== alias);
  53. content += `<span class="notes">${aliases.join(", ")}</span>`;
  54. }
  55. content += "</div>";
  56. }
  57. return content;
  58. }
  59. /**
  60. * Removes an alias from this command. Note that this should only be called before registering the command since it
  61. * does not remove the alias from the command registry.
  62. * @param {string} name The alias to remove.
  63. */
  64. removeAlias(name) {
  65. const index = this.aliases.indexOf(name);
  66. if (index !== -1) this.aliases.splice(index, 1);
  67. }
  68. /**
  69. * Checks if the command can be invoked by the current user.
  70. * @returns {boolean} True if the command can be invoked by the current user, false otherwise.
  71. */
  72. canInvoke() {
  73. return this.requiredRole === "NONE" || game.user.hasRole(this.requiredRole);
  74. }
  75. /**
  76. * Invokes the callback of the command.
  77. * @param {ChatLog} chat The chat application that the command is being invoked from.
  78. * @param {string} parameters The parameters of the command (if any).
  79. * @param {object} messageData The data of the chat message invoking the command.
  80. * @returns {?object|Promise} A chat message data object containing a new message. If omitted, no message will be
  81. * sent. Alternatively, this may return a promise representing the result if the command's callback is asynchronous.
  82. */
  83. invoke(chat, parameters, messageData) {
  84. if (!this.callback) return;
  85. return this.callback(chat, parameters, messageData);
  86. }
  87. /**
  88. * Invokes the autocomplete callback of the command.
  89. * @param {AutocompleteMenu} menu The menu that the autocompletion is being invoked from.
  90. * @param {string} alias The alias that was used to initiate the autocomplete.
  91. * @param {string} parameters The parameters of the command (if any).
  92. * @returns {?string[]} A list of HTML strings or an HTMLCollection containing entries to complete the command or
  93. * null when the command has no autocomplete callback.
  94. */
  95. autocomplete(menu, alias, parameters) {
  96. if (!this.autocompleteCallback) return null;
  97. const result = this.autocompleteCallback(menu, alias, parameters);
  98. if (!result) return [];
  99. if (result instanceof HTMLCollection || Array.isArray(result)) return result;
  100. if (result.next instanceof Function) {
  101. // Callback returned an iterator.
  102. this.abortAutocomplete();
  103. result.menu = menu;
  104. result.remaining = menu.maxEntries;
  105. this.currentAutocomplete = result;
  106. setTimeout(() => this.resumeAutocomplete(result), 400);
  107. return [game.chatCommands.createLoadingElement()];
  108. }
  109. }
  110. /**
  111. * Calls the next iteration of the given generator if it still belongs to the current autocomplete process. The
  112. * result is displayed using the callback passed to @see autocomplete.
  113. * @param {AsyncGenerator} generator The generator to fetch new entries with.
  114. */
  115. async resumeAutocomplete(generator) {
  116. if (!generator || generator !== this.currentAutocomplete) return;
  117. let result;
  118. if (generator.remaining <= 0) {
  119. // Stop iterating after reaching the maximum entries.
  120. generator.return([]);
  121. result = { value: [null], done: true };
  122. } else {
  123. result = await generator.next(); // Get the next set of results.
  124. if (generator !== this.currentAutocomplete) return; // Make sure that we're still current after waiting.
  125. }
  126. const value = result.value ?? [];
  127. generator.remaining -= value.length;
  128. generator.menu.display(value, true, result.done);
  129. if (result.done) this.currentAutocomplete = null;
  130. else setTimeout(() => this.resumeAutocomplete(generator));
  131. }
  132. /**
  133. * Aborts the current autocomplete process (if there is one) without displaying the result.
  134. */
  135. abortAutocomplete() {
  136. this.currentAutocomplete?.return([]);
  137. }
  138. }
  139. export default ChatCommand;