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
This commit is contained in:
parent
11e219dd41
commit
d310f1e3e4
@ -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;
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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:');
|
||||
|
||||
|
@ -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) {
|
||||
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();
|
||||
},
|
||||
|
||||
|
@ -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
src/templates/emoji_picker.js
Normal file
105
src/templates/emoji_picker.js
Normal file
@ -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>
|
||||
`;
|
@ -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>
|
Loading…
Reference in New Issue
Block a user