faster emoji search (kinda)

votekiss
easrng 2022-02-18 13:39:53 -05:00
parent d8d22ed99e
commit a6a856c6a5
2 changed files with 89 additions and 51 deletions

View File

@ -3,7 +3,7 @@ import {
setVideoTime,
setPlaying,
} from "./watch-session.mjs?v=19ef791";
import { emojify, emojis } from "./emojis.mjs?v=19ef791";
import { emojify, findEmojis } from "./emojis.mjs?v=19ef791";
function setCaretPosition(elem, caretPos) {
if (elem.createTextRange) {
@ -26,14 +26,16 @@ const setupChatboxEvents = (socket) => {
const emojiAutocomplete = chatForm.querySelector("#emoji-autocomplete");
oldChatForm.replaceWith(chatForm);
let autocompleting = false;
let autocompleting = false,
showListTimer;
const replaceMessage = (message) => () => {
messageInput.value = message;
autocomplete();
};
async function autocomplete() {
async function autocomplete(fromListTimeout) {
if (autocompleting) return;
clearInterval(showListTimer);
emojiAutocomplete.textContent = "";
autocompleting = true;
let text = messageInput.value.slice(0, messageInput.selectionStart);
@ -41,56 +43,59 @@ const setupChatboxEvents = (socket) => {
if (!match || match[1]) return (autocompleting = false); // We don't need to autocomplete.
const prefix = text.slice(0, match.index);
const search = text.slice(match.index + 1);
if (search.length < 1 && !fromListTimeout) {
autocompleting = false;
showListTimer = setTimeout(() => autocomplete(true), 1000);
return;
}
const suffix = messageInput.value.slice(messageInput.selectionStart);
let selected;
const select = (button) => {
const selected = document.querySelector(".emoji-option.selected");
if (selected) selected.classList.remove("selected");
selected = button;
button.classList.add("selected");
};
emojiAutocomplete.append(
...(await emojis)
.filter(([name]) => name.toLowerCase().startsWith(search.toLowerCase()))
.map(([name, replaceWith, ext], i) => {
const button = Object.assign(document.createElement("button"), {
className: "emoji-option" + (i === 0 ? " selected" : ""),
onmousedown: (e) => e.preventDefault(),
onclick: () => {
messageInput.value = prefix + replaceWith + " " + suffix;
setCaretPosition(
messageInput,
(prefix + " " + replaceWith).length
);
},
onmouseover: () => select(button),
onfocus: () => select(button),
type: "button",
title: name,
});
button.append(
replaceWith[0] !== ":"
? Object.assign(document.createElement("span"), {
textContent: replaceWith,
className: "emoji",
})
: Object.assign(new Image(), {
loading: "lazy",
src: `/emojis/${name}${ext}`,
className: "emoji",
}),
Object.assign(document.createElement("span"), {
textContent: name,
className: "emoji-name",
})
);
return button;
})
...(await findEmojis(search)).map(([name, replaceWith, ext], i) => {
const button = Object.assign(document.createElement("button"), {
className: "emoji-option",
onmousedown: (e) => e.preventDefault(),
onclick: () => {
messageInput.value = prefix + replaceWith + " " + suffix;
setCaretPosition(messageInput, (prefix + " " + replaceWith).length);
},
onmouseover: () => select(button),
onfocus: () => select(button),
type: "button",
title: name,
});
button.append(
replaceWith[0] !== ":"
? Object.assign(document.createElement("span"), {
textContent: replaceWith,
className: "emoji",
})
: Object.assign(new Image(), {
loading: "lazy",
src: `/emojis/${name}${ext}`,
className: "emoji",
}),
Object.assign(document.createElement("span"), {
textContent: name,
className: "emoji-name",
})
);
return button;
})
);
if (emojiAutocomplete.children[0])
if (emojiAutocomplete.children[0]) {
emojiAutocomplete.children[0].scrollIntoView();
select(emojiAutocomplete.children[0]);
}
autocompleting = false;
}
messageInput.addEventListener("input", autocomplete);
messageInput.addEventListener("selectionchange", autocomplete);
messageInput.addEventListener("input", () => autocomplete());
messageInput.addEventListener("selectionchange", () => autocomplete());
messageInput.addEventListener("keydown", (event) => {
if (event.key == "ArrowUp" || event.key == "ArrowDown") {
let selected = document.querySelector(".emoji-option.selected");

View File

@ -1,11 +1,11 @@
export async function emojify(text) {
const emojiList = await emojis;
await emojisLoaded;
let last = 0;
let nodes = [];
text.replace(/:([^\s:]+):/g, (match, name, index) => {
if (last <= index)
nodes.push(document.createTextNode(text.slice(last, index)));
let emoji = emojiList.find((e) => e[0] == name);
let emoji = emojis[name.toLowerCase()[0]].find((e) => e[0] == name);
if (!emoji) {
nodes.push(document.createTextNode(match));
} else {
@ -26,11 +26,44 @@ export async function emojify(text) {
if (last < text.length) nodes.push(document.createTextNode(text.slice(last)));
return nodes;
}
export const emojis = Promise.all([
const emojis = {};
export const emojisLoaded = Promise.all([
fetch("/emojis")
.then((e) => e.json())
.then((e) =>
e.map((e) => [e.slice(0, -4), ":" + e.slice(0, -4) + ":", e.slice(-4)])
),
fetch("/emojis/unicode.json").then((e) => e.json()),
]).then((e) => e.flat(1));
.then((a) => {
for (let e of a) {
const name = e.slice(0, -4),
lower = name.toLowerCase();
emojis[lower[0]] = emojis[lower[0]] || [];
emojis[lower[0]].push([name, ":" + name + ":", e.slice(-4), lower]);
}
}),
fetch("/emojis/unicode.json")
.then((e) => e.json())
.then((a) => {
for (let e of a) {
emojis[e[0][0]] = emojis[e[0][0]] || [];
emojis[e[0][0]].push([e[0], e[1], null, e[0]]);
}
}),
]);
export async function findEmojis(search) {
await emojisLoaded;
let groups = [[], []];
if (search.length < 1) {
for (let letter in emojis)
for (let emoji of emojis[letter]) {
(emoji[1][0] === ":" ? groups[0] : groups[1]).push(emoji);
}
} else {
search = search.toLowerCase();
for (let emoji of emojis[search[0]]) {
if (search.length == 1 || emoji[3].startsWith(search)) {
(emoji[1][0] === ":" ? groups[0] : groups[1]).push(emoji);
}
}
}
return [...groups[0], ...groups[1]];
}