import "./emoji-picker-content.js"; import DOMNavigator from "../dom-navigator"; import { BaseDropdown } from "./dropdown.js"; import { CustomElement } from './element.js'; import { __ } from '@converse/headless/i18n'; import { _converse, api, converse } from "@converse/headless/converse-core"; import { debounce, find } from "lodash-es"; import { html } from "lit-element"; import { tpl_emoji_picker } from "../templates/emoji_picker.js"; import { until } from 'lit-html/directives/until.js'; const u = converse.env.utils; export default class EmojiPicker extends CustomElement { static get properties () { return { 'chatview': { type: Object }, 'current_category': { type: String, 'reflect': true }, 'current_skintone': { type: String, 'reflect': true }, 'model': { type: Object }, 'query': { type: String, 'reflet': true }, // This is an optimization, we lazily render the emoji picker, otherwise tests slow to a crawl. 'render_emojis': { type: Boolean }, } } firstUpdated () { this.listenTo(this.model, 'change', o => this.onModelChanged(o.changed)); this.initArrowNavigation(); } constructor () { super(); this.search_results = []; this.debouncedFilter = debounce(input => this.model.set({'query': input.value}), 250); this.onGlobalKeyDown = ev => this._onGlobalKeyDown(ev); const body = document.querySelector('body'); body.addEventListener('keydown', this.onGlobalKeyDown); } render () { return tpl_emoji_picker({ 'chatview': this.chatview, 'current_category': this.current_category, 'current_skintone': this.current_skintone, 'model': this.model, 'onCategoryPicked': ev => this.chooseCategory(ev), 'onSearchInputBlurred': ev => this.chatview.emitFocused(ev), 'onSearchInputFocus': ev => this.onSearchInputFocus(ev), 'onSearchInputKeyDown': ev => this.onKeyDown(ev), 'onSkintonePicked': ev => this.chooseSkinTone(ev), 'query': this.query, 'search_results': this.search_results, 'render_emojis': this.render_emojis, 'sn2Emoji': shortname => u.shortnamesToEmojis(this.getTonedShortname(shortname)) }); } updated (changed) { changed.has('query') && this.updateSearchResults(); changed.has('current_category') && this.setScrollPosition(); } onModelChanged (changed) { if ('current_category' in changed) this.current_category = changed.current_category; if ('current_skintone' in changed) this.current_skintone = changed.current_skintone; if ('query' in changed) this.query = changed.query; } setScrollPosition () { if (this.preserve_scroll) { this.preserve_scroll = false; return; } const el = this.querySelector('.emoji-lists__container--browse'); const heading = this.querySelector(`#emoji-picker-${this.current_category}`); if (heading) { // +4 due to 2px padding on list elements el.scrollTop = heading.offsetTop - heading.offsetHeight*3 + 4; } } updateSearchResults () { const contains = _converse.FILTER_CONTAINS; if (this.query) { if (this.query === this.old_query) { return this.search_results; } else if (this.old_query && this.query.includes(this.old_query)) { this.search_results = this.search_results.filter(e => contains(e.sn, this.query)); } else { this.search_results = converse.emojis.list.filter(e => contains(e.sn, this.query)); } this.old_query = this.query; } else if (this.search_results.length) { // Avoid re-rendering by only setting to new empty array if it wasn't empty before this.search_results = []; } return this.search_results; } disconnectedCallback() { super.disconnectedCallback() const body = document.querySelector('body'); body.removeEventListener('keydown', this.onGlobalKeyDown); } _onGlobalKeyDown (ev) { if (!this.navigator) { return; } if (ev.keyCode === converse.keycodes.ENTER && this.navigator.selected && u.isVisible(this)) { this.onEnterPressed(ev); } else if (ev.keyCode === converse.keycodes.DOWN_ARROW && !this.navigator.enabled && u.isVisible(this)) { this.enableArrowNavigation(ev); } } setCategoryForElement (el) { const old_category = this.current_category; const category = el?.getAttribute('data-category') || old_category; if (old_category !== category) { this.model.save({'current_category': category}); } } insertIntoTextArea (value) { this.chatview.onEmojiReceivedFromPicker(value); this.model.set({'query': ''}); } chooseSkinTone (ev) { ev.preventDefault(); ev.stopPropagation(); const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; const skintone = target.getAttribute("data-skintone").trim(); if (this.current_skintone === skintone) { this.model.save({'current_skintone': ''}); } else { this.model.save({'current_skintone': skintone}); } } chooseCategory (ev) { ev.preventDefault && ev.preventDefault(); ev.stopPropagation && ev.stopPropagation(); const el = ev.target.matches('li') ? ev.target : u.ancestor(ev.target, 'li'); this.setCategoryForElement(el); this.navigator.select(el); !this.navigator.enabled && this.navigator.enable(); } onKeyDown (ev) { if (ev.keyCode === converse.keycodes.TAB) { if (ev.target.value) { ev.preventDefault(); const match = find(converse.emojis.shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value)); match && this.model.set({'query': match}); } else if (!this.navigator.enabled) { this.enableArrowNavigation(ev); } } else if (ev.keyCode === converse.keycodes.DOWN_ARROW && !this.navigator.enabled) { this.enableArrowNavigation(ev); } else if (ev.keyCode === converse.keycodes.ENTER) { this.onEnterPressed(ev); } else if (ev.keyCode === converse.keycodes.ESCAPE) { this.chatview.el.querySelector('.chat-textarea').focus(); ev.stopPropagation(); ev.preventDefault(); } else if ( ev.keyCode !== converse.keycodes.ENTER && ev.keyCode !== converse.keycodes.DOWN_ARROW ) { this.debouncedFilter(ev.target); } } onEnterPressed (ev) { ev.preventDefault(); ev.stopPropagation(); if (converse.emojis.shortnames.includes(ev.target.value)) { this.insertIntoTextArea(ev.target.value); } else if (this.search_results.length === 1) { this.insertIntoTextArea(this.search_results[0].sn); } else if (this.navigator.selected && this.navigator.selected.matches('.insert-emoji')) { this.insertIntoTextArea(this.navigator.selected.getAttribute('data-emoji')); } else if (this.navigator.selected && this.navigator.selected.matches('.emoji-category')) { this.chooseCategory({'target': this.navigator.selected}); } } onSearchInputFocus (ev) { this.chatview.emitBlurred(ev); this.disableArrowNavigation(); } getTonedShortname (shortname) { if (converse.emojis.toned.includes(shortname) && this.current_skintone) { return `${shortname.slice(0, shortname.length-1)}_${this.current_skintone}:` } return shortname; } initArrowNavigation () { if (!this.navigator) { const default_selector = 'li:not(.hidden):not(.emoji-skintone), .emoji-search'; const options = { 'jump_to_picked': '.emoji-category', 'jump_to_picked_selector': '.emoji-category.picked', 'jump_to_picked_direction': DOMNavigator.DIRECTION.down, 'picked_selector': '.picked', 'scroll_container': this.querySelector('.emoji-picker__lists'), 'getSelector': direction => { if (direction === DOMNavigator.DIRECTION.down) { const c = this.navigator.selected && this.navigator.selected.getAttribute('data-category'); return c ? `ul[data-category="${c}"] li:not(.hidden):not(.emoji-skintone), .emoji-search` : default_selector; } else { return default_selector; } }, 'onSelected': el => { el.matches('.insert-emoji') && this.setCategoryForElement(el.parentElement); el.matches('.insert-emoji, .emoji-category') && el.firstElementChild.focus(); el.matches('.emoji-search') && el.focus(); } }; this.navigator = new DOMNavigator(this, options); } } disableArrowNavigation () { this.navigator.disable(); } enableArrowNavigation (ev) { if (ev) { ev.preventDefault(); ev.stopPropagation(); } this.disableArrowNavigation(); this.navigator.enable(); this.navigator.handleKeydown(ev); } } export class EmojiDropdown extends BaseDropdown { static get properties() { return { chatview: { type: Object } }; } constructor () { super(); // This is an optimization, we lazily render the emoji picker, otherwise tests slow to a crawl. this.render_emojis = false; } initModel () { if (!this.init_promise) { this.init_promise = (async () => { await api.emojis.initialize() const id = `converse.emoji-${_converse.bare_jid}-${this.chatview.model.get('jid')}`; this.model = new _converse.EmojiPicker({'id': id}); this.model.browserStorage = _converse.createStore(id); await new Promise(resolve => this.model.fetch({'success': resolve, 'error': resolve})); })(); } return this.init_promise; } render() { return html`
`; } toggleMenu (ev) { ev.stopPropagation(); ev.preventDefault(); if (u.hasClass('show', this.menu)) { if (u.ancestor(ev.target, '.toggle-emojis')) { this.hideMenu(); } } else { this.showMenu(); } } async showMenu () { await this.init_promise; if (!this.render_emojis) { // Trigger an update so that emojis are rendered this.render_emojis = true; this.requestUpdate(); } super.showMenu(); this.querySelector('.emoji-search')?.focus(); } } api.elements.define('converse-emoji-dropdown', EmojiDropdown); api.elements.define('converse-emoji-picker', EmojiPicker);