Переглянути джерело

Turn the emoji picker into a web component

JC Brand 5 роки тому
батько
коміт
b3e34a0636
4 змінених файлів з 368 додано та 324 видалено
  1. 16 11
      spec/emojis.js
  2. 305 0
      src/components/emoji-picker.js
  3. 18 292
      src/converse-emoji-views.js
  4. 29 21
      src/templates/emoji_picker.js

+ 16 - 11
spec/emojis.js

@@ -64,16 +64,18 @@ describe("Emojis", function () {
             expect(visible_emojis[2].getAttribute('data-emoji')).toBe(':grinning:');
 
             // Test that TAB autocompletes the to first match
-            view.emoji_picker_view.onKeyDown(tab_event);
+            input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
+
+            await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji', picker).length === 1);
             visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker);
-            expect(visible_emojis.length).toBe(1);
             expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':grimacing:');
             expect(input.value).toBe(':grimacing:');
 
             // Check that ENTER now inserts the match
             const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input});
-            view.emoji_picker_view.onKeyDown(enter_event);
-            expect(input.value).toBe('');
+            input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
+
+            await u.waitUntil(() => input.value === '');
             expect(textarea.value).toBe(':grimacing: ');
 
             // Test that username starting with : doesn't cause issues
@@ -124,8 +126,9 @@ describe("Emojis", function () {
                 'preventDefault': function preventDefault () {},
                 'stopPropagation': function stopPropagation () {}
             };
-            view.emoji_picker_view.onKeyDown(event);
-            await u.waitUntil(() => view.emoji_picker_view.model.get('query') === 'smiley');
+            input.dispatchEvent(new KeyboardEvent('keydown', event));
+
+            await u.waitUntil(() => view.emoji_picker_view.model.get('query') === 'smiley', 1000);
             let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker);
             expect(visible_emojis.length).toBe(2);
             expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:');
@@ -133,26 +136,28 @@ describe("Emojis", function () {
 
             // Check that pressing enter without an unambiguous match does nothing
             const enter_event = Object.assign({}, event, {'keyCode': 13});
-            view.emoji_picker_view.onKeyDown(enter_event);
+            input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
             expect(input.value).toBe('smiley');
 
             // Test that TAB autocompletes the to first match
             const tab_event = Object.assign({}, event, {'keyCode': 9, 'key': 'Tab'});
-            view.emoji_picker_view.onKeyDown(tab_event);
-            expect(input.value).toBe(':smiley:');
+            input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
+
+            await u.waitUntil(() => input.value === ':smiley:');
             visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker);
             expect(visible_emojis.length).toBe(1);
             expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':smiley:');
 
             // Check that ENTER now inserts the match
