2019-08-16 10:18:35 +02:00
|
|
|
// Converse.js
|
|
|
|
// https://conversejs.org
|
|
|
|
//
|
|
|
|
// Copyright (c) 2013-2019, the Converse.js developers
|
|
|
|
// Licensed under the Mozilla Public License (MPLv2)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @module converse-emoji-views
|
|
|
|
*/
|
|
|
|
|
|
|
|
import "@converse/headless/converse-emoji";
|
|
|
|
import BrowserStorage from "backbone.browserStorage";
|
|
|
|
import bootstrap from "bootstrap.native";
|
|
|
|
import tpl_emoji_button from "templates/emoji_button.html";
|
|
|
|
import tpl_emojis from "templates/emojis.html";
|
2019-08-16 16:38:49 +02:00
|
|
|
const { Backbone, _ } = converse.env;
|
2019-08-16 10:18:35 +02:00
|
|
|
const u = converse.env.utils;
|
|
|
|
|
|
|
|
|
|
|
|
converse.plugins.add('converse-emoji-views', {
|
|
|
|
/* Plugin dependencies are other plugins which might be
|
|
|
|
* overridden or relied upon, and therefore need to be loaded before
|
|
|
|
* this plugin.
|
|
|
|
*
|
|
|
|
* If the setting "strict_plugin_dependencies" is set to true,
|
|
|
|
* an error will be raised if the plugin is not found. By default it's
|
|
|
|
* false, which means these plugins are only loaded opportunistically.
|
|
|
|
*
|
|
|
|
* NB: These plugins need to have already been loaded via require.js.
|
|
|
|
*/
|
|
|
|
dependencies: ["converse-emoji", "converse-chatview"],
|
|
|
|
|
|
|
|
|
|
|
|
overrides: {
|
|
|
|
ChatBoxView: {
|
|
|
|
events: {
|
|
|
|
'click .toggle-smiley': 'toggleEmojiMenu',
|
|
|
|
},
|
|
|
|
|
|
|
|
onEnterPressed () {
|
|
|
|
if (this.emoji_dropdown && u.isVisible(this.emoji_dropdown.el.querySelector('.emoji-picker'))) {
|
|
|
|
this.emoji_dropdown.toggle();
|
|
|
|
}
|
|
|
|
this.__super__.onEnterPressed.apply(this, arguments);
|
2019-08-19 15:02:54 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
async onTabPressed (ev) {
|
|
|
|
const { _converse } = this.__super__;
|
|
|
|
const input = ev.target;
|
|
|
|
const value = u.getCurrentWord(input, null, /(:.*?:)/g);
|
|
|
|
if (value.startsWith(':')) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
if (this.emoji_dropdown === undefined) {
|
|
|
|
this.createEmojiDropdown();
|
|
|
|
}
|
|
|
|
this.emoji_dropdown.toggle();
|
|
|
|
await _converse.api.waitUntil('emojisInitialized');
|
|
|
|
this.emoji_picker_view.model.set({
|
|
|
|
'autocompleting': value,
|
|
|
|
'position': ev.target.selectionStart
|
|
|
|
});
|
|
|
|
this.emoji_picker_view.filter(value, true);
|
|
|
|
this.emoji_picker_view.render();
|
|
|
|
} else {
|
|
|
|
this.__super__.onTabPressed.apply(this, arguments);
|
|
|
|
}
|
2019-08-16 10:18:35 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
ChatRoomView: {
|
|
|
|
events: {
|
|
|
|
'click .toggle-smiley': 'toggleEmojiMenu'
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
initialize () {
|
|
|
|
/* The initialize function gets called as soon as the plugin is
|
|
|
|
* loaded by converse.js's plugin machinery.
|
|
|
|
*/
|
|
|
|
const { _converse } = this;
|
|
|
|
const { __ } = _converse;
|
|
|
|
|
|
|
|
_converse.api.settings.update({
|
|
|
|
'use_system_emojis': true,
|
|
|
|
'visible_toolbar_buttons': {
|
|
|
|
'emoji': true
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const emoji_aware_chat_view = {
|
|
|
|
|
|
|
|
createEmojiPicker () {
|
|
|
|
if (_converse.emojipicker === undefined) {
|
|
|
|
const storage = _converse.config.get('storage'),
|
|
|
|
id = `converse.emoji-${_converse.bare_jid}`;
|
|
|
|
_converse.emojipicker = new _converse.EmojiPicker({'id': id});
|
|
|
|
_converse.emojipicker.browserStorage = new BrowserStorage[storage](id);
|
|
|
|
_converse.emojipicker.fetch();
|
|
|
|
}
|
|
|
|
this.emoji_picker_view = new _converse.EmojiPickerView({'model': _converse.emojipicker});
|
|
|
|
this.emoji_picker_view.chatview = this;
|
|
|
|
},
|
|
|
|
|
2019-08-19 15:02:54 +02:00
|
|
|
createEmojiDropdown (ev) {
|
|
|
|
const dropdown_el = this.el.querySelector('.toggle-smiley.dropup');
|
|
|
|
this.emoji_dropdown = new bootstrap.Dropdown(dropdown_el, true);
|
|
|
|
this.emoji_dropdown.el = dropdown_el;
|
|
|
|
},
|
|
|
|
|
2019-08-16 10:18:35 +02:00
|
|
|
async toggleEmojiMenu (ev) {
|
|
|
|
if (this.emoji_dropdown === undefined) {
|
|
|
|
ev.stopPropagation();
|
2019-08-19 15:02:54 +02:00
|
|
|
this.createEmojiDropdown();
|
2019-08-16 10:18:35 +02:00
|
|
|
this.emoji_dropdown.toggle();
|
|
|
|
await _converse.api.waitUntil('emojisInitialized');
|
|
|
|
this.emoji_picker_view.render();
|
|
|
|
this.emoji_picker_view.setScrollPosition();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
insertEmojiPicker () {
|
|
|
|
const picker_el = this.el.querySelector('.emoji-picker');
|
|
|
|
if (picker_el !== null) {
|
|
|
|
picker_el.innerHTML = '';
|
|
|
|
picker_el.appendChild(this.emoji_picker_view.el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Object.assign(_converse.ChatBoxView.prototype, emoji_aware_chat_view);
|
|
|
|
|
|
|
|
|
|
|
|
_converse.EmojiPickerView = Backbone.VDOMView.extend({
|
|
|
|
className: 'emoji-picker__container',
|
|
|
|
events: {
|
2019-08-16 16:38:49 +02:00
|
|
|
'click .emoji-picker__header li.emoji-category': 'chooseCategory',
|
2019-08-16 10:18:35 +02:00
|
|
|
'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone',
|
2019-08-16 16:38:49 +02:00
|
|
|
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
|
|
|
|
'keydown .emoji-search': 'onKeyDown'
|
2019-08-16 10:18:35 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
initialize () {
|
2019-08-19 15:02:54 +02:00
|
|
|
this.debouncedFilter = _.debounce(input => this.filter(input.value), 50);
|
2019-08-16 16:38:49 +02:00
|
|
|
this.model.on('change:query', this.render, this);
|
2019-08-16 10:18:35 +02:00
|
|
|
this.model.on('change:current_skintone', this.render, this);
|
|
|
|
this.model.on('change:current_category', () => {
|
|
|
|
this.render();
|
|
|
|
this.setScrollPosition();
|
|
|
|
});
|
|
|
|
_converse.api.trigger('emojiPickerViewInitialized');
|
|
|
|
},
|
|
|
|
|
|
|
|
toHTML () {
|
|
|
|
const html = tpl_emojis(
|
|
|
|
Object.assign(
|
|
|
|
this.model.toJSON(), {
|
2019-08-16 16:38:49 +02:00
|
|
|
'__': __,
|
2019-08-16 10:18:35 +02:00
|
|
|
'_converse': _converse,
|
|
|
|
'emoji_categories': _converse.emoji_categories,
|
|
|
|
'emojis_by_category': u.getEmojisByCategory(),
|
2019-08-16 16:38:49 +02:00
|
|
|
'shouldBeHidden': shortname => this.shouldBeHidden(shortname),
|
2019-08-16 10:18:35 +02:00
|
|
|
'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'],
|
|
|
|
'toned_emojis': _converse.emojis.toned,
|
2019-08-16 12:44:19 +02:00
|
|
|
'transform': u.getEmojiRenderer(),
|
|
|
|
'transformCategory': shortname => u.getEmojiRenderer()(this.getTonedShortname(shortname))
|
2019-08-16 10:18:35 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
return html;
|
|
|
|
},
|
|
|
|
|
2019-08-19 15:02:54 +02:00
|
|
|
filter (value, set_property) {
|
|
|
|
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;
|
|
|
|
}
|
2019-08-16 16:38:49 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
onKeyDown (ev) {
|
|
|
|
if (ev.keyCode === _converse.keycodes.TAB) {
|
|
|
|
ev.preventDefault();
|
|
|
|
const match = _.find(_converse.emoji_shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
|
|
|
|
if (match) {
|
2019-08-19 15:02:54 +02:00
|
|
|
this.filter(match, true);
|
2019-08-16 16:38:49 +02:00
|
|
|
}
|
|
|
|
} else if (ev.keyCode === _converse.keycodes.ENTER) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
if (_converse.emoji_shortnames.includes(ev.target.value)) {
|
2019-08-19 15:02:54 +02:00
|
|
|
const replace = this.model.get('autocompleting');
|
|
|
|
const position = this.model.get('position');
|
|
|
|
this.model.set({'autocompleting': null, 'position': null});
|
|
|
|
this.chatview.insertIntoTextArea(ev.target.value, replace, false, position);
|
2019-08-19 13:40:21 +02:00
|
|
|
this.chatview.emoji_dropdown.toggle();
|
2019-08-19 15:02:54 +02:00
|
|
|
this.filter('', true);
|
2019-08-16 16:38:49 +02:00
|
|
|
}
|
|
|
|
} else {
|
2019-08-19 15:02:54 +02:00
|
|
|
this.debouncedFilter(ev.target.value);
|
2019-08-16 16:38:49 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2019-08-16 12:44:19 +02:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2019-08-16 10:18:35 +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.model.get('current_skintone') === skintone) {
|
|
|
|
this.model.save({'current_skintone': ''});
|
|
|
|
} else {
|
|
|
|
this.model.save({'current_skintone': skintone});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
chooseCategory (ev) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
|
|
|
|
const category = target.getAttribute("data-category").trim();
|
2019-08-19 13:40:21 +02:00
|
|
|
// XXX: See above
|
|
|
|
const input = this.el.querySelector('.emoji-search');
|
|
|
|
input.value = '';
|
|
|
|
this.model.save({'current_category': category, 'query': undefined});
|
2019-08-16 10:18:35 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
setScrollPosition () {
|
|
|
|
const category = this.model.get('current_category');
|
|
|
|
const el = this.el.querySelector('.emoji-picker__lists');
|
|
|
|
const heading = this.el.querySelector(`#emoji-picker-${category}`);
|
|
|
|
el.scrollTop = heading.offsetTop - heading.offsetHeight*2;
|
|
|
|
},
|
|
|
|
|
|
|
|
insertEmoji (ev) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
|
2019-08-19 15:02:54 +02:00
|
|
|
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);
|
2019-08-19 13:40:21 +02:00
|
|
|
this.chatview.emoji_dropdown.toggle();
|
2019-08-19 15:02:54 +02:00
|
|
|
this.filter('', true);
|
2019-08-16 10:18:35 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/************************ BEGIN Event Handlers ************************/
|
|
|
|
_converse.api.listen.on('renderToolbar', view => {
|
|
|
|
if (_converse.visible_toolbar_buttons.emoji) {
|
|
|
|
const html = tpl_emoji_button({'tooltip_insert_smiley': __('Insert emojis')});
|
|
|
|
view.el.querySelector('.chat-toolbar').insertAdjacentHTML('afterBegin', html);
|
|
|
|
view.createEmojiPicker();
|
|
|
|
view.insertEmojiPicker();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|