瀏覽代碼

emoji-views: use lit-html for templating

* declare picker events in lit-html
* init intersection observer only once
* don't set value manually
* don't manually add classes
* avoid x-scrollbar and 'undefined' in search input
JC Brand 5 年之前
父節點
當前提交
d310f1e3e4
共有 7 個文件被更改,包括 154 次插入121 次删除
  1. 8 1
      sass/_emoji.scss
  2. 1 1
      spec/chatbox.js
  3. 5 5
      spec/emojis.js
  4. 35 59
      src/converse-emoji-views.js
  5. 0 1
      src/templates/chatroom_details_modal.js
  6. 105 0
      src/templates/emoji_picker.js
  7. 0 54
      src/templates/emojis.html

+ 8 - 1
sass/_emoji.scss

@@ -26,12 +26,19 @@
                 height: 8em;
                 overflow-y: auto;
                 .emoji-category__heading {
-                    cursor: auto;
+                    clear: both;
                     color: var(--subdued-color);
+                    cursor: auto;
+                    display: block;
                     font-size: var(--font-size);
                     margin: 0;
                     padding: 0.75em 0 0 0.5em;
                 }
+
+                .emoji-lists__container {
+                    overflow-x: hidden;
+                }
+
                 .emoji-picker {
                     li {
                         float: left;

+ 1 - 1
spec/chatbox.js

@@ -448,7 +448,7 @@
 
                     toolbar.querySelector('a.toggle-smiley').click();
                     const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__lists'));
-                    const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji'));
+                    const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'));
                     item.click()
                     expect(counter.textContent).toBe('179');
 

+ 5 - 5
spec/emojis.js

@@ -28,7 +28,7 @@
                 toolbar.querySelector('a.toggle-smiley').click();
                 await u.waitUntil(() => u.isVisible(view.el.querySelector('.emoji-picker__lists')));
                 const picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container'));
-                const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji'));
+                const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'));
                 item.click()
                 expect(view.el.querySelector('textarea.chat-textarea').value).toBe(':smiley: ');
                 toolbar.querySelector('a.toggle-smiley').click(); // Close the panel again
@@ -60,7 +60,7 @@
                 let picker = await u.waitUntil(() => view.el.querySelector('.emoji-picker__container'));
                 const input = picker.querySelector('.emoji-search');
                 expect(input.value).toBe(':gri');
-                let visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
+                let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', picker);
                 expect(visible_emojis.length).toBe(3);
                 expect(visible_emojis[0].getAttribute('data-emoji')).toBe(':grimacing:');
                 expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':grin:');
@@ -68,7 +68,7 @@
 
                 // Test that TAB autocompletes the to first match
                 view.emoji_picker_view.onKeyDown(tab_event);
-                visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
+                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:');
@@ -130,7 +130,7 @@
                 };
                 view.emoji_picker_view.onKeyDown(event);
                 await u.waitUntil(() => view.emoji_picker_view.model.get('query') === 'smiley');
-                let visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
+                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:');
                 expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':smiley_cat:');
@@ -144,7 +144,7 @@
                 const tab_event = Object.assign({}, event, {'keyCode': 9, 'key': 'Tab'});
                 view.emoji_picker_view.onKeyDown(tab_event);
                 expect(input.value).toBe(':smiley:');
-                visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
+                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:');
 

+ 35 - 59
src/converse-emoji-views.js

@@ -3,15 +3,15 @@
  * @copyright 2020, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
-
 import "@converse/headless/converse-emoji";
+import { HTMLView } from "skeletor.js/src/htmlview";
 import { debounce, find, get } from "lodash";
 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 tpl_emojis from "templates/emojis.html";
 
-const { Backbone, sizzle } = converse.env;
 const u = converse.env.utils;
 
 
