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.

318 lines
13 KiB

1 year ago
  1. // Based on https://github.com/orcnog/autocomplete-whisper/blob/master/scripts/autocomplete-whisper.js
  2. export default class Autocomplete {
  3. handleRenderSidebarTab(app, html, data) {
  4. /* Set up markup for our UI to be injected */
  5. const $whisperMenuContainer = $('<div id="command-menu"></div>');
  6. const $ghostTextarea = $('<textarea class="chatghosttextarea" autocomplete="off" readonly disabled></textarea>');
  7. let $whisperMenu = $('<nav id="context-menu" class="expand-up"><ol class="context-items"></ol></nav>');
  8. let regex = new RegExp("\/[A-z]*");
  9. /* Add our UI to the DOM */
  10. $("#chat-message").before($whisperMenuContainer);
  11. $("#chat-message").after($ghostTextarea);
  12. /* Unbind original FVTT chat textarea keydown handler and implemnt our own to catch up/down keys first */
  13. $("#chat-message").off("keydown");
  14. $("#chat-message").on("keydown.menufocus", jumpToMenuHandler);
  15. /* Listen for chat input. Do stuff.*/
  16. $("#chat-message").on("input.whisperer", handleChatInput);
  17. /* Listen for "]" to close an array of targets (names) */
  18. $("#chat-message").on("keydown.closearray", listFinishHandler);
  19. /* Listen for up/down arrow presses to navigate exposed menu */
  20. $("#command-menu").on("keydown.menufocus", menuNavigationHandler);
  21. /* Listen for click on a menu item */
  22. $("#command-menu").on("click", "li", menuItemSelectionHandler);
  23. function handleChatInput() {
  24. if (!game.settings.get("_chatcommands", "autocomplete")) return;
  25. resetGhostText();
  26. const val = $("#chat-message").val();
  27. //console.log(val);
  28. if (val.match(regex)) {
  29. // It's a commands! Show a menu of commands and typeahead text if possible
  30. // let splt = val.split(regex);
  31. // console.log(splt);
  32. let input = val;
  33. let allCommands = [];
  34. allCommands = allCommands.concat(window.game.chatCommands.registeredCommands);
  35. if (game.settings.get("_chatcommands", "includeCoreCommands")) {
  36. allCommands = allCommands.concat(_getCoreCommands());
  37. }
  38. let matchingCommands = allCommands.filter((target) => {
  39. const p = target.commandKey.toUpperCase();
  40. const i = val.toUpperCase();
  41. return p.indexOf(i) >= 0 && p !== i;
  42. });
  43. //console.log(matchingCommands);
  44. if (matchingCommands.length > 0) {
  45. // At least one potential target exists.
  46. // show ghost text to autocomplete if there's a match starting with the characters already typed
  47. ghostText(input, matchingCommands);
  48. // set up and display the menu of whisperable names
  49. let listOfPlayers = "";
  50. for (let p in matchingCommands) {
  51. if (isNaN(p)) continue;
  52. let command = matchingCommands[p];
  53. const name = command.commandKey;
  54. let nameHtml = name;
  55. let startIndex = name.toUpperCase().indexOf(input.toUpperCase());
  56. if (input && startIndex > -1) {
  57. nameHtml = name.substr(0, startIndex) + "<strong>" + name.substr(startIndex, input.length) + "</strong>" + name.substr(startIndex + input.length);
  58. }
  59. listOfPlayers += `<li class="context-item" data-name="${name}" tabIndex="0"><i class="fas ${command.iconClass} fa-fw" style="padding-right: 5px;"></i>${nameHtml} - ${command.description}</li>`;
  60. }
  61. $whisperMenu.find("ol").html(listOfPlayers);
  62. $("#command-menu").html($whisperMenu);
  63. // set up click-outside listener to close menu
  64. $(window).on("click.outsidewhispermenu", (e) => {
  65. var $target = $(e.target);
  66. if (!$target.closest("#command-menu").length) {
  67. closeWhisperMenu();
  68. }
  69. });
  70. } else {
  71. closeWhisperMenu();
  72. }
  73. } else {
  74. closeWhisperMenu();
  75. }
  76. }
  77. function _getCoreCommands() {
  78. let commands = [];
  79. let chatCommands = window.game.chatCommands;
  80. commands.push(chatCommands.createCommandFromData({
  81. commandKey: "/ic",
  82. invokeOnCommand: (chatlog, messageText, chatdata) => {
  83. },
  84. shouldDisplayToChat: false,
  85. iconClass: "fa-dice-d20",
  86. description: "Speak in character"
  87. }));
  88. commands.push(chatCommands.createCommandFromData({
  89. commandKey: "/ooc",
  90. invokeOnCommand: (chatlog, messageText, chatdata) => {
  91. },
  92. shouldDisplayToChat: false,
  93. iconClass: "fa-dice-d20",
  94. description: "Speak out of character"
  95. }));
  96. commands.push(chatCommands.createCommandFromData({
  97. commandKey: "/emote",
  98. invokeOnCommand: (chatlog, messageText, chatdata) => {
  99. },
  100. shouldDisplayToChat: false,
  101. iconClass: "fa-dice-d20",
  102. description: "Emote in character"
  103. }));
  104. commands.push(chatCommands.createCommandFromData({
  105. commandKey: "/whisper",
  106. invokeOnCommand: (chatlog, messageText, chatdata) => {
  107. },
  108. shouldDisplayToChat: false,
  109. iconClass: "fa-dice-d20",
  110. description: "Send a whisper to another player"
  111. }));
  112. commands.push(chatCommands.createCommandFromData({
  113. commandKey: "/w",
  114. invokeOnCommand: (chatlog, messageText, chatdata) => {
  115. },
  116. shouldDisplayToChat: false,
  117. iconClass: "fa-dice-d20",
  118. description: "Send a whisper to another player"
  119. }));
  120. commands.push(chatCommands.createCommandFromData({
  121. commandKey: "/roll",
  122. invokeOnCommand: (chatlog, messageText, chatdata) => {
  123. },
  124. shouldDisplayToChat: false,
  125. iconClass: "fa-dice-d20",
  126. description: "Roll dice"
  127. }));
  128. commands.push(chatCommands.createCommandFromData({
  129. commandKey: "/gmroll",
  130. invokeOnCommand: (chatlog, messageText, chatdata) => {
  131. },
  132. shouldDisplayToChat: false,
  133. iconClass: "fa-dice-d20",
  134. description: "Roll dice that only the GM can see"
  135. }));
  136. commands.push(chatCommands.createCommandFromData({
  137. commandKey: "/blindroll",
  138. invokeOnCommand: (chatlog, messageText, chatdata) => {
  139. },
  140. shouldDisplayToChat: false,
  141. iconClass: "fa-dice-d20",
  142. description: "Roll dice that are hidden"
  143. }));
  144. commands.push(chatCommands.createCommandFromData({
  145. commandKey: "/selfroll",
  146. invokeOnCommand: (chatlog, messageText, chatdata) => {
  147. },
  148. shouldDisplayToChat: false,
  149. iconClass: "fa-dice-d20",
  150. description: "Roll dice that only you can see"
  151. }));
  152. return commands;
  153. }
  154. function listFinishHandler(e) {
  155. if (e.which == 221) { // `]`
  156. let val = $("#chat-message").val();
  157. if (val.match(listOfNamesRegex)) {
  158. if (typeof e === "object") e.preventDefault();
  159. val = val.trim();
  160. const newval = val.substring(val.length - 1) === "," ? val.substring(0, val.length - 1) : val; // remove `,` from the end
  161. $("#chat-message").val(newval + "] ");
  162. closeWhisperMenu();
  163. }
  164. }
  165. }
  166. function jumpToMenuHandler(e) {
  167. if ($("#command-menu").find("li").length) {
  168. if (e.which === 38) { // `up`
  169. $("#command-menu li:last-child").focus();
  170. return false;
  171. } else if (e.which === 40) { // `down`
  172. $("#command-menu li:first-child").focus();
  173. return false;
  174. }
  175. }
  176. if (game.modules.get("autocomplete-whisper") != undefined) {
  177. if ($("#whisper-menu").find("li").length) {
  178. if (e.which === 38) { // `up`
  179. $("#whisper-menu li:last-child").focus();
  180. return false;
  181. } else if (e.which === 40) { // `down`
  182. $("#whisper-menu li:first-child").focus();
  183. return false;
  184. }
  185. }
  186. }
  187. // if player menu is not visible/DNE, execute FVTT's original keydown handler
  188. ui.chat._onChatKeyDown(e);
  189. }
  190. function menuNavigationHandler(e) {
  191. if ($(e.target).is("li.context-item")) {
  192. if (e.which === 38) { // `up`
  193. if ($(e.target).is(":first-child")) {
  194. $("#chat-message").focus();
  195. } else {
  196. $(e.target).prev().focus();
  197. }
  198. return false;
  199. } else if (e.which === 40) { // `down`
  200. if ($(e.target).is(":last-child")) {
  201. $("#chat-message").focus();
  202. } else {
  203. $(e.target).next().focus();
  204. }
  205. return false;
  206. } else if (e.which === 13) { // `enter`
  207. menuItemSelectionHandler(e);
  208. return false;
  209. }
  210. }
  211. }
  212. function menuItemSelectionHandler(e) {
  213. e.stopPropagation();
  214. var autocompleteText = autocomplete($(e.target).text());
  215. $("#chat-message").val(autocompleteText.ghost);
  216. $("#chat-message").focus();
  217. closeWhisperMenu();
  218. if ($("#chat-message").val().indexOf("[") > -1) {
  219. handleChatInput();
  220. }
  221. }
  222. function ghostText(input, matches) {
  223. // show ghost text to autocomplete if there's a match starting with the characters already typed
  224. let filteredMatches = matches.filter((target) => {
  225. const p = target.commandKey.toUpperCase();
  226. const i = input.toUpperCase();
  227. return p.indexOf(i) === 0 && p !== i;
  228. });
  229. if (filteredMatches.length === 1) {
  230. var autocompleteText = autocomplete(filteredMatches[0].commandKey);
  231. $(".chatghosttextarea").val(autocompleteText.ghost);
  232. $(".chatghosttextarea").addClass("show");
  233. $("#chat-message").on("keydown.ghosttab", (e) => {
  234. if (e.which == 9) { // tab
  235. e.preventDefault();
  236. $("#chat-message").val(autocompleteText.ghost);
  237. resetGhostText();
  238. $("#chat-message").focus();
  239. closeWhisperMenu();
  240. if ($("#chat-message").val().indexOf("[") > -1) {
  241. handleChatInput();
  242. }
  243. }
  244. });
  245. } else {
  246. resetGhostText();
  247. }
  248. }
  249. function autocomplete(match) {
  250. if (!match) return;
  251. const typedCharacters = $("#chat-message").val();
  252. let nameToAdd = '';
  253. if (match.toUpperCase().indexOf(typedCharacters.toUpperCase()) === 0) {
  254. var restOfTheName = match.substr(typedCharacters.length);
  255. nameToAdd = typedCharacters + restOfTheName;
  256. } else {
  257. nameToAdd = match;
  258. }
  259. if (nameToAdd.includes(" - ")) {
  260. nameToAdd = nameToAdd.substr(0, nameToAdd.indexOf(" - "));
  261. }
  262. const ghostString = nameToAdd;
  263. return ({
  264. ghost: ghostString
  265. });
  266. }
  267. function closeWhisperMenu() {
  268. $("#command-menu").empty();
  269. $(window).off("click.outsidewhispermenu");
  270. resetGhostText();
  271. }
  272. function resetGhostText() {
  273. $("#chat-message").off("keydown.ghosttab");
  274. $(".chatghosttextarea").val("").removeClass("show");
  275. }
  276. }
  277. }