-            view.emoji_picker_view.onKeyDown(enter_event);
-            expect(input.value).toBe('');
+            input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
+            await u.waitUntil(() => input.value === '');
             expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
             done();
         }));
     });
 
     describe("A Chat Message", function () {
+
         it("will display larger if it's only emojis",
             mock.initConverse(
                 ['rosterGroupsFetched', 'chatBoxesFetched'], {'use_system_emojis': true},

+ 305 - 0
src/components/emoji-picker.js

@@ -0,0 +1,305 @@
+import DOMNavigator from "../dom-navigator";
+import sizzle from 'sizzle';
+import tpl_emoji_picker from "../templates/emoji_picker.js";
+import { CustomElement } from './element.js';
+import { _converse, converse } from "@converse/headless/converse-core";
+import { debounce, find } from "lodash-es";
+
+const u = converse.env.utils;
+
+
+export class EmojiPicker extends CustomElement {
+
+    static get properties () {
+        return {
+            'chatview': { type: Object },
+            'current_category': { type: String },
+            'current_skintone': { type: String },
+            'model': { type: Object },
+            'query': { type: String },
+        }
+    }
+
+    constructor () {
+        super();
+        this.debouncedFilter = debounce(input => this.model.set({'query': input.value}), 500);
+        this.preserve_scroll = false;
+        this._search_results = [];
+        this.onGlobalKeyDown = ev => this._onGlobalKeyDown(ev);
+        const body = document.querySelector('body');
+        body.addEventListener('keydown', this.onGlobalKeyDown);
+    }
+
+    render () {
+        return tpl_emoji_picker({
+            'current_category': this.current_category,
+            'current_skintone': this.current_skintone,
+            'onCategoryPicked': ev => this.chooseCategory(ev),
+            'onEmojiPicked': ev => this.insertEmoji(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,
+            'shouldBeHidden': shortname => this.shouldBeHidden(shortname),
+            'transformCategory': shortname => u.getEmojiRenderer()(this.getTonedShortname(shortname))
+        });
+    }
+
+    firstUpdated () {
+        this.initArrowNavigation();
+        this.initIntersectionObserver();
+    }
+
+    updated (changed) {
+        if (changed.has('current_category') && !this.preserve_scroll) {
+            this.setScrollPosition();
+        }
+    }
+
+    get search_results () {
+        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 {
+            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, preserve_scroll=false) {
+        const old_category = this.current_category;
+        const category = el.getAttribute('data-category') || old_category;
+        if (old_category !== category) {
+            this.preserve_scroll = preserve_scroll;
+            this.model.save({'current_category': category});
+        }
+    }
+
+    setCategoryOnVisibilityChange (ev) {
+        const selected = this.navigator.selected;
+        const intersection_with_selected = ev.filter(i => i.target.contains(selected)).pop();
+        let current;
+        // Choose the intersection that contains the currently selected
+        // element, or otherwise the one with the largest ratio.
+        if (intersection_with_selected) {
+            current = intersection_with_selected;
+        } else {
+            current = ev.reduce((p, c) => c.intersectionRatio >= (p?.intersectionRatio || 0) ? c : p, null);
+        }
+        current && current.isIntersecting && this.setCategoryForElement(current.target, true);
+    }
+
+    insertIntoTextArea (value) {
+        const replace = this.model.get('autocompleting');
+        const position = this.model.get('position');
+        this.model.set({'autocompleting': null, 'position': null});
+        this.chatview.insertIntoTextArea(value, replace, false, position);
+        if (this.chatview.emoji_dropdown) {
+            this.chatview.emoji_dropdown.toggle();
+        }
+        this.model.set({'query': ''});
+        this.disableArrowNavigation();
+    }
+
+    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();
+    }
+
+    insertEmoji (ev) {
+        ev.preventDefault();
+        ev.stopPropagation();
+        const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
+        const replace = this.model.get('autocompleting');
+        const position = this.model.get('position');
+        this.model.set({'autocompleting': null, 'position': null});
+        this.chatview.insertIntoTextArea(target.getAttribute('data-emoji'), replace, false, position);
+        this.chatview.emoji_dropdown.toggle();
+        this.model.set({'query': ''});
+    }
+
+    onKeyDown (ev) {
+        if (ev.keyCode === converse.keycodes.TAB) {
+            if (ev.target.value) {
+                ev.preventDefault();
+                const match = find(_converse.emoji_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.emoji_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();
+    }
+
+    shouldBeHidden (shortname) {
+        // Helper method for the template which decides whether an
+        // emoji should be hidden, based on which skin tone is
+        // currently being applied.
+        if (shortname.includes('_tone')) {
+            if (!this.current_skintone || !shortname.includes(this.current_skintone)) {
+                return true;
+            }
+        } else {
+            if (this.current_skintone && _converse.emojis.toned.includes(shortname)) {
+                return true;
+            }
+        }
+        if (this.query && !_converse.FILTER_CONTAINS(shortname, this.query)) {
+            return true;
+        }
+        return false;
+    }
+
+    getTonedShortname (shortname) {
+        if (_converse.emojis.toned.includes(shortname) && this.current_skintone) {
+            return `${shortname.slice(0, shortname.length-1)}_${this.current_skintone}:`
+        }
+        return shortname;
+    }
+
+    initIntersectionObserver () {
+        if (!window.IntersectionObserver) {
+            return;
+        }
+        if (this.observer) {
+            this.observer.disconnect();
+        } else {
+            const options = {
+                root: this.querySelector('.emoji-picker__lists'),
+                threshold: [0.1]
+            }
+            const handler = ev => this.setCategoryOnVisibilityChange(ev);
+            this.observer = new IntersectionObserver(handler, options);
+        }
+        sizzle('.emoji-picker', this).forEach(a => this.observer.observe(a));
+    }
+
+    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);
+    }
+
+    setScrollPosition () {
+        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;
+        }
+    }
+}
+
+window.customElements.define('converse-emoji-picker', EmojiPicker);

+ 18 - 292
src/converse-emoji-views.js

@@ -3,16 +3,14 @@
  * @copyright 2020, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
+import "./components/emoji-picker.js";
 import "@converse/headless/converse-emoji";
-import DOMNavigator from "./dom-navigator";
 import bootstrap from "bootstrap.native";
-import emoji_picker from "templates/emoji_picker.js";
-import sizzle from 'sizzle';
 import tpl_emoji_button from "templates/emoji_button.html";
 import { View } from "@converse/skeletor/src/view";
 import { __ } from '@converse/headless/i18n';
 import { _converse, api, converse } from '@converse/headless/converse-core';
-import { debounce, find } from "lodash-es";
+import { html } from "lit-html";
 
 const u = converse.env.utils;
 
@@ -82,25 +80,20 @@ converse.plugins.add('converse-emoji-views', {
             async autocompleteInPicker (input, value) {
                 await this.createEmojiDropdown();
                 this.emoji_picker_view.model.set({
+                    'query': value,
                     'autocompleting': value,
                     'position': input.selectionStart
-                }, {'silent': true});
-                this.emoji_picker_view.filter(value);
+                });
                 this.emoji_dropdown.toggle();
             },
 
             async createEmojiPicker () {
                 await api.emojis.initialize()
-
                 const id = `converse.emoji-${_converse.bare_jid}-${this.model.get('jid')}`;
                 const emojipicker = new _converse.EmojiPicker({'id': id});
                 emojipicker.browserStorage = _converse.createStore(id);
-                await new Promise(resolve => emojipicker.fetch({ 'success': resolve, 'error': resolve}));
-
-                this.emoji_picker_view = new _converse.EmojiPickerView({
-                    'model': emojipicker,
-                    'chatview': this
-                });
+                await new Promise(resolve => emojipicker.fetch({'success': resolve, 'error': resolve}));
+                this.emoji_picker_view = new _converse.EmojiPickerView({'model': emojipicker, 'chatview': this});
                 const el = this.el.querySelector('.emoji-picker__container');
                 el.innerHTML = '';
                 el.appendChild(this.emoji_picker_view.el);
@@ -119,7 +112,6 @@ converse.plugins.add('converse-emoji-views', {
                 ev.stopPropagation();
                 await this.createEmojiDropdown();
                 this.emoji_dropdown.toggle();
-                this.emoji_picker_view.setScrollPosition();
             }
         };
         Object.assign(_converse.ChatBoxView.prototype, emoji_aware_chat_view);
@@ -130,288 +122,22 @@ converse.plugins.add('converse-emoji-views', {
 
             initialize (config) {
                 this.chatview = config.chatview;
-                this.onGlobalKeyDown = ev => this._onGlobalKeyDown(ev);
-
-                const body = document.querySelector('body');
-                body.addEventListener('keydown', this.onGlobalKeyDown);
-
-                this.search_results = [];
-                this.debouncedFilter = debounce(input => this.filter(input.value), 250);
-                this.listenTo(this.model, 'change:query', this.render);
-                this.listenTo(this.model, 'change:current_skintone', this.render);
-                this.listenTo(this.model, 'change:current_category', this.render);
+                this.listenTo(this.model, 'change', o => {
+                    if (['current_category', 'current_skintone', 'query'].some(k => k in o.changed)) {
+                        this.render();
+                    }
+                });
                 this.render();
-                this.initArrowNavigation();
-                this.initIntersectionObserver();
             },
 
             toHTML () {
-                return emoji_picker(
-                    Object.assign(
-                        this.model.toJSON(), {
-                            '_converse': _converse,
-                            'emoji_categories': api.settings.get('emoji_categories'),
-                            'emojis_by_category': _converse.emojis.json,
-                            'onSkintonePicked': ev => this.chooseSkinTone(ev),
-                            'onEmojiPicked': ev => this.insertEmoji(ev),
-                            'onCategoryPicked': ev => this.chooseCategory(ev),
-                            'onSearchInputBlurred': ev => this.chatview.emitFocused(ev),
-                            'onSearchInputKeyDown': ev => this.onKeyDown(ev),
-                            'onSearchInputFocus': ev => this.onSearchInputFocus(ev),
-                            'search_results': this.search_results,
-                            'shouldBeHidden': shortname => this.shouldBeHidden(shortname),
-                            'toned_emojis': _converse.emojis.toned,
-                            'transform': u.getEmojiRenderer(),
-                            'transformCategory': shortname => u.getEmojiRenderer()(this.getTonedShortname(shortname))
-                        }
-                    )
-                );
-            },
-
-            afterRender () {
-                this.setScrollPosition();
-            },
-
-            onSearchInputFocus (ev) {
-                this.chatview.emitBlurred(ev);
-                this.disableArrowNavigation();
-            },
-
-            remove () {
-                const body = document.querySelector('body');
-                body.removeEventListener('keydown', this.onGlobalKeyDown);
-                View.prototype.remove.call(this);
-            },
-
-            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.el.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.el, options);
-                    this.listenTo(this.chatview.model, 'destroy', () => this.navigator.destroy());
-                }
-            },
-
-            enableArrowNavigation (ev) {
-                if (ev) {
-                    ev.preventDefault();
-                    ev.stopPropagation();
-                }
-                this.disableArrowNavigation();
-                this.navigator.enable();
-                this.navigator.handleKeydown(ev);
-            },
-
-            disableArrowNavigation () {
-                this.navigator.disable();
-            },
-
-            filter (value) {
-                const old_query = this.model.get('query');
-                if (!value) {
-                    this.search_results = [];
-                } else if (old_query && value.includes(old_query)) {
-                    this.search_results = this.search_results.filter(e => _converse.FILTER_CONTAINS(e.sn, value));
-                } else {
-                    this.search_results = _converse.emojis_list.filter(e => _converse.FILTER_CONTAINS(e.sn, value));
-                }
-                this.model.set({'query': value});
-            },
-
-            setCategoryForElement (el) {
-                const category = el.getAttribute('data-category');
-                const old_category = this.model.get('current_category');
-                if (old_category !== category) {
-                    this.model.save({'current_category': category});
-                }
-            },
-
-            setCategoryOnVisibilityChange (ev) {
-                const selected = this.navigator.selected;
-                const intersection_with_selected = ev.filter(i => i.target.contains(selected)).pop();
-                let current;
-                // Choose the intersection that contains the currently selected
-                // element, or otherwise the one with the largest ratio.
-                if (intersection_with_selected) {
-                    current = intersection_with_selected;
-                } else {
-                    current = ev.reduce((p, c) => c.intersectionRatio >= (p?.intersectionRatio || 0) ? c : p, null);
-                }
-                current && current.isIntersecting && this.setCategoryForElement(current.target);
-            },
-
-            initIntersectionObserver () {
-                if (!window.IntersectionObserver) {
-                    return;
-                }
-                if (this.observer) {
-                    this.observer.disconnect();
-                } else {
-                    const options = {
-                        root: this.el.querySelector('.emoji-picker__lists'),
-                        threshold: [0.1]
-                    }
-                    const handler = ev => this.setCategoryOnVisibilityChange(ev);
-                    this.observer = new IntersectionObserver(handler, options);
-                }
-                sizzle('.emoji-picker', this.el).forEach(a => this.observer.observe(a));
-            },
-
-            insertIntoTextArea (value) {
-                const replace = this.model.get('autocompleting');
-                const position = this.model.get('position');
-                this.model.set({'autocompleting': null, 'position': null});
-                this.chatview.insertIntoTextArea(value, replace, false, position);
-                if (this.chatview.emoji_dropdown) {
-                    this.chatview.emoji_dropdown.toggle();
-                }
-                this.filter('');
-                this.disableArrowNavigation();
-            },
-
-            onEnterPressed (ev) {
-                ev.preventDefault();
-                ev.stopPropagation();
-                if (_converse.emoji_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});
-                }
-            },
-
-            _onGlobalKeyDown (ev) {
-                if (!this.navigator) {
-                    return;
-                }
-                if (ev.keyCode === converse.keycodes.ENTER &&
-                        this.navigator.selected &&
-                        u.isVisible(this.el)) {
-                    this.onEnterPressed(ev);
-                } else if (ev.keyCode === converse.keycodes.DOWN_ARROW &&
-                        !this.navigator.enabled &&
-                        u.isVisible(this.el)) {
-                    this.enableArrowNavigation(ev);
-                }
-            },
-
-            onKeyDown (ev) {
-                if (ev.keyCode === converse.keycodes.TAB) {
-                    if (ev.target.value) {
-                        ev.preventDefault();
-                        const match = find(_converse.emoji_shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
-                        match && this.filter(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);
-                }
-            },
-
-            shouldBeHidden (shortname) {
-                // Helper method for the template which decides whether an
-                // emoji should be hidden, based on which skin tone is
-                // currently being applied.
-                const current_skintone = this.model.get('current_skintone');
-                if (shortname.includes('_tone')) {
-                    if (!current_skintone || !shortname.includes(current_skintone)) {
-                        return true;
-                    }
-                } else {
-                    if (current_skintone && _converse.emojis.toned.includes(shortname)) {
-                        return true;
-                    }
-                }
-                const query = this.model.get('query');
-                if (query && !_converse.FILTER_CONTAINS(shortname, query)) {
-                    return true;
-                }
-                return false;
-            },
-
-            getTonedShortname (shortname) {
-                if (_converse.emojis.toned.includes(shortname) && this.model.get('current_skintone')) {
-                    return `${shortname.slice(0, shortname.length-1)}_${this.model.get('current_skintone')}:`
-                }
-                return shortname;
-            },
-
-            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.model.get('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();
-            },
-
-            setScrollPosition () {
-                const category = this.model.get('current_category');
-                const el = this.el.querySelector('.emoji-lists__container--browse');
-                const heading = this.el.querySelector(`#emoji-picker-${category}`);
-                if (heading) {
-                    // +4 due to 2px padding on list elements
-                    el.scrollTop = heading.offsetTop - heading.offsetHeight*3 + 4;
-                }
-            },
-
-            insertEmoji (ev) {
-                ev.preventDefault();
-                ev.stopPropagation();
-                const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
-                const replace = this.model.get('autocompleting');
-                const position = this.model.get('position');
-                this.model.set({'autocompleting': null, 'position': null});
-                this.chatview.insertIntoTextArea(target.getAttribute('data-emoji'), replace, false, position);
-                this.chatview.emoji_dropdown.toggle();
-                this.filter('');
+                return html`<converse-emoji-picker
+                        .chatview=${this.chatview}
+                        .model=${this.model}
+                        current_category="${this.model.get('current_category') || ''}"
+                        current_skintone="${this.model.get('current_skintone') || ''}"
+                        query="${this.model.get('query') || ''}"
+                    ></converse-emoji-picker>`;
             }
         });
 

+ 29 - 21
src/templates/emoji_picker.js

@@ -1,6 +1,8 @@
-import { html } from "lit-html";
 import { __ } from '@converse/headless/i18n';
+import { _converse, api } from "@converse/headless/converse-core";
+import { html } from "lit-html";
 
+const u = converse.env.utils;
 
 const i18n_search = __('Search');
 const i18n_search_results = __('Search results');
@@ -11,7 +13,7 @@ const emoji_category = (o) => {
     return html`
         <li data-category="${o.category}"
             class="emoji-category ${o.category} ${(o.current_category === o.category) ? 'picked' : ''}"
-            title="${__(o._converse.emoji_category_labels[o.category])}">
+            title="${__(_converse.emoji_category_labels[o.category])}">
 
             <a class="pick-category"
                @click=${o.onCategoryPicked}
@@ -45,7 +47,7 @@ const search_results = (o) => html`
 `;
 
 const emojis_for_category = (o) => html`
-    <a id="emoji-picker-${o.category}" class="emoji-category__heading" data-category="${o.category}">${ __(o._converse.api.settings.get('emoji_category_labels')[o.category]) }</a>
+    <a id="emoji-picker-${o.category}" class="emoji-category__heading" data-category="${o.category}">${ __(api.settings.get('emoji_category_labels')[o.category]) }</a>
     <ul class="emoji-picker" data-category="${o.category}">
         ${ Object.values(o.emojis_by_category[o.category]).map(emoji => emoji_item(Object.assign({emoji}, o))) }
     </ul>
@@ -68,21 +70,27 @@ const all_emojis = (o) => html`
 `;
 
 
-export default (o) => html`
-    <div class="emoji-picker__header">
-        <input class="form-control emoji-search" name="emoji-search" placeholder="${i18n_search}"
-               .value=${o.query || ''}
-               @keydown=${o.onSearchInputKeyDown}
-               @blur=${o.onSearchInputBlurred}
-               @focus=${o.onSearchInputFocus}>
-        ${ o.query ? '' : emoji_picker_header(o) }
-    </div>
-    <div class="emoji-picker__lists">
-        ${search_results(o)}
-        ${all_emojis(o)}
-    </div>
-    <div class="emoji-skintone-picker">
-        <label>Skin tone</label>
-        <ul>${ skintones.map(skintone => skintone_emoji(Object.assign({skintone}, o))) }</ul>
-    </div>
-`;
+export default (o) => {
+    o.emoji_categories = api.settings.get('emoji_categories');
+    o.emojis_by_category = _converse.emojis.json;
+    o.transform = u.getEmojiRenderer();
+    o.toned_emojis = _converse.emojis.toned;
+
+    return html`
+        <div class="emoji-picker__header">
+            <input class="form-control emoji-search" name="emoji-search" placeholder="${i18n_search}"
+                .value=${o.query || ''}
+                @keydown=${o.onSearchInputKeyDown}
+                @blur=${o.onSearchInputBlurred}
+                @focus=${o.onSearchInputFocus}>
+            ${ o.query ? '' : emoji_picker_header(o) }
+        </div>
+        <div class="emoji-picker__lists">
+            ${search_results(o)}
+            ${all_emojis(o)}
+        </div>
+        <div class="emoji-skintone-picker">
+            <label>Skin tone</label>
+            <ul>${ skintones.map(skintone => skintone_emoji(Object.assign({skintone}, o))) }</ul>
+        </div>`;
+}