capy cant sneed

pull/215/head
transbitch 2023-10-24 23:07:43 -04:00
parent e5ea709a0e
commit 0256deda29
1 changed files with 343 additions and 343 deletions

View File

@ -26,419 +26,419 @@
*/ */
class EmojiEngine { class EmojiEngine {
_res; _res;
/** @type {Promise<void>} */ /** @type {Promise<void>} */
loaded = new Promise(res => this._res = res); loaded = new Promise(res => this._res = res);
hasLoaded = false; hasLoaded = false;
/** @type {EmojiTags} */ /** @type {EmojiTags} */
tags = {}; tags = {};
/** @type {EmojiKinds} */ /** @type {EmojiKinds} */
kinds = {}; kinds = {};
// Memoize this value so we don't have to recompute it. // Memoize this value so we don't have to recompute it.
_tag_entries; _tag_entries;
/** @type {{[index: string]: HTMLDivElement}} */ /** @type {{[index: string]: HTMLDivElement}} */
emojiDom = {}; emojiDom = {};
/** @type {{[index: string]: number}} */ /** @type {{[index: string]: number}} */
emojiNameCount = {}; emojiNameCount = {};
/** @type {(name: string) => void} */ /** @type {(name: string) => void} */
onInsert; onInsert;
init = async () => { init = async () => {
if (this.hasLoaded) { if (this.hasLoaded) {
return; return;
} }
await Promise.all([ await Promise.all([
this.loadTags(), this.loadTags(),
this.loadKinds(), this.loadKinds(),
]); ]);
this._tag_entries = Object.entries(this.tags); this._tag_entries = Object.entries(this.tags);
this._res(); this._res();
this.hasLoaded = true; this.hasLoaded = true;
} }
loadTags = async () => { loadTags = async () => {
this.tags = await (await fetch('/emoji_tags.json')).json(); this.tags = await (await fetch('/emoji_tags.json')).json();
} }
loadKinds = async () => { loadKinds = async () => {
this.kinds = await (await fetch('/emoji_kinds.json')).json(); this.kinds = await (await fetch('/emoji_kinds.json')).json();
} }
search = async (query, maxLength = Infinity) => { search = async (query, maxLength = Infinity) => {
await this.loaded; await this.loaded;
const resultsSet = new Set(); const resultsSet = new Set();
const results = []; const results = [];
for (const [tag, entries] of this._tag_entries) { for (const [tag, entries] of this._tag_entries) {
if (!tag.includes(query)) { if (!tag.includes(query)) {
continue; continue;
} }
for (const [name, count] of entries) { for (const [name, count] of entries) {
if (resultsSet.has(name)) { if (resultsSet.has(name)) {
continue; continue;
} else if (count < results[maxLength - 1]?.[1]) { } else if (count < results[maxLength - 1]?.[1]) {
// All the other emojis in this tag have less uses. We can stop here. // All the other emojis in this tag have less uses. We can stop here.
break; break;
} }
resultsSet.add(name); resultsSet.add(name);
// Insert into the array sorted. // Insert into the array sorted.
let i = results.length; let i = results.length;
while (i > 0 && count > results[i - 1][1]) { while (i > 0 && count > results[i - 1][1]) {
i--; i--;
} }
results.splice(i, 0, [name, count]); results.splice(i, 0, [name, count]);
if (results.length >= maxLength) { if (results.length >= maxLength) {
const [name] = results.pop(); const [name] = results.pop();
resultsSet.delete(name); resultsSet.delete(name);
} }
} }
} }
return results.map(([name]) => name); return results.map(([name]) => name);
} }
/** /**
* Get a dom element for a list of emojis in quick dropdown. * Get a dom element for a list of emojis in quick dropdown.
* @param {string[]} emojiNames * @param {string[]} emojiNames
*/ */
getQuickDoms = (emojiNames) => { getQuickDoms = (emojiNames) => {
return emojiNames.map(this.getQuickDom); return emojiNames.map(this.getQuickDom);
} }
/** /**
* *
* @param {*} emojiName * @param {*} emojiName
* @returns DOM element for an emoji quick dropdown. * @returns DOM element for an emoji quick dropdown.
*/ */
getQuickDom = (emojiName) => { getQuickDom = (emojiName) => {
if (this.emojiDom[emojiName]) { if (this.emojiDom[emojiName]) {
return this.emojiDom[emojiName]; return this.emojiDom[emojiName];
} }
const emojiEl = document.createElement('button'); const emojiEl = document.createElement('button');
emojiEl.classList.add('speed-modal-option', 'emoji-option'); emojiEl.classList.add('speed-modal-option', 'emoji-option');
emojiEl.addEventListener('click', (e) => { emojiEl.addEventListener('click', (e) => {
this.onInsert(emojiName); this.onInsert(emojiName);
}); });
const emojiImgEl = document.createElement('img'); const emojiImgEl = document.createElement('img');
emojiImgEl.classList.add('speed-modal-image', 'emoji-option-image'); emojiImgEl.classList.add('speed-modal-image', 'emoji-option-image');
emojiImgEl.src = emojiEngine.src(emojiName); emojiImgEl.src = emojiEngine.src(emojiName);
emojiEl.appendChild(emojiImgEl); emojiEl.appendChild(emojiImgEl);
const emojiNameEl = document.createElement('span'); const emojiNameEl = document.createElement('span');
emojiNameEl.textContent = emojiName; emojiNameEl.textContent = emojiName;
emojiEl.appendChild(emojiNameEl); emojiEl.appendChild(emojiNameEl);
this.emojiDom[emojiName] = emojiEl; this.emojiDom[emojiName] = emojiEl;
return emojiEl; return emojiEl;
} }
src = (name) => { src = (name) => {
return `${SITE_FULL_IMAGES}/e/${name}.webp` return `${SITE_FULL_IMAGES}/e/${name}.webp`
} }
} }
const emojiEngine = new EmojiEngine(); const emojiEngine = new EmojiEngine();
// Quick emoji dropdown & emoji insertion // Quick emoji dropdown & emoji insertion
{ {
const emojiDropdownEl = document.createElement('div'); const emojiDropdownEl = document.createElement('div');
emojiDropdownEl.classList.add('speed-carot-modal'); emojiDropdownEl.classList.add('speed-carot-modal');
/** @type {null | HTMLTextAreaElement} */ /** @type {null | HTMLTextAreaElement} */
let inputEl = null; let inputEl = null;
let visible = false; let visible = false;
let typingEmojiCanceled = false; let typingEmojiCanceled = false;
let firstDomEl = null; let firstDomEl = null;
let firstEmojiName = null; let firstEmojiName = null;
let caretPos = 0; let caretPos = 0;
// Used by onclick attrib of the smile button // Used by onclick attrib of the smile button
window.openEmojiModal = (id) => { window.openEmojiModal = (id) => {
inputEl = document.getElementById(id); inputEl = document.getElementById(id);
initEmojiModal(); initEmojiModal();
} }
emojiEngine.onInsert = (name) => { emojiEngine.onInsert = (name) => {
if (!inputEl) { if (!inputEl) {
return; return;
} }
const match = matchTypingEmoji(); const match = matchTypingEmoji();
if (match) { if (match) {
// We are inserting an emoji which we are typing. // We are inserting an emoji which we are typing.
inputEl.value = `${inputEl.value.slice(0, match.index)}:${name}:${inputEl.value.slice(match.index + name.length)} `; inputEl.value = `${inputEl.value.slice(0, match.index)}:${name}:${inputEl.value.slice(match.index + name.length)} `;
// Draw the focus back to this element. // Draw the focus back to this element.
inputEl.focus(); inputEl.focus();
} else { } else {
// We are inserting a new emoji. // We are inserting a new emoji.
const start = inputEl.value.slice(0, caretPos); const start = inputEl.value.slice(0, caretPos);
const end = inputEl.value.slice(caretPos); const end = inputEl.value.slice(caretPos);
const insert = `:${name}:${end.length === 0 ? ' ' : ''}`; const insert = `:${name}:${end.length === 0 ? ' ' : ''}`;
inputEl.value = `${start}${insert}${end}`; inputEl.value = `${start}${insert}${end}`;
caretPos += insert.length; caretPos += insert.length;
inputEl.setSelectionRange(caretPos, caretPos); inputEl.setSelectionRange(caretPos, caretPos);
} }
typingEmojiCanceled = false; typingEmojiCanceled = false;
update(); update();
// This updates the preview. // This updates the preview.
inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.dispatchEvent(new Event('input', { bubbles: true }));
// Update the favorite count. // Update the favorite count.
if (name in favoriteEmojis) { if (name in favoriteEmojis) {
favoriteEmojis[name]++; favoriteEmojis[name]++;
} else { } else {
favoriteEmojis[name] = 1; favoriteEmojis[name] = 1;
} }
localStorage.setItem("favorite_emojis", JSON.stringify(favoriteEmojis)); localStorage.setItem("favorite_emojis", JSON.stringify(favoriteEmojis));
} }
const inputCanTakeEmojis = (el = inputEl) => { const inputCanTakeEmojis = (el = inputEl) => {
return el?.dataset && 'emojis' in el.dataset; return el?.dataset && 'emojis' in el.dataset;
} }
const matchTypingEmoji = () => { const matchTypingEmoji = () => {
return inputEl?.value.substring(0, inputEl.selectionEnd).match(/:([\w!#]+)$/); return inputEl?.value.substring(0, inputEl.selectionEnd).match(/:([\w!#]+)$/);
} }
const getTypingEmoji = () => { const getTypingEmoji = () => {
return matchTypingEmoji()?.[1] ?? null; return matchTypingEmoji()?.[1] ?? null;
} }
const isTypingEmoji = () => { const isTypingEmoji = () => {
return inputCanTakeEmojis() && getTypingEmoji(); return inputCanTakeEmojis() && getTypingEmoji();
} }
const endTypingEmoji = () => { const endTypingEmoji = () => {
typingEmojiCanceled = false; typingEmojiCanceled = false;
} }
const update = async () => { const update = async () => {
const typing = isTypingEmoji(); const typing = isTypingEmoji();
visible = typing && !typingEmojiCanceled; visible = typing && !typingEmojiCanceled;
if (!visible) { if (!visible) {
emojiDropdownEl.parentElement?.removeChild(emojiDropdownEl); emojiDropdownEl.parentElement?.removeChild(emojiDropdownEl);
return; return;
} }
const oldFirst = firstDomEl; const oldFirst = firstDomEl;
document.body.appendChild(emojiDropdownEl); document.body.appendChild(emojiDropdownEl);
const search = await emojiEngine.search(getTypingEmoji(), 15); const search = await emojiEngine.search(getTypingEmoji(), 15);
firstEmojiName = search[0]; firstEmojiName = search[0];
const domEls = emojiEngine.getQuickDoms(search); const domEls = emojiEngine.getQuickDoms(search);
firstDomEl = domEls[0]; firstDomEl = domEls[0];
if (oldFirst !== firstDomEl) { if (oldFirst !== firstDomEl) {
oldFirst?.classList.remove('selected'); oldFirst?.classList.remove('selected');
firstDomEl.classList.add('selected'); firstDomEl.classList.add('selected');
} }
emojiDropdownEl.replaceChildren(...domEls); emojiDropdownEl.replaceChildren(...domEls);
const { left, bottom } = getCaretPos(inputEl); const { left, bottom } = getCaretPos(inputEl);
// Using transform instead of top/left is faster. // Using transform instead of top/left is faster.
emojiDropdownEl.style.transform = `translate(${left}px, ${bottom}px)`; emojiDropdownEl.style.transform = `translate(${left}px, ${bottom}px)`;
} }
// Add a listener when we start typing. // Add a listener when we start typing.
/** /**
* @param {FocusEvent} e * @param {FocusEvent} e
*/ */
const onKeyStart = (e) => { const onKeyStart = (e) => {
if (inputCanTakeEmojis(e.target)) { if (inputCanTakeEmojis(e.target)) {
inputEl = e.target; inputEl = e.target;
emojiEngine.init(); emojiEngine.init();
window.removeEventListener('keydown', onKeyStart); window.removeEventListener('keydown', onKeyStart);
} }
} }
window.addEventListener('keydown', onKeyStart); window.addEventListener('keydown', onKeyStart);
window.addEventListener('keydown', (e) => { window.addEventListener('keydown', (e) => {
if (!visible) { if (!visible) {
return; return;
} }
const isFocused = document.activeElement === inputEl const isFocused = document.activeElement === inputEl
if (e.key === 'Escape') { if (e.key === 'Escape') {
typingEmojiCanceled = true; typingEmojiCanceled = true;
update(); update();
} else if (e.key === 'Enter' && isFocused) { } else if (e.key === 'Enter' && isFocused) {
emojiEngine.onInsert(firstEmojiName); emojiEngine.onInsert(firstEmojiName);
e.preventDefault(); e.preventDefault();
} else if (e.key === 'Tab' && isFocused) { } else if (e.key === 'Tab' && isFocused) {
firstDomEl.focus(); firstDomEl.focus();
firstDomEl.classList.remove("selected"); firstDomEl.classList.remove("selected");
e.preventDefault(); e.preventDefault();
} }
}); });
['input', 'click', 'focus'].forEach((event) => { ['input', 'click', 'focus'].forEach((event) => {
window.addEventListener(event, (e) => { window.addEventListener(event, (e) => {
if (inputCanTakeEmojis(e.target)) { if (inputCanTakeEmojis(e.target)) {
inputEl = e.target; inputEl = e.target;
caretPos = inputEl.selectionEnd; caretPos = inputEl.selectionEnd;
} }
update(); update();
if (!isTypingEmoji()) { if (!isTypingEmoji()) {
endTypingEmoji(); endTypingEmoji();
} }
}); });
}); });
} }
/** @type {{ [name: string]: number }} */ /** @type {{ [name: string]: number }} */
const favoriteEmojis = JSON.parse(localStorage.getItem("favorite_emojis")) || {}; const favoriteEmojis = JSON.parse(localStorage.getItem("favorite_emojis")) || {};
const initEmojiModal = (() => { const initEmojiModal = (() => {
let hasInit = false; let hasInit = false;
return async () => { return async () => {
if (hasInit) { if (hasInit) {
return; return;
} }
hasInit = true; hasInit = true;
await emojiEngine.init(); await emojiEngine.init();
document.getElementById('emojis-work').style.display = 'none'; document.getElementById('emojis-work').style.display = 'none';
/** @type {{ [tabName: string]: HTMLDivElement }} */ /** @type {{ [tabName: string]: HTMLDivElement }} */
const tabContentEls = {} const tabContentEls = {}
/** @type {(kind: string, el: HTMLButtonElement) => void} */ /** @type {(kind: string, el: HTMLButtonElement) => void} */
const addTabClickListener = (kind, el) => { const addTabClickListener = (kind, el) => {
el.addEventListener('click', (e) => { el.addEventListener('click', (e) => {
setTab(kind); setTab(kind);
}); });
} }
const favorites = Object.entries(favoriteEmojis).sort((a, b) => b[1] - a[1]); const favorites = Object.entries(favoriteEmojis).sort((a, b) => b[1] - a[1]);
/** @type {{ [name: string]: HTMLButtonElement }} */ /** @type {{ [name: string]: HTMLButtonElement }} */
const favoriteClones = {}; const favoriteClones = {};
const favoriteContentEl = (() => { const favoriteContentEl = (() => {
const content = document.createElement('div'); const content = document.createElement('div');
tabContentEls['favorite'] = content; tabContentEls['favorite'] = content;
return content; return content;
})(); })();
let currentTab = 'favorite'; let currentTab = 'favorite';
const setTab = (kind) => { const setTab = (kind) => {
currentTab = kind; currentTab = kind;
tabContent.replaceChildren(tabContentEls[kind]); tabContent.replaceChildren(tabContentEls[kind]);
} }
const emojiModal = document.getElementById('emojiModal'); const emojiModal = document.getElementById('emojiModal');
const emojiTabsEl = document.getElementById('emoji-modal-tabs'); const emojiTabsEl = document.getElementById('emoji-modal-tabs');
const tabContent = document.getElementById('emoji-tab-content'); const tabContent = document.getElementById('emoji-tab-content');
/** @type {HTMLInputElement} */ /** @type {HTMLInputElement} */
const searchInputEl = document.getElementById('emoji_search'); const searchInputEl = document.getElementById('emoji_search');
searchInputEl.disabled = false; searchInputEl.disabled = false;
const searchResultsContainerEl = document.createElement('div'); const searchResultsContainerEl = document.createElement('div');
let isSearching = false; let isSearching = false;
/** @type {{ [index: string ]: HTMLButtonElement }} */ /** @type {{ [index: string ]: HTMLButtonElement }} */
const searchResultsEl = {}; const searchResultsEl = {};
const favoriteTabEl = document.getElementById('emoji-modal-tabs-favorite'); const favoriteTabEl = document.getElementById('emoji-modal-tabs-favorite');
addTabClickListener('favorite', favoriteTabEl); addTabClickListener('favorite', favoriteTabEl);
window.emojiSearch = async () => { window.emojiSearch = async () => {
if (searchInputEl.value.length === 0 && isSearching) { if (searchInputEl.value.length === 0 && isSearching) {
isSearching = false; isSearching = false;
setTab(currentTab); setTab(currentTab);
} else if (searchInputEl.value.length > 0 && !isSearching) { } else if (searchInputEl.value.length > 0 && !isSearching) {
isSearching = true; isSearching = true;
tabContent.replaceChildren(searchResultsContainerEl); tabContent.replaceChildren(searchResultsContainerEl);
} }
if (isSearching) { if (isSearching) {
const query = searchInputEl.value; const query = searchInputEl.value;
requestIdleCallback(() => { requestIdleCallback(() => {
emojiEngine.search(query).then((results) => { emojiEngine.search(query).then((results) => {
requestIdleCallback(() => { requestIdleCallback(() => {
searchResultsContainerEl.replaceChildren(...results.map((name) => searchResultsEl[name])); searchResultsContainerEl.replaceChildren(...results.map((name) => searchResultsEl[name]));
}, { timeout: 100 }); }, { timeout: 100 });
}); });
}, { timeout: 100 }); }, { timeout: 100 });
} }
} }
const promises = Object.entries(emojiEngine.kinds).map(([kind, emojis]) => new Promise((res) => { const promises = Object.entries(emojiEngine.kinds).map(([kind, emojis]) => new Promise((res) => {
const tabEl = (() => { const tabEl = (() => {
const tab = document.createElement('li'); const tab = document.createElement('li');
const button = document.createElement('button'); const button = document.createElement('button');
button.type = 'button'; button.type = 'button';
button.classList.add('nav-link', 'emojitab'); button.classList.add('nav-link', 'emojitab');
button.dataset.bsToggle = 'tab'; button.dataset.bsToggle = 'tab';
button.textContent = kind; button.textContent = kind;
tab.appendChild(button); tab.appendChild(button);
emojiTabsEl.appendChild(tab); emojiTabsEl.appendChild(tab);
addTabClickListener(kind, tab); addTabClickListener(kind, tab);
return tab; return tab;
})(); })();
const tabContentEl = (() => { const tabContentEl = (() => {
const tabContent = document.createElement('div'); const tabContent = document.createElement('div');
return tabContent; return tabContent;
})(); })();
tabContentEls[kind] = tabContentEl; tabContentEls[kind] = tabContentEl;
const tick = () => { const tick = () => {
for (const [name, count] of emojis) { for (const [name, count] of emojis) {
const buttonEl = document.createElement('button'); const buttonEl = document.createElement('button');
buttonEl.type = 'button'; buttonEl.type = 'button';
buttonEl.classList.add('btn', 'm-1', 'px-0', 'emoji2'); buttonEl.classList.add('btn', 'm-1', 'px-0', 'emoji2');
buttonEl.title = `${name} (${count})`; buttonEl.title = `${name} (${count})`;
const imgEl = document.createElement('img'); const imgEl = document.createElement('img');
imgEl.loading = 'lazy'; imgEl.loading = 'lazy';
imgEl.src = emojiEngine.src(name); imgEl.src = emojiEngine.src(name);
imgEl.alt = name; imgEl.alt = name;
buttonEl.appendChild(imgEl); buttonEl.appendChild(imgEl);
const searchClone = buttonEl.cloneNode(true); const searchClone = buttonEl.cloneNode(true);
const els = [buttonEl, searchClone]; const els = [buttonEl, searchClone];
if (name in favoriteEmojis) { if (name in favoriteEmojis) {
const favoriteClone = buttonEl.cloneNode(true); const favoriteClone = buttonEl.cloneNode(true);
favoriteClone.title = `${name} (${favoriteEmojis[name]})`; favoriteClone.title = `${name} (${favoriteEmojis[name]})`;
els.push(favoriteClone); els.push(favoriteClone);
favoriteClones[name] = favoriteClone; favoriteClones[name] = favoriteClone;
} }
els.forEach((el) => { els.forEach((el) => {
el.addEventListener('click', (e) => { el.addEventListener('click', (e) => {
emojiEngine.onInsert(name); emojiEngine.onInsert(name);
}); });
}); });
tabContentEl.appendChild(buttonEl); tabContentEl.appendChild(buttonEl);
searchResultsEl[name] = searchClone; searchResultsEl[name] = searchClone;
} }
res(); res();
} }
requestIdleCallback(tick, { timeout: 250 }); requestIdleCallback(tick, { timeout: 250 });
})); }));
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
for (const [name] of favorites) { for (const [name] of favorites) {
if (!(name in favoriteClones)) { if (!(name in favoriteClones)) {
continue; continue;
} }
favoriteContentEl.appendChild(favoriteClones[name]); favoriteContentEl.appendChild(favoriteClones[name]);
} }
}); });
setTab(currentTab); setTab(currentTab);
} }
})(); })();