2020-07-01 21:45:18 +02:00
|
|
|
import "./emoji-picker-content.js";
|
2020-06-23 11:44:22 +02:00
|
|
|
import DOMNavigator from "../dom-navigator";
|
2020-06-08 12:12:49 +02:00
|
|
|
import { BaseDropdown } from "./dropdown.js";
|
2020-06-23 11:44:22 +02:00
|
|
|
import { CustomElement } from './element.js';
|
2020-09-10 07:33:00 +02:00
|
|
|
import { __ } from '../i18n';
|
2020-07-01 21:45:18 +02:00
|
|
|
import { _converse, api, converse } from "@converse/headless/converse-core";
|
2020-06-23 11:44:22 +02:00
|
|
|
import { debounce, find } from "lodash-es";
|
2020-06-08 12:12:49 +02:00
|
|
|
import { html } from "lit-element";
|
2020-07-01 21:45:18 +02:00
|
|
|
import { tpl_emoji_picker } from "../templates/emoji_picker.js";
|
2020-06-08 12:12:49 +02:00
|
|
|
import { until } from 'lit-html/directives/until.js';
|
2020-06-23 11:44:22 +02:00
|
|
|
|
|
|
|
const u = converse.env.utils;
|
|
|
|
|
|
|
|
|
2020-07-01 21:45:18 +02:00
|
|
|
export default class EmojiPicker extends CustomElement {
|
2020-06-23 11:44:22 +02:00
|
|
|
|
|
|
|
static get properties () {
|
|
|
|
return {
|
|
|
|
'chatview': { type: Object },
|
2020-06-08 12:12:49 +02:00
|
|
|
'current_category': { type: String, 'reflect': true },
|
|
|
|
'current_skintone': { type: String, 'reflect': true },
|
2020-06-23 11:44:22 +02:00
|
|
|
'model': { type: Object },
|
2020-06-08 12:12:49 +02:00
|
|
|
'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 },
|
2020-06-23 11:44:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-08 12:12:49 +02:00
|
|
|
firstUpdated () {
|
|
|
|
this.listenTo(this.model, 'change', o => this.onModelChanged(o.changed));
|
|
|
|
this.initArrowNavigation();
|
|
|
|
}
|
|
|
|
|
2020-06-23 11:44:22 +02:00
|
|
|
constructor () {
|
|
|
|
super();
|
2020-07-13 09:55:01 +02:00
|
|
|
this._search_results = [];
|
2020-06-25 11:18:13 +02:00
|
|
|
this.debouncedFilter = debounce(input => this.model.set({'query': input.value}), 250);
|
2020-08-14 15:07:15 +02:00
|
|
|
this.registerEvents();
|
2020-06-23 11:44:22 +02:00
|
|
|
}
|
|
|
|
|
2020-07-13 09:55:01 +02:00
|
|
|
get search_results () {
|
|
|
|
return this._search_results;
|
|
|
|
}
|
|
|
|
|
|
|
|
set search_results (value) {
|
|
|
|
this._search_results = value;
|
|
|
|
this.requestUpdate();
|
|
|
|
}
|
|
|
|
|
2020-06-23 11:44:22 +02:00
|
|
|
render () {
|
|
|
|
return tpl_emoji_picker({
|
2020-06-25 11:18:13 +02:00
|
|
|
'chatview': this.chatview,
|
2020-06-23 11:44:22 +02:00
|
|
|
'current_category': this.current_category,
|
|
|
|
'current_skintone': this.current_skintone,
|
2020-06-25 11:18:13 +02:00
|
|
|
'model': this.model,
|
2020-06-23 11:44:22 +02:00
|
|
|
'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,
|
2020-06-08 12:12:49 +02:00
|
|
|
'render_emojis': this.render_emojis,
|
2020-06-25 11:18:13 +02:00
|
|
|
'sn2Emoji': shortname => u.shortnamesToEmojis(this.getTonedShortname(shortname))
|
2020-06-23 11:44:22 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
updated (changed) {
|
2020-07-13 09:55:01 +02:00
|
|
|
changed.has('query') && this.updateSearchResults(changed);
|
2020-06-25 11:18:13 +02:00
|
|
|
changed.has('current_category') && this.setScrollPosition();
|
|
|
|
}
|
|
|
|
|
2020-06-08 12:12:49 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-06-25 11:18:13 +02:00
|
|
|
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;
|
2020-06-23 11:44:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-13 09:55:01 +02:00
|
|
|
updateSearchResults (changed) {
|
|
|
|
const old_query = changed.get('query');
|
2020-06-23 11:44:22 +02:00
|
|
|
const contains = _converse.FILTER_CONTAINS;
|
|
|
|
if (this.query) {
|
2020-07-13 09:55:01 +02:00
|
|
|
if (this.query === old_query) {
|
2020-06-25 11:18:13 +02:00
|
|
|
return this.search_results;
|
2020-07-13 09:55:01 +02:00
|
|
|
} else if (old_query && this.query.includes(old_query)) {
|
2020-06-25 11:18:13 +02:00
|
|
|
this.search_results = this.search_results.filter(e => contains(e.sn, this.query));
|
2020-06-23 11:44:22 +02:00
|
|
|
} else {
|
2020-06-08 12:12:49 +02:00
|
|
|
this.search_results = converse.emojis.list.filter(e => contains(e.sn, this.query));
|
2020-06-23 11:44:22 +02:00
|
|
|
}
|
2020-06-25 11:18:13 +02:00
|
|
|
} 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 = [];
|
2020-06-23 11:44:22 +02:00
|
|
|
}
|
2020-07-13 09:55:01 +02:00
|
|
|
this.requestUpdate();
|
2020-06-23 11:44:22 +02:00
|
|
|
}
|
|
|
|
|
2020-08-14 15:07:15 +02:00
|
|
|
registerEvents () {
|
|
|
|
this.onGlobalKeyDown = ev => this._onGlobalKeyDown(ev);
|
|
|
|
const body = document.querySelector('body');
|
|
|
|
body.addEventListener('keydown', this.onGlobalKeyDown);
|
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback () {
|
|
|
|
super.connectedCallback();
|
|
|
|
this.registerEvents();
|
|
|
|
}
|
|
|
|
|
2020-06-23 11:44:22 +02:00
|
|
|
disconnectedCallback() {
|
|
|
|
const body = document.querySelector('body');
|
|
|
|
body.removeEventListener('keydown', this.onGlobalKeyDown);
|
2020-08-14 15:07:15 +02:00
|
|
|
super.disconnectedCallback()
|
2020-06-23 11:44:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
_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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-25 11:18:13 +02:00
|
|
|
setCategoryForElement (el) {
|
2020-06-23 11:44:22 +02:00
|
|
|
const old_category = this.current_category;
|
2020-06-08 12:12:49 +02:00
|
|
|
const category = el?.getAttribute('data-category') || old_category;
|
2020-06-23 11:44:22 +02:00
|
|
|
if (old_category !== category) {
|
|
|
|
this.model.save({'current_category': category});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
insertIntoTextArea (value) {
|
2020-06-08 12:12:49 +02:00
|
|
|
this.chatview.onEmojiReceivedFromPicker(value);
|
2020-07-13 10:10:58 +02:00
|
|
|
this.model.set({'autocompleting': null, 'query': '', 'ac_position': null});
|
2020-06-23 11:44:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2020-06-08 12:12:49 +02:00
|
|
|
const match = find(converse.emojis.shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
|
2020-06-23 11:44:22 +02:00
|
|
|
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) {
|
2020-07-09 21:14:13 +02:00
|
|
|
u.ancestor(this, 'converse-emoji-dropdown').hideMenu();
|
2020-06-23 11:44:22 +02:00
|
|
|
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) {
|
2020-09-17 13:50:52 +02:00
|
|
|
if (ev.emoji_keypress_handled) {
|
|
|
|
// Prevent the emoji from being inserted a 2nd time due to this
|
|
|
|
// method being called by two event handlers: onKeyDown and _onGlobalKeyDown
|
|
|
|
return;
|
|
|
|
}
|
2020-06-23 11:44:22 +02:00
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
2020-09-17 13:50:52 +02:00
|
|
|
ev.emoji_keypress_handled = true;
|
2020-06-08 12:12:49 +02:00
|
|
|
if (converse.emojis.shortnames.includes(ev.target.value)) {
|
2020-06-23 11:44:22 +02:00
|
|
|
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) {
|
2020-06-08 12:12:49 +02:00
|
|
|
if (converse.emojis.toned.includes(shortname) && this.current_skintone) {
|
2020-06-23 11:44:22 +02:00
|
|
|
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) {
|
2020-07-09 21:14:13 +02:00
|
|
|
ev?.preventDefault?.();
|
|
|
|
ev?.stopPropagation?.();
|
2020-06-23 11:44:22 +02:00
|
|
|
this.disableArrowNavigation();
|
|
|
|
this.navigator.enable();
|
|
|
|
this.navigator.handleKeydown(ev);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-25 11:18:13 +02:00
|
|
|
|
2020-06-08 12:12:49 +02:00
|
|
|
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}));
|
2020-07-13 10:10:58 +02:00
|
|
|
// We never want still be in the autocompleting state upon page load
|
|
|
|
this.model.set({'autocompleting': null, 'ac_position': null});
|
2020-06-08 12:12:49 +02:00
|
|
|
})();
|
|
|
|
}
|
|
|
|
return this.init_promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
return html`
|
|
|
|
<div class="dropup">
|
|
|
|
<button class="toggle-emojis"
|
|
|
|
title="${__('Insert emojis')}"
|
|
|
|
data-toggle="dropdown"
|
|
|
|
aria-haspopup="true"
|
|
|
|
aria-expanded="false">
|
2020-07-09 21:21:17 +02:00
|
|
|
<converse-icon
|
|
|
|
class="fa fa-smile "
|
|
|
|
path-prefix="${api.settings.get('assets_path')}"
|
|
|
|
size="1em"></converse-icon>
|
2020-06-08 12:12:49 +02:00
|
|
|
</button>
|
|
|
|
<div class="dropdown-menu">
|
|
|
|
${until(this.initModel().then(() => html`
|
|
|
|
<converse-emoji-picker
|
|
|
|
.chatview=${this.chatview}
|
|
|
|
.model=${this.model}
|
|
|
|
?render_emojis=${this.render_emojis}
|
|
|
|
current_category="${this.model.get('current_category') || ''}"
|
|
|
|
current_skintone="${this.model.get('current_skintone') || ''}"
|
|
|
|
query="${this.model.get('query') || ''}"
|
|
|
|
></converse-emoji-picker>`), '')}
|
|
|
|
</div>
|
|
|
|
</div>`;
|
|
|
|
}
|
|
|
|
|
2020-08-14 15:07:15 +02:00
|
|
|
connectedCallback () {
|
|
|
|
super.connectedCallback();
|
|
|
|
this.render_emojis = false;
|
|
|
|
}
|
|
|
|
|
2020-06-08 12:12:49 +02:00
|
|
|
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 () {
|
2020-07-09 21:21:17 +02:00
|
|
|
await this.initModel();
|
2020-06-08 12:12:49 +02:00
|
|
|
if (!this.render_emojis) {
|
|
|
|
// Trigger an update so that emojis are rendered
|
|
|
|
this.render_emojis = true;
|
2020-07-09 21:21:17 +02:00
|
|
|
await this.requestUpdate();
|
2020-06-08 12:12:49 +02:00
|
|
|
}
|
|
|
|
super.showMenu();
|
2020-07-09 21:21:17 +02:00
|
|
|
setTimeout(() => this.querySelector('.emoji-search')?.focus());
|
2020-06-08 12:12:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
api.elements.define('converse-emoji-dropdown', EmojiDropdown);
|
2020-07-01 21:45:18 +02:00
|
|
|
api.elements.define('converse-emoji-picker', EmojiPicker);
|