// 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");
}
}
}