diff --git a/files/assets/js/core.js b/files/assets/js/core.js
index 9b533b5c32..39f763a30e 100644
--- a/files/assets/js/core.js
+++ b/files/assets/js/core.js
@@ -693,6 +693,12 @@ if (screen_width < 768) {
}
}
+document.addEventListener('show.bs.modal', () => {
+ if (typeof close_inline_speed_emoji_modal === "function") {
+ close_inline_speed_emoji_modal();
+ }
+});
+
document.addEventListener('hide.bs.modal', () => {
if (typeof close_inline_speed_emoji_modal === "function") {
close_inline_speed_emoji_modal();
diff --git a/files/assets/js/emoji_modal.js b/files/assets/js/emoji_modal.js
deleted file mode 100644
index b6c1e4f832..0000000000
--- a/files/assets/js/emoji_modal.js
+++ /dev/null
@@ -1,620 +0,0 @@
-/*
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see .
-
-Copyright (C) 2022 Dr Steven Transmisia, anti-evil engineer,
- 2022 Nekobit, king autist
-*/
-
-// Status
-/**
- * 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";
-let emojiSpeedEngineState = "inactive";
-
-// DOM stuff
-const classesSelectorDOM = document.getElementById("emoji-modal-tabs");
-const emojiButtonTemplateDOM = document.getElementById("emoji-button-template");
-const emojiResultsDOM = document.getElementById("tab-content");
-
-const emojiSelectSuffixDOMs = document.getElementsByClassName("emoji-suffix");
-const emojiSelectPostfixDOMs= document.getElementsByClassName("emoji-postfix");
-
-const emojiNotFoundDOM = document.getElementById("no-emojis-found");
-const emojiWorkingDOM = document.getElementById("emojis-work");
-
-const emojiSearchBarDOM = document.getElementById('emoji_search');
-
-let emojiInputTargetDOM = undefined;
-
-// Emojis usage stats. I don't really like this format but I'll keep it for backward comp.
-const favorite_emojis = JSON.parse(localStorage.getItem("favorite_emojis")) || {};
-
-/** Associative array of all the emojis' DOM */
-let emojiDOMs = {};
-
-let globalEmojis;
-
-const EMOIJ_SEARCH_ENGINE_MIN_INTERVAL = 350;
-let emojiSearcher = {
- working: false,
- queries: [],
-
- addQuery: function(query)
- {
- this.queries.push(query);
- if (!this.working)
- this.work();
- },
-
- work: async function work() {
- this.working = true;
-
- while(this.queries.length > 0)
- {
- const startTime = Date.now();
-
- // Get last input
- const query = this.queries[this.queries.length - 1].toLowerCase();
- this.queries = [];
-
- // To improve perf we avoid showing all emojis at the same time.
- if (query === "")
- {
- await classesSelectorDOM.children[0].children[0].click();
- classesSelectorDOM.children[0].children[0].classList.add("active");
- continue;
- }
-
- // Search
- const resultSet = emojisSearchDictionary.completeSearch(query);
-
- // update stuff
- for(const [emojiName, emojiDOM] of Object.entries(emojiDOMs))
- emojiDOM.hidden = !resultSet.has(emojiName);
-
- emojiNotFoundDOM.hidden = resultSet.size !== 0;
-
- let sleepTime = EMOIJ_SEARCH_ENGINE_MIN_INTERVAL - (Date.now() - startTime);
- if (sleepTime > 0)
- await new Promise(r => setTimeout(r, sleepTime));
- }
-
- this.working = false;
- }
-};
-
-// tags dictionary. KEEP IT SORT
-class EmoijsDictNode
-{
- constructor(tag, name) {
- this.tag = tag;
- this.emojiNames = [name];
- }
-}
-const emojisSearchDictionary = {
- dict: [],
-
- updateTag: function(tag, emojiName) {
- if (tag === undefined || emojiName === undefined)
- return;
-
- let low = 0;
- let high = this.dict.length;
-
- while (low < high) {
- let mid = (low + high) >>> 1;
- if (this.dict[mid].tag < tag)
- low = mid + 1;
- else
- high = mid;
- }
-
- let target = low;
- if (this.dict[target] !== undefined && this.dict[target].tag === tag)
- this.dict[target].emojiNames.push(emojiName);
- else
- this.dict.splice(target ,0,new EmoijsDictNode(tag, emojiName));
- },
-
- /**
- * We also check for substrings! (sigh)
- * @param {String} tag
- * @returns {Set}
- */
- completeSearch: function(query) {
- query = query.toLowerCase()
- const result = new Set();
-
- for(let i = 0; i < this.dict.length; i++)
- if (this.dict[i].tag.startsWith('@'))
- {
- if (this.dict[i].tag == query)
- for(let j = 0; j < this.dict[i].emojiNames.length; j++)
- result.add(this.dict[i].emojiNames[j])
- }
- else if(this.dict[i].tag.includes(query))
- for(let j = 0; j < this.dict[i].emojiNames.length; j++)
- result.add(this.dict[i].emojiNames[j])
-
- return result;
- }
-};
-
-function makeEmojisSearchDictionary() {
- // get public emojis list
- const headers = new Headers({xhr: "xhr"})
- const emoji_num = document.getElementById('emoji_num').value
- return fetch(`/emojis.csv?x=${emoji_num}`, {
- headers,
- })
- .then(res => res.json())
- .then(emojis => {
- globalEmojis = emojis.map(({name, author_username, count}) => ({name, author_username, count}));
-
- for (let i = 0; i < emojis.length; i++)
- {
- const emoji = emojis[i];
-
- emojisSearchDictionary.updateTag(emoji.name, emoji.name);
-
- if (!emoji.author_username.endsWith(' user')) {
- emojisSearchDictionary.updateTag(`@${emoji.author_username.toLowerCase()}`, emoji.name);
- if (emoji.author_original_username != emoji.author_username)
- emojisSearchDictionary.updateTag(`@${emoji.author_original_username.toLowerCase()}`, emoji.name);
- if (emoji.author_prelock_username !== undefined)
- emojisSearchDictionary.updateTag(`@${emoji.author_prelock_username.toLowerCase()}`, emoji.name);
- }
-
- if (emoji.tags instanceof Array)
- for(let i = 0; i < emoji.tags.length; i++)
- emojisSearchDictionary.updateTag(emoji.tags[i], emoji.name);
- }
-
- emojiSpeedEngineState = "ready";
- })
-}
-
-
-// get public emojis list
-function fetchEmojis() {
- const headers = new Headers({xhr: "xhr"})
- const emoji_num = document.getElementById('emoji_num').value
- return fetch(`/emojis.csv?x=${emoji_num}`, {
- 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!");
-
- let classes = ["Marsey", "Platy", "Wolf", "Donkey Kong", "Tay", "Capy", "Carp", "Marsey Flags", "Marsey Alphabet", "Classic", "Rage", "Wojak", "Misc"]
-
- const bussyDOM = document.createElement("div");
-
- for(let i = 0; i < emojis.length; i++)
- {
- const emoji = emojis[i];
-
- // Create emoji DOM
- const emojiDOM = document.importNode(emojiButtonTemplateDOM.content, true).children[0];
-
- emojiDOM.title = emoji.name
- emojiDOM.title += "\nauthor\t" + emoji.author_username
- 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 = `${SITE_FULL_IMAGES}/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;
-
- // Add to the document!
- bussyDOM.appendChild(emojiDOM);
- }
-
- // Create header
- for(let className of classes)
- {
- let classSelectorDOM = document.createElement("li");
- classSelectorDOM.classList.add("nav-item");
-
- let classSelectorLinkDOM = document.createElement("button");
- classSelectorLinkDOM.type = "button";
- classSelectorLinkDOM.classList.add("nav-link", "emojitab");
- classSelectorLinkDOM.dataset.bsToggle = "tab";
- classSelectorLinkDOM.dataset.className = className;
- classSelectorLinkDOM.textContent = className;
- classSelectorLinkDOM.addEventListener('click', switchEmojiTab);
-
- classSelectorDOM.appendChild(classSelectorLinkDOM);
- classesSelectorDOM.appendChild(classSelectorDOM);
- }
-
- // Show favorite for start.
- classesSelectorDOM.children[0].children[0].click();
-
- // Send it to the render machine!
- emojiResultsDOM.appendChild(bussyDOM);
-
- emojiResultsDOM.hidden = false;
- emojiWorkingDOM.hidden = true;
- emojiSearchBarDOM.disabled = false;
-
- emojiEngineState = "ready";
- })
-}
-
-/**
-*
-* @param {Event} e
-*/
-function switchEmojiTab(e)
-{
- const className = e.currentTarget.dataset.className;
-
- emojiSearchBarDOM.value = "";
- focusSearchBar(emojiSearchBarDOM);
- emojiNotFoundDOM.hidden = true;
-
- // Special case: favorites
- if (className === "favorite")
- {
- for(const emojiDOM of Object.values(emojiDOMs))
- emojiDOM.hidden = true;
-
- const favs = Object.keys(Object.fromEntries(
- Object.entries(favorite_emojis).sort(([,a],[,b]) => b-a)
- )).slice(0, 25);
-
- for (const emoji of favs)
- if (emojiDOMs[emoji] instanceof HTMLElement)
- emojiDOMs[emoji].hidden = false;
-
- return;
- }
-
- for(const emojiDOM of Object.values(emojiDOMs))
- emojiDOM.hidden = emojiDOM.dataset.className !== className;
-
- document.getElementById('emoji-container').scrollTop = 0;
-}
-
-for (const emojitab of document.getElementsByClassName('emojitab')) {
- emojitab.addEventListener('click', (e)=>{switchEmojiTab(e)})
-}
-
-async function start_search() {
- emojiSearcher.addQuery(emojiSearchBarDOM.value.trim());
-
- // Remove any selected tab, now it is meaningless
- for(let i = 0; i < classesSelectorDOM.children.length; i++)
- classesSelectorDOM.children[i].children[0].classList.remove("active");
-}
-
-/**
-* Add the selected emoji to the targeted text area
-* @param {Event} event
-*/
-function emojiAddToInput(event)
-{
- // This should not happen if used properly but whatever
- if (!(emojiInputTargetDOM instanceof HTMLTextAreaElement) && !(emojiInputTargetDOM instanceof HTMLInputElement))
- return;
-
- let strToInsert = event.currentTarget.dataset.emojiName;
-
- for(let i = 0; i < emojiSelectPostfixDOMs.length; i++)
- if (emojiSelectPostfixDOMs[i].checked)
- strToInsert = strToInsert + emojiSelectPostfixDOMs[i].value;
-
- for(let i = 0; i < emojiSelectSuffixDOMs.length; i++)
- if (emojiSelectSuffixDOMs[i].checked)
- strToInsert = emojiSelectSuffixDOMs[i].value + strToInsert;
-
- strToInsert = ":" + strToInsert + ":"
- insertText(emojiInputTargetDOM, strToInsert)
-
- // kick-start the preview
- emojiInputTargetDOM.dispatchEvent(new Event('input'));
-
- // Update favs. from old code
- if (favorite_emojis[event.currentTarget.dataset.emojiName])
- favorite_emojis[event.currentTarget.dataset.emojiName] += 1;
- else
- favorite_emojis[event.currentTarget.dataset.emojiName] = 1;
- localStorage.setItem("favorite_emojis", JSON.stringify(favorite_emojis));
-}
-
-let emoji_typing_state = false;
-
-function update_ghost_div_textarea(text)
-{
- let ghostdiv
-
- if (location.pathname == '/chat')
- ghostdiv = document.getElementById("ghostdiv-chat");
- else
- ghostdiv = text.parentNode.getElementsByClassName("ghostdiv")[0];
-
- if (!ghostdiv) return;
-
- ghostdiv.textContent = text.value.substring(0, text.selectionStart);
-
- ghostdiv.insertAdjacentHTML('beforeend', "");
-
- // Now lets get coordinates
-
- ghostdiv.style.display = "block";
- let end = ghostdiv.querySelector("span");
- const carot_coords = end.getBoundingClientRect();
- const ghostdiv_coords = ghostdiv.getBoundingClientRect();
- ghostdiv.style.display = "none";
- return { pos: text.selectionStart, x: carot_coords.x, y: carot_coords.y - ghostdiv_coords.y };
-}
-
-// Used for anything where a user is typing, specifically for the emoji modal
-// Just leave it global, I don't care
-let speed_carot_modal = document.createElement("div");
-speed_carot_modal.id = "speed-carot-modal";
-speed_carot_modal.style.position = "absolute";
-speed_carot_modal.style.left = "0px";
-speed_carot_modal.style.top = "0px";
-speed_carot_modal.style.display = "none";
-document.body.appendChild(speed_carot_modal);
-
-let e
-
-let current_word = "";
-let selecting;
-let emoji_index = 0;
-
-function curr_word_is_emoji()
-{
- return current_word && current_word.charAt(0) == ":" &&
- current_word.charAt(current_word.length-1) != ":";
-}
-
-function close_inline_speed_emoji_modal() {
- selecting = false;
- speed_carot_modal.style.display = "none";
-}
-
-function populate_speed_emoji_modal(results, textbox)
-{
- selecting = true;
-
- if (!results || results.size === 0)
- {
- speed_carot_modal.style.display = "none";
- return -1;
- }
-
- emoji_index = 0;
- speed_carot_modal.scrollTop = 0;
- speed_carot_modal.innerHTML = "";
- const MAXXX = 50;
- // Not sure why the results is a Set... but oh well
- let i = 0;
- for (let emoji of results)
- {
- let name = emoji.name
-
- if (i++ > MAXXX) return i;
- let emoji_option = document.createElement("div");
- emoji_option.className = "speed-modal-option emoji-option " + (i === 1 ? "selected" : "");
- emoji_option.tabIndex = 0;
- let emoji_option_img = document.createElement("img");
- emoji_option_img.className = "speed-modal-image emoji-option-image";
- // This is a bit
- emoji_option_img.src = `${SITE_FULL_IMAGES}/e/${name}.webp`
- let emoji_option_text = document.createElement("span");
-
- emoji_option_text.title = name;
-
- emoji_option_text.title += "\nauthor\t" + emoji.author_username
-
- if (emoji.count !== undefined)
- emoji_option_text.title += "\nused\t" + emoji.count;
-
- emoji_option_text.textContent = name;
-
- if (current_word.includes("#")) name = `#${name}`
- if (current_word.includes("!")) name = `!${name}`
-
- emoji_option.addEventListener('click', () => {
- close_inline_speed_emoji_modal()
- textbox.value = textbox.value.replace(new RegExp(current_word+"(?=\\s|$)", "gi"), `:${name}: `)
- textbox.focus()
- if (typeof markdown === "function" && textbox.dataset.preview) {
- markdown(textbox)
- }
- });
- // Pack
- emoji_option.appendChild(emoji_option_img);
- emoji_option.appendChild(emoji_option_text);
- speed_carot_modal.appendChild(emoji_option);
- }
- if (i === 0) speed_carot_modal.style.display = "none";
- else speed_carot_modal.style.display = "initial";
- return i;
-}
-
-function update_speed_emoji_modal(event)
-{
- const box_coords = update_ghost_div_textarea(event.target);
-
- box_coords.x = Math.min(box_coords.x, screen_width - 150)
-
- let text = event.target.value;
-
- // Unused, but left incase anyone wants to use this more efficient method for emojis
- switch (event.data)
- {
- case ':':
- emoji_typing_state = true;
- break;
- case ' ':
- emoji_typing_state = false;
- break;
- default:
- break;
- }
-
- // 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[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 */
- if (current_word && curr_word_is_emoji() && current_word != ":")
- {
- openSpeedModal().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 - 30 + "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, ""));
-
- const found = globalEmojis.filter(i => resultSet.has(i.name));
-
- populate_speed_emoji_modal(found, event.target);
- });
- }
- else {
- speed_carot_modal.style.display = "none";
- }
-}
-
-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;
-
- 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)
- {
- 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();
- }
-}
-
-function insertGhostDivs(element) {
- let forms = element.querySelectorAll("textarea, .allow-emojis");
- forms.forEach(i => {
- let ghostdiv
- if (i.id == 'input-text-chat') {
- ghostdiv = document.getElementsByClassName("ghostdiv")[0];
- }
- else {
- ghostdiv = document.createElement("div");
- ghostdiv.className = "ghostdiv";
- ghostdiv.style.display = "none";
- i.after(ghostdiv);
- }
- i.addEventListener('input', update_speed_emoji_modal, false);
- i.addEventListener('keydown', speed_carot_navigate, false);
- });
-}
-
-const emojiModal = document.getElementById('emojiModal')
-
-function openEmojiModal(t, inputTargetIDName)
-{
- selecting = false;
- speed_carot_modal.style.display = "none";
-
- if (inputTargetIDName) {
- emojiInputTargetDOM = document.getElementById(inputTargetIDName);
- emojiModal.addEventListener('hide.bs.modal', () => {
- setTimeout(() => {
- emojiInputTargetDOM.focus();
- }, 200);
- }, {once : true});
- }
-
- if (t && t.dataset.previousModal) {
- emojiModal.addEventListener('hide.bs.modal', () => {
- bootstrap.Modal.getOrCreateInstance(document.getElementById(t.dataset.previousModal)).show()
- }, {once : true});
- }
-
- switch (emojiEngineState) {
- case "inactive":
- emojiEngineState = "loading"
- makeEmojisSearchDictionary();
- 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");
- }
-}
-
-function openSpeedModal()
-{
- switch (emojiSpeedEngineState) {
- case "inactive":
- emojiSpeedEngineState = "loading"
- return makeEmojisSearchDictionary();
- 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 () {
- focusSearchBar(emojiSearchBarDOM);
- setTimeout(() => {
- focusSearchBar(emojiSearchBarDOM);
- }, 200);
- setTimeout(() => {
- focusSearchBar(emojiSearchBarDOM);
- }, 1000);
-});
diff --git a/files/assets/js/emoji_modal/emoji_modal.js b/files/assets/js/emoji_modal/emoji_modal.js
new file mode 100644
index 0000000000..d96a36c756
--- /dev/null
+++ b/files/assets/js/emoji_modal/emoji_modal.js
@@ -0,0 +1,276 @@
+let emojiEngineState = "inactive";
+
+// DOM stuff
+const classesSelectorDOM = document.getElementById("emoji-modal-tabs");
+const emojiButtonTemplateDOM = document.getElementById("emoji-button-template");
+const emojiResultsDOM = document.getElementById("tab-content");
+
+const emojiSelectSuffixDOMs = document.getElementsByClassName("emoji-suffix");
+const emojiSelectPostfixDOMs= document.getElementsByClassName("emoji-postfix");
+
+const emojiNotFoundDOM = document.getElementById("no-emojis-found");
+const emojiWorkingDOM = document.getElementById("emojis-work");
+
+const emojiSearchBarDOM = document.getElementById('emoji_search');
+
+let emojiInputTargetDOM = undefined;
+
+// Emojis usage stats. I don't really like this format but I'll keep it for backward comp.
+const favorite_emojis = JSON.parse(localStorage.getItem("favorite_emojis")) || {};
+
+/** Associative array of all the emojis' DOM */
+let emojiDOMs = {};
+
+const EMOIJ_SEARCH_ENGINE_MIN_INTERVAL = 350;
+let emojiSearcher = {
+ working: false,
+ queries: [],
+
+ addQuery: function(query)
+ {
+ this.queries.push(query);
+ if (!this.working)
+ this.work();
+ },
+
+ work: async function work() {
+ this.working = true;
+
+ while(this.queries.length > 0)
+ {
+ const startTime = Date.now();
+
+ // Get last input
+ const query = this.queries[this.queries.length - 1].toLowerCase();
+ this.queries = [];
+
+ // To improve perf we avoid showing all emojis at the same time.
+ if (query === "")
+ {
+ await classesSelectorDOM.children[0].children[0].click();
+ classesSelectorDOM.children[0].children[0].classList.add("active");
+ continue;
+ }
+
+ // Search
+ const resultSet = emojisSearchDictionary.completeSearch(query);
+
+ // update stuff
+ for(const [emojiName, emojiDOM] of Object.entries(emojiDOMs))
+ emojiDOM.hidden = !resultSet.has(emojiName);
+
+ emojiNotFoundDOM.hidden = resultSet.size !== 0;
+
+ let sleepTime = EMOIJ_SEARCH_ENGINE_MIN_INTERVAL - (Date.now() - startTime);
+ if (sleepTime > 0)
+ await new Promise(r => setTimeout(r, sleepTime));
+ }
+
+ this.working = false;
+ }
+};
+
+// get public emojis list
+function fetchEmojis() {
+ const headers = new Headers({xhr: "xhr"})
+ const emoji_num = document.getElementById('emoji_num').value
+ return fetch(`/emojis.csv?x=${emoji_num}`, {
+ 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!");
+
+ let classes = ["Marsey", "Platy", "Wolf", "Donkey Kong", "Tay", "Capy", "Carp", "Marsey Flags", "Marsey Alphabet", "Classic", "Rage", "Wojak", "Misc"]
+
+ const bussyDOM = document.createElement("div");
+
+ for(let i = 0; i < emojis.length; i++)
+ {
+ const emoji = emojis[i];
+
+ // Create emoji DOM
+ const emojiDOM = document.importNode(emojiButtonTemplateDOM.content, true).children[0];
+
+ emojiDOM.title = emoji.name
+ emojiDOM.title += "\nauthor\t" + emoji.author_username
+ 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 = `${SITE_FULL_IMAGES}/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;
+
+ // Add to the document!
+ bussyDOM.appendChild(emojiDOM);
+ }
+
+ // Create header
+ for(let className of classes)
+ {
+ let classSelectorDOM = document.createElement("li");
+ classSelectorDOM.classList.add("nav-item");
+
+ let classSelectorLinkDOM = document.createElement("button");
+ classSelectorLinkDOM.type = "button";
+ classSelectorLinkDOM.classList.add("nav-link", "emojitab");
+ classSelectorLinkDOM.dataset.bsToggle = "tab";
+ classSelectorLinkDOM.dataset.className = className;
+ classSelectorLinkDOM.textContent = className;
+ classSelectorLinkDOM.addEventListener('click', switchEmojiTab);
+
+ classSelectorDOM.appendChild(classSelectorLinkDOM);
+ classesSelectorDOM.appendChild(classSelectorDOM);
+ }
+
+ // Show favorite for start.
+ classesSelectorDOM.children[0].children[0].click();
+
+ // Send it to the render machine!
+ emojiResultsDOM.appendChild(bussyDOM);
+
+ emojiResultsDOM.hidden = false;
+ emojiWorkingDOM.hidden = true;
+ emojiSearchBarDOM.disabled = false;
+
+ emojiEngineState = "ready";
+ })
+}
+
+/**
+*
+* @param {Event} e
+*/
+function switchEmojiTab(e)
+{
+ const className = e.currentTarget.dataset.className;
+
+ emojiSearchBarDOM.value = "";
+ focusSearchBar(emojiSearchBarDOM);
+ emojiNotFoundDOM.hidden = true;
+
+ // Special case: favorites
+ if (className === "favorite")
+ {
+ for(const emojiDOM of Object.values(emojiDOMs))
+ emojiDOM.hidden = true;
+
+ const favs = Object.keys(Object.fromEntries(
+ Object.entries(favorite_emojis).sort(([,a],[,b]) => b-a)
+ )).slice(0, 25);
+
+ for (const emoji of favs)
+ if (emojiDOMs[emoji] instanceof HTMLElement)
+ emojiDOMs[emoji].hidden = false;
+
+ return;
+ }
+
+ for(const emojiDOM of Object.values(emojiDOMs))
+ emojiDOM.hidden = emojiDOM.dataset.className !== className;
+
+ document.getElementById('emoji-container').scrollTop = 0;
+}
+
+for (const emojitab of document.getElementsByClassName('emojitab')) {
+ emojitab.addEventListener('click', (e)=>{switchEmojiTab(e)})
+}
+
+async function start_search() {
+ emojiSearcher.addQuery(emojiSearchBarDOM.value.trim());
+
+ // Remove any selected tab, now it is meaningless
+ for(let i = 0; i < classesSelectorDOM.children.length; i++)
+ classesSelectorDOM.children[i].children[0].classList.remove("active");
+}
+
+/**
+* Add the selected emoji to the targeted text area
+* @param {Event} event
+*/
+function emojiAddToInput(event)
+{
+ // This should not happen if used properly but whatever
+ if (!(emojiInputTargetDOM instanceof HTMLTextAreaElement) && !(emojiInputTargetDOM instanceof HTMLInputElement))
+ return;
+
+ let strToInsert = event.currentTarget.dataset.emojiName;
+
+ for(let i = 0; i < emojiSelectPostfixDOMs.length; i++)
+ if (emojiSelectPostfixDOMs[i].checked)
+ strToInsert = strToInsert + emojiSelectPostfixDOMs[i].value;
+
+ for(let i = 0; i < emojiSelectSuffixDOMs.length; i++)
+ if (emojiSelectSuffixDOMs[i].checked)
+ strToInsert = emojiSelectSuffixDOMs[i].value + strToInsert;
+
+ strToInsert = ":" + strToInsert + ":"
+ insertText(emojiInputTargetDOM, strToInsert)
+
+ // kick-start the preview
+ emojiInputTargetDOM.dispatchEvent(new Event('input'));
+
+ // Update favs. from old code
+ if (favorite_emojis[event.currentTarget.dataset.emojiName])
+ favorite_emojis[event.currentTarget.dataset.emojiName] += 1;
+ else
+ favorite_emojis[event.currentTarget.dataset.emojiName] = 1;
+ localStorage.setItem("favorite_emojis", JSON.stringify(favorite_emojis));
+}
+
+const emojiModal = document.getElementById('emojiModal')
+
+function openEmojiModal(t, inputTargetIDName)
+{
+ selecting = false;
+
+ if (inputTargetIDName) {
+ emojiInputTargetDOM = document.getElementById(inputTargetIDName);
+ emojiModal.addEventListener('hide.bs.modal', () => {
+ setTimeout(() => {
+ emojiInputTargetDOM.focus();
+ }, 200);
+ }, {once : true});
+ }
+
+ if (t && t.dataset.previousModal) {
+ emojiModal.addEventListener('hide.bs.modal', () => {
+ bootstrap.Modal.getOrCreateInstance(document.getElementById(t.dataset.previousModal)).show()
+ }, {once : true});
+ }
+
+ switch (emojiEngineState) {
+ case "inactive":
+ emojiEngineState = "loading"
+ makeEmojisSearchDictionary();
+ 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 () {
+ focusSearchBar(emojiSearchBarDOM);
+ setTimeout(() => {
+ focusSearchBar(emojiSearchBarDOM);
+ }, 200);
+ setTimeout(() => {
+ focusSearchBar(emojiSearchBarDOM);
+ }, 1000);
+});
diff --git a/files/assets/js/emoji_modal/search_dictionary.js b/files/assets/js/emoji_modal/search_dictionary.js
new file mode 100644
index 0000000000..ad7981c34d
--- /dev/null
+++ b/files/assets/js/emoji_modal/search_dictionary.js
@@ -0,0 +1,90 @@
+// tags dictionary. KEEP IT SORT
+class EmoijsDictNode
+{
+ constructor(tag, name) {
+ this.tag = tag;
+ this.emojiNames = [name];
+ }
+}
+const emojisSearchDictionary = {
+ dict: [],
+
+ updateTag: function(tag, emojiName) {
+ if (tag === undefined || emojiName === undefined)
+ return;
+
+ let low = 0;
+ let high = this.dict.length;
+
+ while (low < high) {
+ let mid = (low + high) >>> 1;
+ if (this.dict[mid].tag < tag)
+ low = mid + 1;
+ else
+ high = mid;
+ }
+
+ let target = low;
+ if (this.dict[target] !== undefined && this.dict[target].tag === tag)
+ this.dict[target].emojiNames.push(emojiName);
+ else
+ this.dict.splice(target ,0,new EmoijsDictNode(tag, emojiName));
+ },
+
+ /**
+ * We also check for substrings! (sigh)
+ * @param {String} tag
+ * @returns {Set}
+ */
+ completeSearch: function(query) {
+ query = query.toLowerCase()
+ const result = new Set();
+
+ for(let i = 0; i < this.dict.length; i++)
+ if (this.dict[i].tag.startsWith('@'))
+ {
+ if (this.dict[i].tag == query)
+ for(let j = 0; j < this.dict[i].emojiNames.length; j++)
+ result.add(this.dict[i].emojiNames[j])
+ }
+ else if(this.dict[i].tag.includes(query))
+ for(let j = 0; j < this.dict[i].emojiNames.length; j++)
+ result.add(this.dict[i].emojiNames[j])
+
+ return result;
+ }
+};
+
+function makeEmojisSearchDictionary() {
+ // get public emojis list
+ const headers = new Headers({xhr: "xhr"})
+ const emoji_num = document.getElementById('emoji_num').value
+ return fetch(`/emojis.csv?x=${emoji_num}`, {
+ headers,
+ })
+ .then(res => res.json())
+ .then(emojis => {
+ globalEmojis = emojis.map(({name, author_username, count}) => ({name, author_username, count}));
+
+ for (let i = 0; i < emojis.length; i++)
+ {
+ const emoji = emojis[i];
+
+ emojisSearchDictionary.updateTag(emoji.name, emoji.name);
+
+ if (!emoji.author_username.endsWith(' user')) {
+ emojisSearchDictionary.updateTag(`@${emoji.author_username.toLowerCase()}`, emoji.name);
+ if (emoji.author_original_username != emoji.author_username)
+ emojisSearchDictionary.updateTag(`@${emoji.author_original_username.toLowerCase()}`, emoji.name);
+ if (emoji.author_prelock_username !== undefined)
+ emojisSearchDictionary.updateTag(`@${emoji.author_prelock_username.toLowerCase()}`, emoji.name);
+ }
+
+ if (emoji.tags instanceof Array)
+ for(let i = 0; i < emoji.tags.length; i++)
+ emojisSearchDictionary.updateTag(emoji.tags[i], emoji.name);
+ }
+
+ emojiSpeedEngineState = "ready";
+ })
+}
diff --git a/files/assets/js/emoji_modal/speed_emoji_modal.js b/files/assets/js/emoji_modal/speed_emoji_modal.js
new file mode 100644
index 0000000000..24eb5077a5
--- /dev/null
+++ b/files/assets/js/emoji_modal/speed_emoji_modal.js
@@ -0,0 +1,226 @@
+let emojiSpeedEngineState = "inactive";
+let emoji_typing_state = false;
+let globalEmojis;
+
+function update_ghost_div_textarea(text)
+{
+ let ghostdiv
+
+ if (location.pathname == '/chat')
+ ghostdiv = document.getElementById("ghostdiv-chat");
+ else
+ ghostdiv = text.parentNode.getElementsByClassName("ghostdiv")[0];
+
+ if (!ghostdiv) return;
+
+ ghostdiv.textContent = text.value.substring(0, text.selectionStart);
+
+ ghostdiv.insertAdjacentHTML('beforeend', "");
+
+ // Now lets get coordinates
+
+ ghostdiv.style.display = "block";
+ let end = ghostdiv.querySelector("span");
+ const carot_coords = end.getBoundingClientRect();
+ const ghostdiv_coords = ghostdiv.getBoundingClientRect();
+ ghostdiv.style.display = "none";
+ return { pos: text.selectionStart, x: carot_coords.x, y: carot_coords.y - ghostdiv_coords.y };
+}
+
+// Used for anything where a user is typing, specifically for the emoji modal
+// Just leave it global, I don't care
+let speed_carot_modal = document.createElement("div");
+speed_carot_modal.id = "speed-carot-modal";
+speed_carot_modal.style.position = "absolute";
+speed_carot_modal.style.left = "0px";
+speed_carot_modal.style.top = "0px";
+speed_carot_modal.style.display = "none";
+document.body.appendChild(speed_carot_modal);
+
+let e
+
+let current_word = "";
+let selecting;
+let emoji_index = 0;
+
+function curr_word_is_emoji()
+{
+ return current_word && current_word.charAt(0) == ":" &&
+ current_word.charAt(current_word.length-1) != ":";
+}
+
+function close_inline_speed_emoji_modal() {
+ selecting = false;
+ speed_carot_modal.style.display = "none";
+}
+
+function populate_speed_emoji_modal(results, textbox)
+{
+ selecting = true;
+
+ if (!results || results.size === 0)
+ {
+ speed_carot_modal.style.display = "none";
+ return -1;
+ }
+
+ emoji_index = 0;
+ speed_carot_modal.scrollTop = 0;
+ speed_carot_modal.innerHTML = "";
+ const MAXXX = 50;
+ // Not sure why the results is a Set... but oh well
+ let i = 0;
+ for (let emoji of results)
+ {
+ let name = emoji.name
+
+ if (i++ > MAXXX) return i;
+ let emoji_option = document.createElement("div");
+ emoji_option.className = "speed-modal-option emoji-option " + (i === 1 ? "selected" : "");
+ emoji_option.tabIndex = 0;
+ let emoji_option_img = document.createElement("img");
+ emoji_option_img.className = "speed-modal-image emoji-option-image";
+ // This is a bit
+ emoji_option_img.src = `${SITE_FULL_IMAGES}/e/${name}.webp`
+ let emoji_option_text = document.createElement("span");
+
+ emoji_option_text.title = name;
+
+ emoji_option_text.title += "\nauthor\t" + emoji.author_username
+
+ if (emoji.count !== undefined)
+ emoji_option_text.title += "\nused\t" + emoji.count;
+
+ emoji_option_text.textContent = name;
+
+ if (current_word.includes("#")) name = `#${name}`
+ if (current_word.includes("!")) name = `!${name}`
+
+ emoji_option.addEventListener('click', () => {
+ close_inline_speed_emoji_modal()
+ textbox.value = textbox.value.replace(new RegExp(current_word+"(?=\\s|$)", "gi"), `:${name}: `)
+ textbox.focus()
+ if (typeof markdown === "function" && textbox.dataset.preview) {
+ markdown(textbox)
+ }
+ });
+ // Pack
+ emoji_option.appendChild(emoji_option_img);
+ emoji_option.appendChild(emoji_option_text);
+ speed_carot_modal.appendChild(emoji_option);
+ }
+ if (i === 0) speed_carot_modal.style.display = "none";
+ else speed_carot_modal.style.display = "initial";
+ return i;
+}
+
+function update_speed_emoji_modal(event)
+{
+ const box_coords = update_ghost_div_textarea(event.target);
+
+ box_coords.x = Math.min(box_coords.x, screen_width - 150)
+
+ let text = event.target.value;
+
+ // Unused, but left incase anyone wants to use this more efficient method for emojis
+ switch (event.data)
+ {
+ case ':':
+ emoji_typing_state = true;
+ break;
+ case ' ':
+ emoji_typing_state = false;
+ break;
+ default:
+ break;
+ }
+
+ // 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[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 */
+ if (current_word && curr_word_is_emoji() && current_word != ":")
+ {
+ openSpeedModal().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 - 30 + "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, ""));
+
+ const found = globalEmojis.filter(i => resultSet.has(i.name));
+
+ populate_speed_emoji_modal(found, event.target);
+ });
+ }
+ else {
+ speed_carot_modal.style.display = "none";
+ }
+}
+
+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;
+
+ 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)
+ {
+ 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();
+ }
+}
+
+function insertGhostDivs(element) {
+ let forms = element.querySelectorAll("textarea, .allow-emojis");
+ forms.forEach(i => {
+ let ghostdiv
+ if (i.id == 'input-text-chat') {
+ ghostdiv = document.getElementsByClassName("ghostdiv")[0];
+ }
+ else {
+ ghostdiv = document.createElement("div");
+ ghostdiv.className = "ghostdiv";
+ ghostdiv.style.display = "none";
+ i.after(ghostdiv);
+ }
+ i.addEventListener('input', update_speed_emoji_modal, false);
+ i.addEventListener('keydown', speed_carot_navigate, false);
+ });
+}
+
+function openSpeedModal()
+{
+ switch (emojiSpeedEngineState) {
+ case "inactive":
+ emojiSpeedEngineState = "loading"
+ return makeEmojisSearchDictionary();
+ 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");
+ }
+}
diff --git a/files/templates/modals/emoji.html b/files/templates/modals/emoji.html
index e3065ccde2..ae1dd70375 100644
--- a/files/templates/modals/emoji.html
+++ b/files/templates/modals/emoji.html
@@ -71,4 +71,6 @@
-
+
+
+