// eslint-disable-next-line spaced-comment /// import { Translator } from 'translator'; import { render } from 'benchpress'; import { insertIntoTextarea, updateTextareaSelection } from 'composer/controls'; import { apply as applyScrollStop } from 'scrollStop'; import { buster, base, table, buildEmoji, init as initEmoji, search, } from 'emoji'; const $html = $('html'); export const dialogActions = { open(dialog: JQuery) { $html.addClass('emoji-insert'); dialog.addClass('open'); dialog.find('.emoji-dialog-search').focus(); return dialog; }, close(dialog: JQuery) { $html.removeClass('emoji-insert'); return dialog.removeClass('open'); }, }; const priorities: { [name: string]: number; } = { people: 10, nature: 9, food: 8, activity: 7, travel: 6, objects: 5, symbols: 4, flags: 3, regional: 2, modifier: 1, other: 0, }; if (window.config.emojiCustomFirst) { priorities.custom = 100; } const translator = Translator.create(); function stringCompare(a: string, b: string) { if (a < b) { return -1; } if (a > b) { return 1; } return 0; } // create modal export function init(callback: Callback) { Promise.all([ $.getJSON(`${base}/emoji/categories.json?${buster}`), $.getJSON(`${base}/emoji/packs.json?${buster}`), new Promise((resolve) => initEmoji(resolve)), ]) .then(([categoriesInfo, packs]: [MetaData.Categories, MetaData.Packs, undefined]) => { const categories = Object.keys(categoriesInfo).map((category) => { const emojis = categoriesInfo[category].map((name) => table[name]); return { name: category, emojis: emojis.map((emoji) => ({ name: emoji.name, html: buildEmoji(emoji, true), })).sort((a, b) => stringCompare(a.name, b.name)), }; }).sort((a, b) => { const aPriority = priorities[a.name] || 0; const bPriority = priorities[b.name] || 0; return bPriority - aPriority; }); return render('partials/emoji-dialog', { categories, packs, }); }) .then((result) => translator.translate(result)).then((html) => { const dialog = $(html).appendTo('body'); dialog.find('.emoji-dialog-search').on('input', (e) => { const value = (e.target as HTMLInputElement).value; if (!value) { dialog.find('.emoji-dialog-search-results').addClass('hidden'); dialog.find('.nav-tabs .emoji-dialog-search-results').next().find('a').tab('show'); return; } const results = search(value) .slice(0, 100) .map( (emoji) => `${buildEmoji(emoji, false)}` ) .join('\n'); dialog.find('.tab-pane.emoji-dialog-search-results').html(results); dialog.find('.emoji-dialog-search-results').removeClass('hidden'); dialog.find('.nav-tabs .emoji-dialog-search-results a').tab('show'); }); const tabContent = dialog.find('.emoji-tabs .tab-content'); function showDeferred(container: JQuery) { const rect = tabContent[0].getBoundingClientRect(); const num = Math.ceil((rect.width * rect.height) / (40 * 40)); container .find('.emoji-link img.defer') .filter((i, elem) => { if (i <= num) { return true; } const elemRect = elem.getBoundingClientRect(); return elemRect.right > rect.left && elemRect.left < rect.right && elemRect.bottom > rect.top && elemRect.top < rect.bottom; }) .removeClass('defer') .each((i, elem) => { const src = elem.getAttribute('data-src'); elem.setAttribute('src', src); }); } const firstTab = dialog.find('.emoji-tabs .nav-tabs a').click((e) => { e.preventDefault(); $(e.target).tab('show'); }).on('show.bs.tab', (e) => { showDeferred($(e.target.getAttribute('href'))); }).eq(1); setTimeout(() => firstTab.trigger('show.bs.tab'), 10); tabContent.on('scroll', () => { showDeferred(tabContent.find('.tab-pane.active')); }); applyScrollStop(tabContent[0]); const close = () => dialogActions.close(dialog); $(window).on([ 'action:composer.discard', 'action:composer.submit', 'action:composer.minimize', 'action:chat.minimize', 'action:chat.closed', ].join(' '), close); dialog.find('.close').click(close); $(document).on('click', (e) => { if (!$(e.target).is('.emoji-dialog *')) { close(); } }); if (dialog.draggable) { dialog.draggable({ handle: '.top-bar', }); } callback(dialog); }) .catch((err) => { console.error('Failed to initialize emoji dialog', err); }); } export function toggle( opener: HTMLElement | null, onClick: (e: JQuery.Event, name: string, dialog: JQuery) => void ) { function after(dialog: JQuery) { if (dialog.hasClass('open')) { dialogActions.close(dialog); return; } dialog.off('click').on('click', '.emoji-link', (e) => { e.preventDefault(); const name = (e.currentTarget as HTMLAnchorElement).name; onClick(e, name, dialog); }); // default if there's no button const buttonRect = opener ? opener.getBoundingClientRect() : { top: window.innerHeight / 3, left: window.innerWidth / 3, }; const position = { bottom: 'auto', top: 'auto', right: 'auto', left: 'auto', }; if (buttonRect.top > 440) { position.top = `${buttonRect.top - 400}px`; } else { position.top = `${buttonRect.top + 40}px`; } if (buttonRect.left < window.innerWidth / 2) { position.left = `${buttonRect.left + 40}px`; } else { position.left = `${buttonRect.left - 400}px`; } dialog.css(position); dialogActions.open(dialog); } const dialog = $('#emoji-dialog'); if (dialog.length) { after(dialog); } else { init(after); } } export function toggleForInsert( textarea: HTMLTextAreaElement, selectStart: number, selectEnd: number, event: JQuery.ClickEvent ) { // handle new and old API case let button; if (event && event.target) { button = $(event.target); } else { button = $(textarea).parents('.composer-container').find('[data-format="emoji-add-emoji"]'); } button = button[0]; toggle(button, (e, name) => { const text = `:${name}: `; const { selectionStart, selectionEnd } = textarea; const end = selectionEnd + text.length; const start = selectionStart === selectionEnd ? end : selectionStart; insertIntoTextarea(textarea, text); updateTextareaSelection(textarea, start, end); $(textarea).trigger('input'); }); }