4e67e55a7d
Fix #1459 https://github.com/thelounge/lounge/issues/1459 Add rawNicks() to more easily get the list of nicks for the current channel or chat without overly complicating its caller.
321 lines
6.8 KiB
JavaScript
321 lines
6.8 KiB
JavaScript
"use strict";
|
|
|
|
const $ = require("jquery");
|
|
const fuzzy = require("fuzzy");
|
|
const Mousetrap = require("mousetrap");
|
|
const {Textcomplete, Textarea} = require("textcomplete");
|
|
const emojiMap = require("./libs/simplemap.json");
|
|
const options = require("./options");
|
|
const constants = require("./constants");
|
|
|
|
const input = $("#input");
|
|
let textcomplete;
|
|
|
|
module.exports = {
|
|
enable: enableAutocomplete,
|
|
disable: () => {
|
|
input.off("input.tabcomplete");
|
|
Mousetrap(input.get(0)).off("tab", "keydown");
|
|
|
|
if (textcomplete) {
|
|
textcomplete.destroy();
|
|
textcomplete = null;
|
|
}
|
|
},
|
|
};
|
|
|
|
const chat = $("#chat");
|
|
const sidebar = $("#sidebar");
|
|
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
|
|
if (!options.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
|
|
return original + options.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,
|
|
};
|
|
|
|
function enableAutocomplete() {
|
|
let tabCount = 0;
|
|
let lastMatch = "";
|
|
let currentMatches = [];
|
|
|
|
input.on("input.tabcomplete", () => {
|
|
tabCount = 0;
|
|
currentMatches = [];
|
|
lastMatch = "";
|
|
});
|
|
|
|
Mousetrap(input.get(0)).bind("tab", (e) => {
|
|
if (input.data("autocompleting")) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
const text = input.val();
|
|
|
|
if (input.get(0).selectionStart !== text.length) {
|
|
return;
|
|
}
|
|
|
|
if (tabCount === 0) {
|
|
lastMatch = text.split(/\s/).pop();
|
|
|
|
if (lastMatch.length === 0) {
|
|
return;
|
|
}
|
|
|
|
currentMatches = completeNicks(lastMatch, false);
|
|
|
|
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);
|
|
|
|
lastMatch = newMatch;
|
|
tabCount++;
|
|
}, "keydown");
|
|
|
|
const editor = new Textarea(input.get(0));
|
|
textcomplete = new Textcomplete(editor, {
|
|
dropdown: {
|
|
className: "textcomplete-menu",
|
|
placement: "top",
|
|
},
|
|
});
|
|
|
|
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", () => {
|
|
input.data("autocompleting", true);
|
|
});
|
|
|
|
textcomplete.on("hidden", () => {
|
|
input.data("autocompleting", false);
|
|
});
|
|
}
|
|
|
|
function fuzzyGrep(term, array) {
|
|
const results = fuzzy.filter(
|
|
term,
|
|
array,
|
|
{
|
|
pre: "<b>",
|
|
post: "</b>",
|
|
}
|
|
);
|
|
return results.map((el) => [el.string, el.original]);
|
|
}
|
|
|
|
function rawNicks() {
|
|
const chan = chat.find(".active");
|
|
const users = chan.find(".users");
|
|
|
|
// If this channel has a list of nicks, just return it
|
|
if (users.length > 0) {
|
|
return users.data("nicks");
|
|
}
|
|
|
|
const me = $("#nick-value").text();
|
|
const otherUser = chan.attr("aria-label");
|
|
|
|
// If this is a query, add their name to autocomplete
|
|
if (me !== otherUser && chan.data("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 = [];
|
|
|
|
sidebar.find(".chan.active")
|
|
.parent()
|
|
.find(".chan")
|
|
.each(function() {
|
|
const self = $(this);
|
|
|
|
if (!self.hasClass("lobby")) {
|
|
words.push(self.attr("aria-label"));
|
|
}
|
|
});
|
|
|
|
return fuzzyGrep(word, words);
|
|
}
|