Turn the emoji picker into a web component
This commit is contained in:
parent
be20b8e1a0
commit
b3e34a0636
@ -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
src/components/emoji-picker.js
Normal file
305
src/components/emoji-picker.js
Normal file
@ -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);
|
@ -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>`;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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>`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user