From 83a16bc090157accf2f05307260f202f593082e8 Mon Sep 17 00:00:00 2001 From: KindaCrayCray Date: Wed, 28 Jun 2023 16:59:44 +0200 Subject: [PATCH 1/5] Clean up emoji modal keyboard nav Changed arrow keys to wrap around instead. Need to add autoscroll for for the modal itself. --- files/assets/js/emoji_modal.js | 38 ++++++++++++---------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/files/assets/js/emoji_modal.js b/files/assets/js/emoji_modal.js index 2a017ff87..9a091e1c2 100644 --- a/files/assets/js/emoji_modal.js +++ b/files/assets/js/emoji_modal.js @@ -463,39 +463,27 @@ function update_speed_emoji_modal(event) } } -function speed_carot_navigate(e) +function speed_carot_navigate(event) { if (!selecting) return; let select_items = speed_carot_modal.querySelectorAll(".speed-modal-option"); 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"); - switch (e.keyCode) - { - 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; - } - + modal_keybinds[event.key](); select_items[emoji_index].classList.add("selected"); - e.preventDefault(); + event.preventDefault(); } } -- 2.34.1 From d1332f33871795ce3b27911a389c7ce7f318e896 Mon Sep 17 00:00:00 2001 From: KindaCrayCray Date: Wed, 28 Jun 2023 17:14:45 +0200 Subject: [PATCH 2/5] Add scrolling when selected emoji is not visible --- files/assets/js/emoji_modal.js | 1 + 1 file changed, 1 insertion(+) diff --git a/files/assets/js/emoji_modal.js b/files/assets/js/emoji_modal.js index 9a091e1c2..9b5c349c4 100644 --- a/files/assets/js/emoji_modal.js +++ b/files/assets/js/emoji_modal.js @@ -483,6 +483,7 @@ function speed_carot_navigate(event) select_items[emoji_index].classList.remove("selected"); modal_keybinds[event.key](); select_items[emoji_index].classList.add("selected"); + select_items[emoji_index].scrollIntoView({inline: "end", block: "nearest"}); event.preventDefault(); } } -- 2.34.1 From 50849c8279bacb209698c0846bcdc26339694c5b Mon Sep 17 00:00:00 2001 From: KindaCrayCray Date: Wed, 28 Jun 2023 17:31:13 +0200 Subject: [PATCH 3/5] Casefold capital characters in inline search --- files/assets/js/emoji_modal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/assets/js/emoji_modal.js b/files/assets/js/emoji_modal.js index 9b5c349c4..905b52e1a 100644 --- a/files/assets/js/emoji_modal.js +++ b/files/assets/js/emoji_modal.js @@ -435,7 +435,7 @@ function update_speed_emoji_modal(event) // Get current word at string, such as ":marse" or "word" let coords = text.indexOf(' ',box_coords.pos); 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 * kept it unless someone wants to provide an option to toggle it for performance */ @@ -451,7 +451,7 @@ function update_speed_emoji_modal(event) speed_carot_modal.style.top = modal_pos.y + box_coords.y + 14 + "px"; // 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)); -- 2.34.1 From f18716960a5bcca72374b14fb875d265a9061111 Mon Sep 17 00:00:00 2001 From: KindaCrayCray Date: Fri, 30 Jun 2023 03:30:47 +0200 Subject: [PATCH 4/5] Fix first use of inline emoji picker not appearing Previously, on the first use of the inline emoji modal, it would not display the emojis because the display code would not wait for the network request to resolve. Now, the modal will appear when the emojis are successfully loaded and then respond to the keystrokes. --- files/assets/js/emoji_modal.js | 187 ++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 86 deletions(-) diff --git a/files/assets/js/emoji_modal.js b/files/assets/js/emoji_modal.js index 905b52e1a..0ce417074 100644 --- a/files/assets/js/emoji_modal.js +++ b/files/assets/js/emoji_modal.js @@ -15,7 +15,13 @@ Copyright (C) 2022 Dr Steven Transmisia, anti-evil engineer, */ // 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 const classesSelectorDOM = document.getElementById("emoji-modal-tabs"); @@ -146,88 +152,93 @@ const emojisSearchDictionary = { }; // get public emojis list -const emojiRequest = new XMLHttpRequest(); -emojiRequest.open("GET", '/emojis'); -emojiRequest.setRequestHeader('xhr', 'xhr'); -emojiRequest.onload = async () => { - let emojis = JSON.parse(emojiRequest.response); - if(! (emojis instanceof Array )) - throw new TypeError("[EMOJI DIALOG] rDrama's server should have sent a JSON-coded Array!"); +function fetchEmojis() { + const headers = new Headers({xhr: "xhr"}) + return fetch("/emojis", { + headers, + }) + .then(res => res.json()) + .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++) - { - const emoji = emojis[i]; + for(let i = 0; i < emojis.length; i++) + { + const emoji = emojis[i]; - emojisSearchDictionary.updateTag(emoji.name, emoji.name); - if(emoji.author !== undefined && emoji.author !== null) - { - emojisSearchDictionary.updateTag(`@${emoji.author.toLowerCase()}`, emoji.name); - } + emojisSearchDictionary.updateTag(emoji.name, emoji.name); + if(emoji.author !== undefined && emoji.author !== null) + { + emojisSearchDictionary.updateTag(`@${emoji.author.toLowerCase()}`, emoji.name); + } - if(emoji.tags instanceof Array) - for(let i = 0; i < emoji.tags.length; i++) - emojisSearchDictionary.updateTag(emoji.tags[i], emoji.name); + if(emoji.tags instanceof Array) + for(let i = 0; i < emoji.tags.length; i++) + emojisSearchDictionary.updateTag(emoji.tags[i], emoji.name); - // Create emoji DOM - const emojiDOM = document.importNode(emojiButtonTemplateDOM.content, true).children[0]; + // Create emoji DOM + const emojiDOM = document.importNode(emojiButtonTemplateDOM.content, true).children[0]; - emojiDOM.title = emoji.name - if(emoji.author !== undefined && emoji.author !== null) - emojiDOM.title += "\nauthor\t" + emoji.author - if(emoji.count !== undefined) - emojiDOM.title += "\nused\t" + emoji.count; - emojiDOM.dataset.className = emoji.kind; - emojiDOM.dataset.emojiName = emoji.name; - emojiDOM.onclick = emojiAddToInput; - emojiDOM.hidden = true; + emojiDOM.title = emoji.name + if(emoji.author !== undefined && emoji.author !== null) + emojiDOM.title += "\nauthor\t" + emoji.author + if(emoji.count !== undefined) + emojiDOM.title += "\nused\t" + emoji.count; + emojiDOM.dataset.className = emoji.kind; + emojiDOM.dataset.emojiName = emoji.name; + emojiDOM.onclick = emojiAddToInput; + emojiDOM.hidden = true; - const emojiIMGDOM = emojiDOM.children[0]; - emojiIMGDOM.src = "/e/" + emoji.name + ".webp"; - emojiIMGDOM.alt = emoji.name; - /** Disableing lazy loading seems to reduce cpu usage somehow (?) - * idk it is difficult to benchmark */ - emojiIMGDOM.loading = "lazy"; + const emojiIMGDOM = emojiDOM.children[0]; + emojiIMGDOM.src = "/e/" + emoji.name + ".webp"; + emojiIMGDOM.alt = emoji.name; + /** Disableing lazy loading seems to reduce cpu usage somehow (?) + * idk it is difficult to benchmark */ + emojiIMGDOM.loading = "lazy"; - // Save reference - emojiDOMs[emoji.name] = emojiDOM; + // Save reference + emojiDOMs[emoji.name] = emojiDOM; - // Add to the document! - bussyDOM.appendChild(emojiDOM); - } + // Add to the document! + bussyDOM.appendChild(emojiDOM); + } - // Create header - for(let className of classes) - { - let classSelectorDOM = document.createElement("li"); - classSelectorDOM.classList.add("nav-item"); + // Create header + for(let className of classes) + { + let classSelectorDOM = document.createElement("li"); + classSelectorDOM.classList.add("nav-item"); - let classSelectorLinkDOM = document.createElement("a"); - classSelectorLinkDOM.href = "#"; - classSelectorLinkDOM.classList.add("nav-link", "emojitab"); - classSelectorLinkDOM.dataset.bsToggle = "tab"; - classSelectorLinkDOM.dataset.className = className; - classSelectorLinkDOM.innerText = className; - classSelectorLinkDOM.addEventListener('click', switchEmojiTab); + let classSelectorLinkDOM = document.createElement("a"); + classSelectorLinkDOM.href = "#"; + classSelectorLinkDOM.classList.add("nav-link", "emojitab"); + classSelectorLinkDOM.dataset.bsToggle = "tab"; + classSelectorLinkDOM.dataset.className = className; + classSelectorLinkDOM.innerText = className; + classSelectorLinkDOM.addEventListener('click', switchEmojiTab); - classSelectorDOM.appendChild(classSelectorLinkDOM); - classesSelectorDOM.appendChild(classSelectorDOM); - } + classSelectorDOM.appendChild(classSelectorLinkDOM); + classesSelectorDOM.appendChild(classSelectorDOM); + } - // Show favorite for start. - await classesSelectorDOM.children[0].children[0].click(); + // Show favorite for start. + classesSelectorDOM.children[0].children[0].click(); - // Send it to the render machine! - emojiResultsDOM.appendChild(bussyDOM); + // Send it to the render machine! + emojiResultsDOM.appendChild(bussyDOM); - emojiResultsDOM.hidden = false; - emojiWorkingDOM.hidden = true; - emojiSearchBarDOM.disabled = false; + emojiResultsDOM.hidden = false; + emojiWorkingDOM.hidden = true; + emojiSearchBarDOM.disabled = false; + + emojiEngineState = "ready"; + }) } /** @@ -441,22 +452,22 @@ function update_speed_emoji_modal(event) * kept it unless someone wants to provide an option to toggle it for performance */ if (curr_word_is_emoji() && current_word != ":") { - loadEmojis(null); - let modal_pos = event.target.getBoundingClientRect(); - modal_pos.x += window.scrollX; - modal_pos.y += window.scrollY; + loadEmojis().then( () => { + let modal_pos = event.target.getBoundingClientRect(); + modal_pos.x += window.scrollX; + modal_pos.y += window.scrollY; - speed_carot_modal.style.display = "initial"; - 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.display = "initial"; + speed_carot_modal.style.left = box_coords.x - 35 + "px"; + speed_carot_modal.style.top = modal_pos.y + box_coords.y + 14 + "px"; - // Do the search (and do something with it) - const resultSet = emojisSearchDictionary.completeSearch(current_word.substring(1).replace(/[#!]/g, "")); + // Do the search (and do something with it) + const resultSet = emojisSearchDictionary.completeSearch(current_word.substring(1).replace(/[#!]/g, "")); - const found = globalEmojis.filter(i => resultSet.has(i.name)); - - populate_speed_emoji_modal(found, event.target); + const found = globalEmojis.filter(i => resultSet.has(i.name)); + populate_speed_emoji_modal(found, event.target); + }); } else { speed_carot_modal.style.display = "none"; @@ -499,19 +510,23 @@ forms.forEach(i => { i.addEventListener('keydown', speed_carot_navigate, false); }); -function loadEmojis(inputTargetIDName) +function loadEmojis() { selecting = false; speed_carot_modal.style.display = "none"; - if(!emojiEngineStarted) - { - emojiEngineStarted = true; - emojiRequest.send(); + switch (emojiEngineState) { + case "inactive": + emojiEngineState = "loading" + return fetchEmojis(); + case "loading": + // this is a subpar solution because it means that globalEmojis won't be loaded for later keystrokes + // however, it doesn't matter because onInput only checks what the user is typing after everything is loaded + case "ready": + return Promise.resolve(); + default: + throw Error("Unknown emoji engine state"); } - - if (inputTargetIDName) - emojiInputTargetDOM = document.getElementById(inputTargetIDName); } document.getElementById('emojiModal').addEventListener('shown.bs.modal', function () { -- 2.34.1 From a11e24a9ba99b0792b89cefb536d3094a674582b Mon Sep 17 00:00:00 2001 From: KindaCrayCray Date: Fri, 30 Jun 2023 04:41:58 +0200 Subject: [PATCH 5/5] Ignore case in emoji replacement The capital letters from the user input could still be there since current_word is lowercase --- files/assets/js/emoji_modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/assets/js/emoji_modal.js b/files/assets/js/emoji_modal.js index 0ce417074..3961eaf3c 100644 --- a/files/assets/js/emoji_modal.js +++ b/files/assets/js/emoji_modal.js @@ -408,7 +408,7 @@ function populate_speed_emoji_modal(results, textbox) emoji_option.addEventListener('click', () => { selecting = false; 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() if (document.location.pathname != '/chat'){ markdown(textbox) -- 2.34.1