forked from rDrama/rDrama
1
0
Fork 0

Improve inline emoji modal (with bugfixes) (#162)

- Fix/modernize keyboard navigation (making selections work and autoscroll)
- Allow capital letters to be used in the inline emoji input
- Fix first emoji typed not causing the picker to appear

\*Fixed bug that broke emoji pickers

Reviewed-on: rDrama/rDrama#162
Co-authored-by: KindaCrayCray <kindacraycray@noreply.fsdfsd.net>
Co-committed-by: KindaCrayCray <kindacraycray@noreply.fsdfsd.net>
master
KindaCrayCray 2023-07-01 00:04:23 +00:00 committed by Aevann
parent f0cfa8c916
commit fedb366b6b
1 changed files with 118 additions and 112 deletions

View File

@ -15,7 +15,13 @@ Copyright (C) 2022 Dr Steven Transmisia, anti-evil engineer,
*/ */
// Status // Status
let emojiEngineStarted = false; /**
* inactive - user has not tried using an emoji
* loading - user has tried to use an emoji, and the engine is initializing itself
* ready - engine can handle all emoji usage
* @type {"inactive"|"loading"|"ready"}
*/
let emojiEngineState = "inactive";
// DOM stuff // DOM stuff
const classesSelectorDOM = document.getElementById("emoji-modal-tabs"); const classesSelectorDOM = document.getElementById("emoji-modal-tabs");
@ -147,88 +153,93 @@ const emojisSearchDictionary = {
}; };
// get public emojis list // get public emojis list
const emojiRequest = new XMLHttpRequest(); function fetchEmojis() {
emojiRequest.open("GET", '/emojis'); const headers = new Headers({xhr: "xhr"})
emojiRequest.setRequestHeader('xhr', 'xhr'); return fetch("/emojis", {
emojiRequest.onload = async () => { headers,
let emojis = JSON.parse(emojiRequest.response); })
if(! (emojis instanceof Array )) .then(res => res.json())
throw new TypeError("[EMOJI DIALOG] rDrama's server should have sent a JSON-coded Array!"); .then(emojis => {
if(! (emojis instanceof Array ))
throw new TypeError("[EMOJI DIALOG] rDrama's server should have sent a JSON-coded Array!");
globalEmojis = emojis.map(({name, author, count}) => ({name, author, count})); globalEmojis = emojis.map(({name, author, count}) => ({name, author, count}));
let classes = ["Marsey", "Platy", "Wolf", "Donkey Kong", "Tay", "Capy", "Carp", "Marsey Flags", "Marsey Alphabet", "Classic", "Rage", "Wojak", "Misc"] let classes = ["Marsey", "Platy", "Wolf", "Donkey Kong", "Tay", "Capy", "Carp", "Marsey Flags", "Marsey Alphabet", "Classic", "Rage", "Wojak", "Misc"]
const bussyDOM = document.createElement("div"); const bussyDOM = document.createElement("div");
for(let i = 0; i < emojis.length; i++) for(let i = 0; i < emojis.length; i++)
{ {
const emoji = emojis[i]; const emoji = emojis[i];
emojisSearchDictionary.updateTag(emoji.name, emoji.name); emojisSearchDictionary.updateTag(emoji.name, emoji.name);
if(emoji.author !== undefined && emoji.author !== null) if(emoji.author !== undefined && emoji.author !== null)
{ {
emojisSearchDictionary.updateTag(`@${emoji.author.toLowerCase()}`, emoji.name); emojisSearchDictionary.updateTag(`@${emoji.author.toLowerCase()}`, emoji.name);
} }
if(emoji.tags instanceof Array) if(emoji.tags instanceof Array)
for(let i = 0; i < emoji.tags.length; i++) for(let i = 0; i < emoji.tags.length; i++)
emojisSearchDictionary.updateTag(emoji.tags[i], emoji.name); emojisSearchDictionary.updateTag(emoji.tags[i], emoji.name);
// Create emoji DOM // Create emoji DOM
const emojiDOM = document.importNode(emojiButtonTemplateDOM.content, true).children[0]; const emojiDOM = document.importNode(emojiButtonTemplateDOM.content, true).children[0];
emojiDOM.title = emoji.name emojiDOM.title = emoji.name
if(emoji.author !== undefined && emoji.author !== null) if(emoji.author !== undefined && emoji.author !== null)
emojiDOM.title += "\nauthor\t" + emoji.author emojiDOM.title += "\nauthor\t" + emoji.author
if(emoji.count !== undefined) if(emoji.count !== undefined)
emojiDOM.title += "\nused\t" + emoji.count; emojiDOM.title += "\nused\t" + emoji.count;
emojiDOM.dataset.className = emoji.kind; emojiDOM.dataset.className = emoji.kind;
emojiDOM.dataset.emojiName = emoji.name; emojiDOM.dataset.emojiName = emoji.name;
emojiDOM.onclick = emojiAddToInput; emojiDOM.onclick = emojiAddToInput;
emojiDOM.hidden = true; emojiDOM.hidden = true;
const emojiIMGDOM = emojiDOM.children[0]; const emojiIMGDOM = emojiDOM.children[0];
emojiIMGDOM.src = "/e/" + emoji.name + ".webp"; emojiIMGDOM.src = "/e/" + emoji.name + ".webp";
emojiIMGDOM.alt = emoji.name; emojiIMGDOM.alt = emoji.name;
/** Disableing lazy loading seems to reduce cpu usage somehow (?) /** Disableing lazy loading seems to reduce cpu usage somehow (?)
* idk it is difficult to benchmark */ * idk it is difficult to benchmark */
emojiIMGDOM.loading = "lazy"; emojiIMGDOM.loading = "lazy";
// Save reference // Save reference
emojiDOMs[emoji.name] = emojiDOM; emojiDOMs[emoji.name] = emojiDOM;
// Add to the document! // Add to the document!
bussyDOM.appendChild(emojiDOM); bussyDOM.appendChild(emojiDOM);
} }
// Create header // Create header
for(let className of classes) for(let className of classes)
{ {
let classSelectorDOM = document.createElement("li"); let classSelectorDOM = document.createElement("li");
classSelectorDOM.classList.add("nav-item"); classSelectorDOM.classList.add("nav-item");
let classSelectorLinkDOM = document.createElement("a"); let classSelectorLinkDOM = document.createElement("a");
classSelectorLinkDOM.href = "#"; classSelectorLinkDOM.href = "#";
classSelectorLinkDOM.classList.add("nav-link", "emojitab"); classSelectorLinkDOM.classList.add("nav-link", "emojitab");
classSelectorLinkDOM.dataset.bsToggle = "tab"; classSelectorLinkDOM.dataset.bsToggle = "tab";
classSelectorLinkDOM.dataset.className = className; classSelectorLinkDOM.dataset.className = className;
classSelectorLinkDOM.innerText = className; classSelectorLinkDOM.innerText = className;
classSelectorLinkDOM.addEventListener('click', switchEmojiTab); classSelectorLinkDOM.addEventListener('click', switchEmojiTab);
classSelectorDOM.appendChild(classSelectorLinkDOM); classSelectorDOM.appendChild(classSelectorLinkDOM);
classesSelectorDOM.appendChild(classSelectorDOM); classesSelectorDOM.appendChild(classSelectorDOM);
} }
// Show favorite for start. // Show favorite for start.
await classesSelectorDOM.children[0].children[0].click(); classesSelectorDOM.children[0].children[0].click();
// Send it to the render machine! // Send it to the render machine!
emojiResultsDOM.appendChild(bussyDOM); emojiResultsDOM.appendChild(bussyDOM);
emojiResultsDOM.hidden = false; emojiResultsDOM.hidden = false;
emojiWorkingDOM.hidden = true; emojiWorkingDOM.hidden = true;
emojiSearchBarDOM.disabled = false; emojiSearchBarDOM.disabled = false;
emojiEngineState = "ready";
})
} }
/** /**
@ -398,7 +409,7 @@ function populate_speed_emoji_modal(results, textbox)
emoji_option.addEventListener('click', () => { emoji_option.addEventListener('click', () => {
selecting = false; selecting = false;
speed_carot_modal.style.display = "none"; speed_carot_modal.style.display = "none";
textbox.value = textbox.value.replace(new RegExp(current_word+"(?=\\s|$)", "g"), `:${name}: `) textbox.value = textbox.value.replace(new RegExp(current_word+"(?=\\s|$)", "gi"), `:${name}: `)
textbox.focus() textbox.focus()
if (document.location.pathname != '/chat'){ if (document.location.pathname != '/chat'){
markdown(textbox) markdown(textbox)
@ -436,67 +447,56 @@ function update_speed_emoji_modal(event)
// Get current word at string, such as ":marse" or "word" // Get current word at string, such as ":marse" or "word"
let coords = text.indexOf(' ',box_coords.pos); let coords = text.indexOf(' ',box_coords.pos);
current_word = /:[!#a-zA-Z0-9_]+(?=\n|$)/.exec(text.slice(0, coords === -1 ? text.length : coords)); current_word = /:[!#a-zA-Z0-9_]+(?=\n|$)/.exec(text.slice(0, coords === -1 ? text.length : coords));
if (current_word) current_word = current_word.toString(); if (current_word) current_word = current_word[0].toLowerCase();
/* We could also check emoji_typing_state here, which is less accurate but more efficient. I've /* We could also check emoji_typing_state here, which is less accurate but more efficient. I've
* kept it unless someone wants to provide an option to toggle it for performance */ * kept it unless someone wants to provide an option to toggle it for performance */
if (curr_word_is_emoji() && current_word != ":") if (curr_word_is_emoji() && current_word != ":")
{ {
loadEmojis(null); loadEmojis().then( () => {
let modal_pos = event.target.getBoundingClientRect(); let modal_pos = event.target.getBoundingClientRect();
modal_pos.x += window.scrollX; modal_pos.x += window.scrollX;
modal_pos.y += window.scrollY; modal_pos.y += window.scrollY;
speed_carot_modal.style.display = "initial"; speed_carot_modal.style.display = "initial";
speed_carot_modal.style.left = box_coords.x - 35 + "px"; speed_carot_modal.style.left = box_coords.x - 35 + "px";
speed_carot_modal.style.top = modal_pos.y + box_coords.y + 14 + "px"; speed_carot_modal.style.top = modal_pos.y + box_coords.y + 14 + "px";
// Do the search (and do something with it) // Do the search (and do something with it)
const resultSet = emojisSearchDictionary.completeSearch(current_word.substr(1).replace(/#/g, "").replace(/!/g, "")) const resultSet = emojisSearchDictionary.completeSearch(current_word.substring(1).replace(/[#!]/g, ""));
const found = globalEmojis.filter(i => resultSet.has(i.name)); const found = globalEmojis.filter(i => resultSet.has(i.name));
populate_speed_emoji_modal(found, event.target);
populate_speed_emoji_modal(found, event.target);
});
} }
else { else {
speed_carot_modal.style.display = "none"; speed_carot_modal.style.display = "none";
} }
} }
function speed_carot_navigate(e) function speed_carot_navigate(event)
{ {
if (!selecting) return; if (!selecting) return;
let select_items = speed_carot_modal.querySelectorAll(".speed-modal-option"); let select_items = speed_carot_modal.querySelectorAll(".speed-modal-option");
if (!select_items || !curr_word_is_emoji()) return; if (!select_items || !curr_word_is_emoji()) return;
// Up or down arrow or enter
if (e.keyCode == 38 || e.keyCode == 40 || e.keyCode == 13) const modal_keybinds = {
// go up one, wrapping around to the bottom if pressed at the top
ArrowUp: () => emoji_index = ((emoji_index - 1) + select_items.length) % select_items.length,
// go down one, wrapping around to the top if pressed at the bottom
ArrowDown: () => emoji_index = ((emoji_index + 1) + select_items.length) % select_items.length,
// select the emoji
Enter: () => select_items[emoji_index].click(),
}
if (event.key in modal_keybinds)
{ {
if (emoji_index > select_items.length)
emoji_index = select_items;
select_items[emoji_index].classList.remove("selected"); select_items[emoji_index].classList.remove("selected");
switch (e.keyCode) modal_keybinds[event.key]();
{
case 38: // Up arrow
if (emoji_index)
emoji_index--;
break;
case 40: // Down arrow
if (emoji_index < select_items.length-1) emoji_index++;
break;
case 13:
select_items[emoji_index].click();
default:
break;
}
select_items[emoji_index].classList.add("selected"); select_items[emoji_index].classList.add("selected");
e.preventDefault(); select_items[emoji_index].scrollIntoView({inline: "end", block: "nearest"});
event.preventDefault();
} }
} }
@ -516,14 +516,20 @@ function loadEmojis(inputTargetIDName)
selecting = false; selecting = false;
speed_carot_modal.style.display = "none"; speed_carot_modal.style.display = "none";
if(!emojiEngineStarted) if (inputTargetIDName) emojiInputTargetDOM = document.getElementById(inputTargetIDName);
{
emojiEngineStarted = true;
emojiRequest.send();
}
if (inputTargetIDName) switch (emojiEngineState) {
emojiInputTargetDOM = document.getElementById(inputTargetIDName); case "inactive":
emojiEngineState = "loading"
return fetchEmojis();
case "loading":
// this works because once the fetch completes, the first keystroke callback will fire and use the current value
return Promise.reject();
case "ready":
return Promise.resolve();
default:
throw Error("Unknown emoji engine state");
}
} }
document.getElementById('emojiModal').addEventListener('shown.bs.modal', function () { document.getElementById('emojiModal').addEventListener('shown.bs.modal', function () {