Emoji autofill

This commit is contained in:
2026-10-21 14:28:58 -04:00
parent 8708c9b086
commit 2a2d976013
4 changed files with 116 additions and 2 deletions
+107
View File
@@ -637,9 +637,116 @@ async function loadChat() {
}
}
const CHAT_EMOJIS = [
["smile", "😄"], ["smiley", "😃"], ["grin", "😁"], ["laughing", "😆"],
["joy", "😂"], ["rofl", "🤣"], ["wink", "😉"], ["blush", "😊"],
["heart_eyes", "😍"], ["thinking", "🤔"], ["sunglasses", "😎"], ["sweat_smile", "😅"],
["nervous", "😬"], ["eyes", "👀"], ["cry", "😢"], ["sob", "😭"],
["neutral", "😐"], ["surprised", "😮"], ["scream", "😱"], ["sleeping", "😴"],
["heart", "❤️"], ["green_heart", "💚"], ["blue_heart", "💙"], ["yellow_heart", "💛"],
["purple_heart", "💜"], ["black_heart", "🖤"], ["white_heart", "🤍"],
["thumbsup", "👍"], ["+1", "👍"], ["thumbsdown", "👎"], ["-1", "👎"],
["clap", "👏"], ["wave", "👋"], ["pray", "🙏"], ["muscle", "💪"],
["fire", "🔥"], ["tada", "🎉"], ["party", "🥳"], ["sparkles", "✨"],
["star", "⭐"], ["trophy", "🏆"], ["medal", "🏅"], ["rocket", "🚀"],
["hourglass", "⌛"], ["clock", "🕒"], ["alarm", "⏰"], ["stopwatch", "⏱️"],
["button", "🔘"], ["red_circle", "🔴"], ["blue_circle", "🔵"], ["green_circle", "🟢"],
["yellow_circle", "🟡"], ["white_circle", "⚪"], ["black_circle", "⚫"],
["100", "💯"], ["gg", "🤝"], ["skull", "💀"], ["coffee", "☕"],
];
let emojiMatches = [];
let emojiSelection = 0;
let emojiRange = null;
function closeEmojiSuggestions() {
emojiMatches = [];
emojiRange = null;
$("emoji-suggestions").hidden = true;
$("chat-input").setAttribute("aria-expanded", "false");
$("chat-input").removeAttribute("aria-activedescendant");
}
function renderEmojiSuggestions() {
const list = $("emoji-suggestions");
list.replaceChildren(...emojiMatches.map(([name, emoji], index) => {
const option = el("li", undefined, "emoji-option");
option.id = `emoji-option-${index}`;
option.dataset.index = String(index);
option.setAttribute("role", "option");
option.setAttribute("aria-selected", String(index === emojiSelection));
option.append(el("span", emoji, "emoji-glyph"), el("span", `:${name}:`));
return option;
}));
list.hidden = false;
$("chat-input").setAttribute("aria-expanded", "true");
$("chat-input").setAttribute("aria-activedescendant", `emoji-option-${emojiSelection}`);
}
function suggestEmoji() {
const input = $("chat-input");
if (input.selectionStart !== input.selectionEnd) return closeEmojiSuggestions();
const before = input.value.slice(0, input.selectionStart);
const match = before.match(/(?:^|\s):([a-z0-9_+\-]*):?$/i);
if (!match) return closeEmojiSuggestions();
const query = match[1].toLowerCase();
// A completed shortcode must match exactly; never turn an unknown code into another emoji.
const complete = before.endsWith(":") && match[0].trim() !== ":";
emojiMatches = CHAT_EMOJIS.filter(([name]) => complete ? name === query : name.startsWith(query)).slice(0, 6);
if (!emojiMatches.length) return closeEmojiSuggestions();
emojiSelection = 0;
emojiRange = { start: before.lastIndexOf(":", complete ? before.length - 2 : before.length - 1), end: input.selectionStart };
renderEmojiSuggestions();
}
function insertEmoji(index) {
if (!emojiRange || !emojiMatches[index]) return;
const input = $("chat-input");
const emoji = emojiMatches[index][1];
const { start, end } = emojiRange;
if (input.value.length - (end - start) + emoji.length > input.maxLength) {
$("chat-error").textContent = "This emoji would exceed the message length limit.";
closeEmojiSuggestions();
return;
}
input.setRangeText(emoji, start, end, "end");
$("chat-error").textContent = "";
closeEmojiSuggestions();
input.focus();
}
$("chat-input").addEventListener("input", (event) => {
if (!event.isComposing) suggestEmoji();
});
$("chat-input").addEventListener("compositionend", suggestEmoji);
$("chat-input").addEventListener("click", suggestEmoji);
$("chat-input").addEventListener("blur", closeEmojiSuggestions);
$("chat-input").addEventListener("keydown", (event) => {
if (event.isComposing || !emojiMatches.length) return;
if (["ArrowDown", "ArrowUp"].includes(event.key)) {
event.preventDefault();
emojiSelection = (emojiSelection + (event.key === "ArrowDown" ? 1 : -1) + emojiMatches.length) % emojiMatches.length;
renderEmojiSuggestions();
} else if (["Enter", "Tab"].includes(event.key)) {
event.preventDefault();
insertEmoji(emojiSelection);
} else if (event.key === "Escape") {
event.preventDefault();
closeEmojiSuggestions();
} else if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
closeEmojiSuggestions();
}
});
$("emoji-suggestions").addEventListener("pointerdown", (event) => {
const option = event.target.closest("[data-index]");
if (!option) return;
event.preventDefault(); // keep the caret and avoid blur before insertion
insertEmoji(Number(option.dataset.index));
});
$("chat-form").addEventListener("submit", async (event) => {
event.preventDefault();
const input = $("chat-input");
closeEmojiSuggestions();
$("chat-send").disabled = true;
$("chat-error").textContent = "";
try {