// Based on https://github.com/orcnog/autocomplete-whisper/blob/master/scripts/autocomplete-whisper.js export default class Autocomplete { handleRenderSidebarTab(app, html, data) { /* Set up markup for our UI to be injected */ const $whisperMenuContainer = $('
'); const $ghostTextarea = $(''); let $whisperMenu = $(''); let regex = new RegExp("\/[A-z]*"); /* Add our UI to the DOM */ $("#chat-message").before($whisperMenuContainer); $("#chat-message").after($ghostTextarea); /* Unbind original FVTT chat textarea keydown handler and implemnt our own to catch up/down keys first */ $("#chat-message").off("keydown"); $("#chat-message").on("keydown.menufocus", jumpToMenuHandler); /* Listen for chat input. Do stuff.*/ $("#chat-message").on("input.whisperer", handleChatInput); /* Listen for "]" to close an array of targets (names) */ $("#chat-message").on("keydown.closearray", listFinishHandler); /* Listen for up/down arrow presses to navigate exposed menu */ $("#command-menu").on("keydown.menufocus", menuNavigationHandler); /* Listen for click on a menu item */ $("#command-menu").on("click", "li", menuItemSelectionHandler); function handleChatInput() { if (!game.settings.get("_chatcommands", "autocomplete")) return; resetGhostText(); const val = $("#chat-message").val(); //console.log(val); if (val.match(regex)) { // It's a commands! Show a menu of commands and typeahead text if possible // let splt = val.split(regex); // console.log(splt); let input = val; let allCommands = []; allCommands = allCommands.concat(window.game.chatCommands.registeredCommands); if (game.settings.get("_chatcommands", "includeCoreCommands")) { allCommands = allCommands.concat(_getCoreCommands()); } let matchingCommands = allCommands.filter((target) => { const p = target.commandKey.toUpperCase(); const i = val.toUpperCase(); return p.indexOf(i) >= 0 && p !== i; }); //console.log(matchingCommands); if (matchingCommands.length > 0) { // At least one potential target exists. // show ghost text to autocomplete if there's a match starting with the characters already typed ghostText(input, matchingCommands); // set up and display the menu of whisperable names let listOfPlayers = ""; for (let p in matchingCommands) { if (isNaN(p)) continue; let command = matchingCommands[p]; const name = command.commandKey; let nameHtml = name; let startIndex = name.toUpperCase().indexOf(input.toUpperCase()); if (input && startIndex > -1) { nameHtml = name.substr(0, startIndex) + "" + name.substr(startIndex, input.length) + "" + name.substr(startIndex + input.length); } listOfPlayers += `
  • ${nameHtml} - ${command.description}
  • `; } $whisperMenu.find("ol").html(listOfPlayers); $("#command-menu").html($whisperMenu); // set up click-outside listener to close menu $(window).on("click.outsidewhispermenu", (e) => { var $target = $(e.target); if (!$target.closest("#command-menu").length) { closeWhisperMenu(); } }); } else { closeWhisperMenu(); } } else { closeWhisperMenu(); } } function _getCoreCommands() { let commands = []; let chatCommands = window.game.chatCommands; commands.push(chatCommands.createCommandFromData({ commandKey: "/ic", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Speak in character" })); commands.push(chatCommands.createCommandFromData({ commandKey: "/ooc", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Speak out of character" })); commands.push(chatCommands.createCommandFromData({ commandKey: "/emote", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Emote in character" })); commands.push(chatCommands.createCommandFromData({ commandKey: "/whisper", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Send a whisper to another player" })); commands.push(chatCommands.createCommandFromData({ commandKey: "/w", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Send a whisper to another player" })); commands.push(chatCommands.createCommandFromData({ commandKey: "/roll", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Roll dice" })); commands.push(chatCommands.createCommandFromData({ commandKey: "/gmroll", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Roll dice that only the GM can see" })); commands.push(chatCommands.createCommandFromData({ commandKey: "/blindroll", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Roll dice that are hidden" })); commands.push(chatCommands.createCommandFromData({ commandKey: "/selfroll", invokeOnCommand: (chatlog, messageText, chatdata) => { }, shouldDisplayToChat: false, iconClass: "fa-dice-d20", description: "Roll dice that only you can see" })); return commands; } function listFinishHandler(e) { if (e.which == 221) { // `]` let val = $("#chat-message").val(); if (val.match(listOfNamesRegex)) { if (typeof e === "object") e.preventDefault(); val = val.trim(); const newval = val.substring(val.length - 1) === "," ? val.substring(0, val.length - 1) : val; // remove `,` from the end $("#chat-message").val(newval + "] "); closeWhisperMenu(); } } } function jumpToMenuHandler(e) { if ($("#command-menu").find("li").length) { if (e.which === 38) { // `up` $("#command-menu li:last-child").focus(); return false; } else if (e.which === 40) { // `down` $("#command-menu li:first-child").focus(); return false; } } if (game.modules.get("autocomplete-whisper") != undefined) { if ($("#whisper-menu").find("li").length) { if (e.which === 38) { // `up` $("#whisper-menu li:last-child").focus(); return false; } else if (e.which === 40) { // `down` $("#whisper-menu li:first-child").focus(); return false; } } } // if player menu is not visible/DNE, execute FVTT's original keydown handler ui.chat._onChatKeyDown(e); } function menuNavigationHandler(e) { if ($(e.target).is("li.context-item")) { if (e.which === 38) { // `up` if ($(e.target).is(":first-child")) { $("#chat-message").focus(); } else { $(e.target).prev().focus(); } return false; } else if (e.which === 40) { // `down` if ($(e.target).is(":last-child")) { $("#chat-message").focus(); } else { $(e.target).next().focus(); } return false; } else if (e.which === 13) { // `enter` menuItemSelectionHandler(e); return false; } } } function menuItemSelectionHandler(e) { e.stopPropagation(); var autocompleteText = autocomplete($(e.target).text()); $("#chat-message").val(autocompleteText.ghost); $("#chat-message").focus(); closeWhisperMenu(); if ($("#chat-message").val().indexOf("[") > -1) { handleChatInput(); } } function ghostText(input, matches) { // show ghost text to autocomplete if there's a match starting with the characters already typed let filteredMatches = matches.filter((target) => { const p = target.commandKey.toUpperCase(); const i = input.toUpperCase(); return p.indexOf(i) === 0 && p !== i; }); if (filteredMatches.length === 1) { var autocompleteText = autocomplete(filteredMatches[0].commandKey); $(".chatghosttextarea").val(autocompleteText.ghost); $(".chatghosttextarea").addClass("show"); $("#chat-message").on("keydown.ghosttab", (e) => { if (e.which == 9) { // tab e.preventDefault(); $("#chat-message").val(autocompleteText.ghost); resetGhostText(); $("#chat-message").focus(); closeWhisperMenu(); if ($("#chat-message").val().indexOf("[") > -1) { handleChatInput(); } } }); } else { resetGhostText(); } } function autocomplete(match) { if (!match) return; const typedCharacters = $("#chat-message").val(); let nameToAdd = ''; if (match.toUpperCase().indexOf(typedCharacters.toUpperCase()) === 0) { var restOfTheName = match.substr(typedCharacters.length); nameToAdd = typedCharacters + restOfTheName; } else { nameToAdd = match; } if (nameToAdd.includes(" - ")) { nameToAdd = nameToAdd.substr(0, nameToAdd.indexOf(" - ")); } const ghostString = nameToAdd; return ({ ghost: ghostString }); } function closeWhisperMenu() { $("#command-menu").empty(); $(window).off("click.outsidewhispermenu"); resetGhostText(); } function resetGhostText() { $("#chat-message").off("keydown.ghosttab"); $(".chatghosttextarea").val("").removeClass("show"); } } }