thelounge/client/js/autocompletion.js

330 lines
7 KiB
JavaScript
Raw Normal View History

"use strict";
const $ = require("jquery");
const fuzzy = require("fuzzy");
2017-12-05 20:05:53 +01:00
const Mousetrap = require("mousetrap");
const {Textcomplete, Textarea} = require("textcomplete");
const emojiMap = require("./libs/simplemap.json");
const constants = require("./constants");
2018-07-08 16:57:02 +02:00
const {vueApp} = require("./vue");
2018-07-08 16:57:02 +02:00
let input;
2017-12-05 18:44:40 +01:00
let textcomplete;
Offer optional syncing of client settings Write synced settings to localstorage. move settings and webpush init to init.js stub for server sending clientsettings get very basic setting sync working Also update client.config.clientSettings on settings:set Full setting sync with mandatory and excluded sync options Actually check client preferences. Further settings restructuring. Refactor options.js make storage act in a sane manner. Add new parameter to applySetting Do not sync if the setting is stored as a result of syncing General clean up, commenting and restructing. sync from server on checking "sync" offer initial sync Better deal with DOM being ready and instances of inital sync showing Don't try to disable autocompletion when not enabled. Restructure option.js to seperate functions from settings. More consistency in naming options vs settings Switch processSetting and applySetting names reflecting their functionality better. move options init back to configuration. simplify how settings are synced around. move options init after template building. Remove unneeded hasOwnProperty Use global for #theme and only apply theme in applySetting Return when no server side clientsettings excist. Autocompletion options to options.settings Make nocss param in url work again. Actually filter out empty highlight values. Clarify alwaysSync comment. Remove manual step for initial sync change attr to prop in options.js replace unbind with off in autocompletion.js Do not sync settings when the lounge is set to public. fix eslint error Fix merge error Do not show sync warning after page refresh when sync is enabled Move setting sync label in actual label. Improve server setting sync handling performance and failure potential. Don't give impression that the desktop notificiation is off when the browser permission is denied. Refine showing and hiding of notification warnings. rename all setting socket events to singular setting. add experimental note and icon to settingsync. fix css linting error
2017-12-11 20:01:15 +01:00
let enabled = false;
2017-12-05 18:44:40 +01:00
module.exports = {
enable: enableAutocomplete,
disable() {
Offer optional syncing of client settings Write synced settings to localstorage. move settings and webpush init to init.js stub for server sending clientsettings get very basic setting sync working Also update client.config.clientSettings on settings:set Full setting sync with mandatory and excluded sync options Actually check client preferences. Further settings restructuring. Refactor options.js make storage act in a sane manner. Add new parameter to applySetting Do not sync if the setting is stored as a result of syncing General clean up, commenting and restructing. sync from server on checking "sync" offer initial sync Better deal with DOM being ready and instances of inital sync showing Don't try to disable autocompletion when not enabled. Restructure option.js to seperate functions from settings. More consistency in naming options vs settings Switch processSetting and applySetting names reflecting their functionality better. move options init back to configuration. simplify how settings are synced around. move options init after template building. Remove unneeded hasOwnProperty Use global for #theme and only apply theme in applySetting Return when no server side clientsettings excist. Autocompletion options to options.settings Make nocss param in url work again. Actually filter out empty highlight values. Clarify alwaysSync comment. Remove manual step for initial sync change attr to prop in options.js replace unbind with off in autocompletion.js Do not sync settings when the lounge is set to public. fix eslint error Fix merge error Do not show sync warning after page refresh when sync is enabled Move setting sync label in actual label. Improve server setting sync handling performance and failure potential. Don't give impression that the desktop notificiation is off when the browser permission is denied. Refine showing and hiding of notification warnings. rename all setting socket events to singular setting. add experimental note and icon to settingsync. fix css linting error
2017-12-11 20:01:15 +01:00
if (enabled) {
input.off("input.tabcomplete");
Mousetrap(input.get(0)).unbind("tab", "keydown");
textcomplete.destroy();
Offer optional syncing of client settings Write synced settings to localstorage. move settings and webpush init to init.js stub for server sending clientsettings get very basic setting sync working Also update client.config.clientSettings on settings:set Full setting sync with mandatory and excluded sync options Actually check client preferences. Further settings restructuring. Refactor options.js make storage act in a sane manner. Add new parameter to applySetting Do not sync if the setting is stored as a result of syncing General clean up, commenting and restructing. sync from server on checking "sync" offer initial sync Better deal with DOM being ready and instances of inital sync showing Don't try to disable autocompletion when not enabled. Restructure option.js to seperate functions from settings. More consistency in naming options vs settings Switch processSetting and applySetting names reflecting their functionality better. move options init back to configuration. simplify how settings are synced around. move options init after template building. Remove unneeded hasOwnProperty Use global for #theme and only apply theme in applySetting Return when no server side clientsettings excist. Autocompletion options to options.settings Make nocss param in url work again. Actually filter out empty highlight values. Clarify alwaysSync comment. Remove manual step for initial sync change attr to prop in options.js replace unbind with off in autocompletion.js Do not sync settings when the lounge is set to public. fix eslint error Fix merge error Do not show sync warning after page refresh when sync is enabled Move setting sync label in actual label. Improve server setting sync handling performance and failure potential. Don't give impression that the desktop notificiation is off when the browser permission is denied. Refine showing and hiding of notification warnings. rename all setting socket events to singular setting. add experimental note and icon to settingsync. fix css linting error
2017-12-11 20:01:15 +01:00
enabled = false;
}
2017-12-05 20:05:53 +01:00
},
2017-12-05 18:44:40 +01:00
};
$("#form").on("submit", () => {
if (enabled) {
textcomplete.hide();
}
});
const emojiSearchTerms = Object.keys(emojiMap);
const emojiStrategy = {
id: "emoji",
match: /\B:([-+\w:?]{2,}):?$/,
search(term, callback) {
// Trim colon from the matched term,
// as we are unable to get a clean string from match regex
term = term.replace(/:$/, ""),
callback(fuzzyGrep(term, emojiSearchTerms));
},
template([string, original]) {
return `<span class="emoji">${emojiMap[original]}</span> ${string}`;
},
replace([, original]) {
return emojiMap[original];
},
index: 1,
};
const nicksStrategy = {
id: "nicks",
match: /\B(@([a-zA-Z_[\]\\^{}|`@][a-zA-Z0-9_[\]\\^{}|`-]*)?)$/,
search(term, callback) {
term = term.slice(1);
if (term[0] === "@") {
callback(completeNicks(term.slice(1), true)
.map((val) => ["@" + val[0], "@" + val[1]]));
} else {
callback(completeNicks(term, true));
}
},
template([string]) {
return string;
},
replace([, original], position = 1) {
// If no postfix specified, return autocompleted nick as-is
2018-07-08 16:57:02 +02:00
if (!vueApp.settings.nickPostfix) {
return original;
}
// If there is whitespace in the input already, append space to nick
if (position > 0 && /\s/.test($("#input").val())) {
return original + " ";
}
// If nick is first in the input, append specified postfix
2018-07-08 16:57:02 +02:00
return original + vueApp.settings.nickPostfix;
},
index: 1,
};
const chanStrategy = {
id: "chans",
match: /\B((#|\+|&|![A-Z0-9]{5})([^\x00\x0A\x0D\x20\x2C\x3A]+(:[^\x00\x0A\x0D\x20\x2C\x3A]*)?)?)$/,
search(term, callback, match) {
callback(completeChans(match[0]));
},
template([string]) {
return string;
},
replace([, original]) {
return original;
},
index: 1,
};
const commandStrategy = {
id: "commands",
match: /^\/(\w*)$/,
search(term, callback) {
callback(completeCommands("/" + term));
},
template([string]) {
return string;
},
replace([, original]) {
return original;
},
index: 1,
};
const foregroundColorStrategy = {
id: "foreground-colors",
match: /\x03(\d{0,2}|[A-Za-z ]{0,10})$/,
search(term, callback) {
term = term.toLowerCase();
const matchingColorCodes = constants.colorCodeMap
.filter((i) => fuzzy.test(term, i[0]) || fuzzy.test(term, i[1]))
.map((i) => {
if (fuzzy.test(term, i[1])) {
return [i[0], fuzzy.match(term, i[1], {
pre: "<b>",
post: "</b>",
}).rendered];
}
return i;
});
callback(matchingColorCodes);
},
template(value) {
return `<span class="irc-fg${parseInt(value[0], 10)}">${value[1]}</span>`;
},
replace(value) {
return "\x03" + value[0];
},
index: 1,
};
const backgroundColorStrategy = {
id: "background-colors",
match: /\x03(\d{2}),(\d{0,2}|[A-Za-z ]{0,10})$/,
search(term, callback, match) {
term = term.toLowerCase();
const matchingColorCodes = constants.colorCodeMap
.filter((i) => fuzzy.test(term, i[0]) || fuzzy.test(term, i[1]))
.map((pair) => {
if (fuzzy.test(term, pair[1])) {
return [pair[0], fuzzy.match(term, pair[1], {
pre: "<b>",
post: "</b>",
}).rendered];
}
return pair;
})
.map((pair) => pair.concat(match[1])); // Needed to pass fg color to `template`...
callback(matchingColorCodes);
},
template(value) {
return `<span class="irc-fg${parseInt(value[2], 10)} irc-bg irc-bg${parseInt(value[0], 10)}">${value[1]}</span>`;
},
replace(value) {
return "\x03$1," + value[0];
},
index: 2,
};
2018-07-09 20:53:49 +02:00
function enableAutocomplete(inputRef) {
Offer optional syncing of client settings Write synced settings to localstorage. move settings and webpush init to init.js stub for server sending clientsettings get very basic setting sync working Also update client.config.clientSettings on settings:set Full setting sync with mandatory and excluded sync options Actually check client preferences. Further settings restructuring. Refactor options.js make storage act in a sane manner. Add new parameter to applySetting Do not sync if the setting is stored as a result of syncing General clean up, commenting and restructing. sync from server on checking "sync" offer initial sync Better deal with DOM being ready and instances of inital sync showing Don't try to disable autocompletion when not enabled. Restructure option.js to seperate functions from settings. More consistency in naming options vs settings Switch processSetting and applySetting names reflecting their functionality better. move options init back to configuration. simplify how settings are synced around. move options init after template building. Remove unneeded hasOwnProperty Use global for #theme and only apply theme in applySetting Return when no server side clientsettings excist. Autocompletion options to options.settings Make nocss param in url work again. Actually filter out empty highlight values. Clarify alwaysSync comment. Remove manual step for initial sync change attr to prop in options.js replace unbind with off in autocompletion.js Do not sync settings when the lounge is set to public. fix eslint error Fix merge error Do not show sync warning after page refresh when sync is enabled Move setting sync label in actual label. Improve server setting sync handling performance and failure potential. Don't give impression that the desktop notificiation is off when the browser permission is denied. Refine showing and hiding of notification warnings. rename all setting socket events to singular setting. add experimental note and icon to settingsync. fix css linting error
2017-12-11 20:01:15 +01:00
enabled = true;
let autocompleting = false;
2017-12-05 20:05:53 +01:00
let tabCount = 0;
let lastMatch = "";
2017-12-05 20:05:53 +01:00
let currentMatches = [];
2018-07-09 20:53:49 +02:00
input = $(inputRef);
2017-12-05 20:05:53 +01:00
input.on("input.tabcomplete", (e) => {
if (e.detail === "autocomplete") {
return;
}
2017-12-05 20:05:53 +01:00
tabCount = 0;
currentMatches = [];
lastMatch = "";
});
2017-12-05 20:05:53 +01:00
Mousetrap(input.get(0)).bind("tab", (e) => {
if (autocompleting) {
2017-12-05 20:05:53 +01:00
return;
}
e.preventDefault();
const text = input.val();
if (input.get(0).selectionStart !== text.length) {
return;
}
if (tabCount === 0) {
lastMatch = text.split(/\s/).pop();
2017-12-05 20:05:53 +01:00
if (lastMatch.length === 0) {
2017-12-05 20:05:53 +01:00
return;
}
currentMatches = completeNicks(lastMatch, false);
2017-12-05 20:05:53 +01:00
if (currentMatches.length === 0) {
return;
}
}
const position = input.get(0).selectionStart - lastMatch.length;
const newMatch = nicksStrategy.replace([0, currentMatches[tabCount % currentMatches.length]], position);
input.val(text.substr(0, position) + newMatch);
2017-12-05 20:05:53 +01:00
2018-07-11 11:29:49 +02:00
// Propagate change to Vue model
input.get(0).dispatchEvent(new CustomEvent("input", {
detail: "autocomplete",
}));
2018-07-11 11:29:49 +02:00
lastMatch = newMatch;
2017-12-05 20:05:53 +01:00
tabCount++;
}, "keydown");
const editor = new Textarea(input.get(0));
2017-12-05 18:44:40 +01:00
textcomplete = new Textcomplete(editor, {
dropdown: {
className: "textcomplete-menu",
placement: "top",
},
});
2017-12-05 18:44:40 +01:00
textcomplete.register([
emojiStrategy,
nicksStrategy,
chanStrategy,
commandStrategy,
foregroundColorStrategy,
backgroundColorStrategy,
]);
// Activate the first item by default
// https://github.com/yuku-t/textcomplete/issues/93
textcomplete.on("rendered", () => {
if (textcomplete.dropdown.items.length > 0) {
textcomplete.dropdown.items[0].activate();
}
});
textcomplete.on("show", () => {
autocompleting = true;
2017-12-05 18:44:40 +01:00
});
textcomplete.on("hidden", () => {
autocompleting = false;
2017-12-05 18:44:40 +01:00
});
}
function fuzzyGrep(term, array) {
const results = fuzzy.filter(
term,
array,
{
pre: "<b>",
post: "</b>",
}
);
return results.map((el) => [el.string, el.original]);
}
function rawNicks() {
2018-07-08 16:57:02 +02:00
if (vueApp.activeChannel.channel.users.length > 0) {
const users = vueApp.activeChannel.channel.users.slice();
2018-07-08 16:57:02 +02:00
return users.sort((a, b) => b.lastMessage - a.lastMessage).map((u) => u.nick);
}
2018-07-08 16:57:02 +02:00
const me = vueApp.activeChannel.network.nick;
const otherUser = vueApp.activeChannel.channel.name;
// If this is a query, add their name to autocomplete
2018-07-08 16:57:02 +02:00
if (me !== otherUser && vueApp.activeChannel.channel.type === "query") {
return [otherUser, me];
}
// Return our own name by default for anything that isn't a channel or query
return [me];
}
function completeNicks(word, isFuzzy) {
const users = rawNicks();
word = word.toLowerCase();
if (isFuzzy) {
return fuzzyGrep(word, users);
}
return $.grep(
users,
(w) => !w.toLowerCase().indexOf(word)
);
}
function completeCommands(word) {
const words = constants.commands.slice();
return fuzzyGrep(word, words);
}
function completeChans(word) {
const words = [];
2018-07-08 16:57:02 +02:00
for (const channel of vueApp.activeChannel.network.channels) {
if (channel.type === "channel") {
words.push(channel.name);
}
}
return fuzzyGrep(word, words);
}