@@ -126,15 +126,8 @@ converse.plugins.add('converse-emoji-views', {
         Object.assign(_converse.ChatBoxView.prototype, emoji_aware_chat_view);
 
 
-        _converse.EmojiPickerView = Backbone.VDOMView.extend({
-            className: 'emoji-picker',
-            events: {
-                'click .emoji-picker__header li.emoji-category .pick-category': 'chooseCategory',
-                'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone',
-                'click .insert-emoji': 'insertEmoji',
-                'focus .emoji-search': 'disableArrowNavigation',
-                'keydown .emoji-search': 'onKeyDown'
-            },
+        _converse.EmojiPickerView = HTMLView.extend({
+            className: 'emoji-picker dropdown-menu toolbar-menu',
 
             initialize (config) {
                 this.chatview = config.chatview;
@@ -144,50 +137,47 @@ converse.plugins.add('converse-emoji-views', {
                 body.addEventListener('keydown', this.onGlobalKeyDown);
 
                 this.search_results = [];
-                this.debouncedFilter = debounce(input => this.filter(input.value), 150);
-                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();
-                    const category = this.model.get('current_category');
-                    const el = this.el.querySelector(`.emoji-category[data-category="${category}"]`);
-                    this.navigator.select(el);
-                    !this.navigator.enabled && this.navigator.enable();
-                });
+                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.render();
+                this.initArrowNavigation();
+                this.initIntersectionObserver();
             },
 
             toHTML () {
-                return tpl_emojis(
+                return emoji_picker(
                     Object.assign(
                         this.model.toJSON(), {
-                            '__': __,
                             '_converse': _converse,
                             'emoji_categories': _converse.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(),
+                            'search_results': this.search_results,
                             'shouldBeHidden': shortname => this.shouldBeHidden(shortname),
-                            'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'],
                             'toned_emojis': _converse.emojis.toned,
                             'transform': u.getEmojiRenderer(),
-                            'transformCategory': shortname => u.getEmojiRenderer()(this.getTonedShortname(shortname)),
-                            'search_results': this.search_results
+                            'transformCategory': shortname => u.getEmojiRenderer()(this.getTonedShortname(shortname))
                         }
                     )
                 );
             },
 
+            onSearchInputFocus (ev) {
+                this.chatview.emitBlurred(ev);
+                this.disableArrowNavigation();
+            },
+
             remove () {
                 const body = document.querySelector('body');
                 body.removeEventListener('keydown', this.onGlobalKeyDown);
-                Backbone.VDOMView.prototype.remove.call(this);
-            },
-
-            afterRender () {
-                this.initIntersectionObserver();
-                const textarea = this.el.querySelector('.emoji-search');
-                textarea.addEventListener('focus', ev => this.chatview.emitFocused(ev));
-                textarea.addEventListener('blur', ev => this.chatview.emitBlurred(ev));
-                this.initArrowNavigation();
+                HTMLView.prototype.remove.call(this);
             },
 
             initArrowNavigation () {
@@ -242,28 +232,13 @@ converse.plugins.add('converse-emoji-views', {
                     this.search_results = _converse.emojis_list.filter(e => _converse.FILTER_CONTAINS(e.sn, value));
                 }
                 this.model.set({'query': value});
-                if (set_property) {
-                    // XXX: Ideally we would set `query` on the model and
-                    // then let the view re-render, instead of doing it
-                    // manually here. Snabbdom supports setting properties,
-                    // Backbone.VDOMView doesn't.
-                    const input = this.el.querySelector('.emoji-search');
-                    input.value = 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},
-                        {'silent': true}
-                    );
-                    const category_els = sizzle('.emoji-picker__header .emoji-category', this.el);
-                    category_els.forEach(el => u.removeClass('picked', el));
-                    const new_el = category_els.filter(el => el.getAttribute('data-category') === category).pop();
-                    new_el && u.addClass('picked', new_el);
+                    this.model.save({'current_category': category});
                 }
             },
 
@@ -341,11 +316,13 @@ converse.plugins.add('converse-emoji-views', {
 
             onKeyDown (ev) {
                 if (ev.keyCode === converse.keycodes.RIGHT_ARROW) {
-                    ev.preventDefault();
-                    ev.stopPropagation();
-                    ev.target.blur();
-                    const first_el = this.el.querySelector('.pick-category');
-                    this.navigator.select(first_el, 'right');
+                    if (this.navigator.enabled) {
+                        ev.preventDefault();
+                        ev.stopPropagation();
+                        ev.target.blur();
+                        const first_el = this.el.querySelector('.pick-category');
+                        this.navigator.select(first_el, 'right');
+                    }
                 } else if (ev.keyCode === converse.keycodes.TAB) {
                     if (ev.target.value) {
                         ev.preventDefault();
@@ -410,11 +387,10 @@ converse.plugins.add('converse-emoji-views', {
             chooseCategory (ev) {
                 ev.preventDefault && ev.preventDefault();
                 ev.stopPropagation && ev.stopPropagation();
-                const input = this.el.querySelector('.emoji-search');
-                input.value = '';
                 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();
                 this.setScrollPosition();
             },
 

+ 0 - 1
src/templates/chatroom_details_modal.js

@@ -8,7 +8,6 @@ import xss from "xss/dist/xss";
 const i18n_address =  __('Groupchat address (JID)');
 const i18n_archiving = __('Message archiving');
 const i18n_archiving_help = __('Messages are archived on the server');
-const i18n_close = __('Close');
 const i18n_desc = __('Description');
 const i18n_features = __('Features');
 const i18n_hidden = __('Hidden');

+ 105 - 0
src/templates/emoji_picker.js

@@ -0,0 +1,105 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
+import xss from "xss/dist/xss";
+
+
+const i18n_search = __('Search');
+const i18n_search_results = __('Search results');
+const skintones = ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'];
+
+
+const emoji_category = (o) => {
+    const category_emoji = xss.filterXSS(o.transformCategory(o.emoji_categories[o.category]), {'whitelist': {'img': []}});
+    return html`
+        <li data-category="${o.category}"
+            class="emoji-category ${o.current_category} ${o.category} ${(o.current_category === o.category) ? 'picked' : ''}"
+            title="${__(o._converse.emoji_category_labels[o.category])}">
+
+            <a class="pick-category"
+               @click=${o.onCategoryPicked}
+               href="#emoji-picker-${o.category}"
+               data-category="${o.category}">${unsafeHTML(category_emoji)} </a>
+        </li>
+    `;
+}
+
+const emoji_picker_header = (o) => html`
+    <ul>
+        ${ Object.keys(o.emoji_categories).map(category => (o.emoji_categories[category] ? emoji_category(Object.assign({category}, o)) : '')) }
+    </ul>
+`;
+
+
+const emoji_item = (o) => {
+    let emoji;
+    if (o._converse.use_system_emojis) {
+        emoji = o.transform(o.emoji.sn);
+    } else {
+        emoji = unsafeHTML(xss.filterXSS(o.transform(o.emoji.sn), {'whitelist': {'img': []}}));
+    }
+    return html`
+        <li class="emoji insert-emoji ${o.shouldBeHidden(o.emoji.sn) ? 'hidden' : ''}" data-emoji="${o.emoji.sn}" title="${o.emoji.sn}">
+            <a href="#" @click=${o.onEmojiPicked} data-emoji="${o.emoji.sn}">${emoji}</a>
+        </li>
+    `;
+}
+
+const search_results = (o) => html`
+    <span ?hidden=${!o.query} class="emoji-lists__container emojis-lists__container--search">
+    <a id="emoji-picker-search-results" class="emoji-category__heading">${i18n_search_results}</a>
+    <ul class="emoji-picker">
+        ${ o.search_results.map(emoji => emoji_item(Object.assign({emoji}, o))) }
+    </ul>
+    </span>
+`;
+
+const emojis_for_category = (o) => html`
+    <a id="emoji-picker-${o.category}" class="emoji-category__heading" data-category="${o.category}">${ __(o._converse.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>
+`;
+
+
+const skintone_emoji = (o) => {
+    const shortname = ':'+o.skintone+':';
+    let emoji;
+    if (o._converse.use_system_emojis) {
+        emoji = o.transform(shortname);
+    } else {
+        emoji = unsafeHTML(xss.filterXSS(o.transform(shortname), {'whitelist': {'img': []}}));
+    }
+    return html`
+        <li data-skintone="${o.skintone}" class="emoji-skintone ${(o.current_skintone === o.skintone) ? 'picked' : ''}">
+            <a class="pick-skintone" href="#" data-skintone="${o.skintone}" @click=${o.onSkintonePicked}>${emoji}</a>
+        </li>
+    `;
+}
+
+
+const all_emojis = (o) => html`
+    <span ?hidden=${o.query} class="emoji-lists__container">
+        ${Object.keys(o.emoji_categories).map(category => (o.emoji_categories[category] ? emojis_for_category(Object.assign({category}, o)) : ''))}
+    </span>
+`;
+
+
+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>
+`;

+ 0 - 54
src/templates/emojis.html

@@ -1,54 +0,0 @@
-<div class="emoji-picker dropdown-menu toolbar-menu">
-    <div class="emoji-picker__header">
-        <input class="form-control emoji-search" name="emoji-search" placeholder="{{{o.__('Search')}}}"/>
-        {[ if (!o.query) { ]}
-        <ul>
-            {[ Object.keys(o.emoji_categories).forEach(function (category) { ]}
-                {[ if (o.emoji_categories[category]) { ]}
-                <li data-category="{{{category}}}" class="emoji-category {{{o.current_category}}} {{{ category}}} {[ if (o.current_category === category) { ]} picked {[ } ]}"
-                    title="{{{ o.__(o._converse.emoji_category_labels[category]) }}}">
-                    <a class="pick-category" href="#emoji-picker-{{{category}}}" data-category="{{{category}}}"> {{ o.transformCategory(o.emoji_categories[category]) }} </a>
-                </li>
-                {[ } ]}
-            {[ }); ]}
-        </ul>
-        {[ } ]}
-    </div>
-    <div class="emoji-picker__lists">
-        {[ if (o.query) { ]}
-            <a id="emoji-picker-search-results" class="emoji-category__heading">{{{o.__('Search results')}}}</a>
-            <ul class="emoji-picker">
-                {[ o.search_results.forEach(function (emoji) { ]}
-                <li class="emoji insert-emoji {[ if (o.shouldBeHidden(emoji.sn)) { ]} hidden {[ }; ]}"
-                    data-emoji="{{{emoji.sn}}}" title="{{{emoji.sn}}}">
-                        <a href="#" data-emoji="{{{emoji.sn}}}"> {{ o.transform(emoji.sn) }}  </a>
-                </li>
-                {[ }); ]}
-            </ul>
-        {[ } else { ]}
-            {[ Object.keys(o.emoji_categories).forEach(function (category) { ]}
-                {[ if (o.emoji_categories[category]) { ]}
-                    <a id="emoji-picker-{{{category}}}" class="emoji-category__heading" data-category="{{{category}}}">{{{ o.__(o._converse.emoji_category_labels[category]) }}} </a>
-                    <ul class="emoji-picker" data-category="{{{category}}}">
-                        {[ Object.values(o.emojis_by_category[category]).forEach(function (emoji) { ]}
-                        <li class="emoji insert-emoji {[ if (o.shouldBeHidden(emoji.sn)) { ]} hidden {[ }; ]}"
-                            data-emoji="{{{emoji.sn}}}" title="{{{emoji.sn}}}">
-                                <a href="#" data-emoji="{{{emoji.sn}}}"> {{ o.transform(emoji.sn) }}  </a>
-                        </li>
-                        {[ }); ]}
-                    </ul>
-                {[ } ]}
-            {[ }); ]}
-        {[ } ]}
-    </div>
-    <div class="emoji-skintone-picker">
-        <label>Skin tone</label>
-        <ul>
-            {[ o.skintones.forEach(function (skintone) { ]}
-                <li data-skintone="{{{skintone}}}" class="emoji-skintone {[ if (o.current_skintone === skintone) { ]} picked {[ } ]}">
-                    <a class="pick-skintone" href="#" data-skintone="{{{skintone}}}"> {{ o.transform(':'+skintone+':') }} </a>
-                </li>
-            {[ }); ]}
-        </ul>
-    </div>
-</div>