Commit e4dc9fa8 authored by JC Brand's avatar JC Brand

Open emojis popup when TAB is pressed on a word starting with :

parent 9099ef89
...@@ -883,6 +883,8 @@ converse.plugins.add('converse-chatview', { ...@@ -883,6 +883,8 @@ converse.plugins.add('converse-chatview', {
if (ev.keyCode === _converse.keycodes.FORWARD_SLASH) { if (ev.keyCode === _converse.keycodes.FORWARD_SLASH) {
// Forward slash is used to run commands. Nothing to do here. // Forward slash is used to run commands. Nothing to do here.
return; return;
} else if (ev.keyCode === _converse.keycodes.TAB) {
return this.onTabPressed(ev);
} else if (ev.keyCode === _converse.keycodes.ESCAPE) { } else if (ev.keyCode === _converse.keycodes.ESCAPE) {
return this.onEscapePressed(ev); return this.onEscapePressed(ev);
} else if (ev.keyCode === _converse.keycodes.ENTER) { } else if (ev.keyCode === _converse.keycodes.ENTER) {
...@@ -922,6 +924,8 @@ converse.plugins.add('converse-chatview', { ...@@ -922,6 +924,8 @@ converse.plugins.add('converse-chatview', {
return this.onFormSubmitted(ev); return this.onFormSubmitted(ev);
}, },
onTabPressed (ev) {}, // noop, overridden in other plugins
onEscapePressed (ev) { onEscapePressed (ev) {
ev.preventDefault(); ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting'), const idx = this.model.messages.findLastIndex('correcting'),
...@@ -1021,7 +1025,19 @@ converse.plugins.add('converse-chatview', { ...@@ -1021,7 +1025,19 @@ converse.plugins.add('converse-chatview', {
return this; return this;
}, },
insertIntoTextArea (value, replace=false, correcting=false) { /**
* Insert a particular string value into the textarea of this chat box.
* @private
* @method _converse.ChatBoxView#insertIntoTextArea
* @param {string} value - The value to be inserted.
* @param {(boolean|string)} [replace] - Whether an existing value
* should be replaced. If set to `true`, the entire textarea will
* be replaced with the new value. If set to a string, then only
* that string will be replaced *if* a position is also specified.
* @param {integer} [position] - The end index of the string to be
* replaced with the new value.
*/
insertIntoTextArea (value, replace=false, correcting=false, position) {
const textarea = this.el.querySelector('.chat-textarea'); const textarea = this.el.querySelector('.chat-textarea');
if (correcting) { if (correcting) {
u.addClass('correcting', textarea); u.addClass('correcting', textarea);
...@@ -1029,8 +1045,17 @@ converse.plugins.add('converse-chatview', { ...@@ -1029,8 +1045,17 @@ converse.plugins.add('converse-chatview', {
u.removeClass('correcting', textarea); u.removeClass('correcting', textarea);
} }
if (replace) { if (replace) {
textarea.value = ''; if (position && typeof replace == 'string') {
textarea.value = value; textarea.value = textarea.value.replace(
new RegExp(replace, 'g'),
(match, offset) => {
return offset == position-replace.length ? value : match
}
);
} else {
textarea.value = '';
textarea.value = value;
}
} else { } else {
let existing = textarea.value; let existing = textarea.value;
if (existing && (existing[existing.length-1] !== ' ')) { if (existing && (existing[existing.length-1] !== ' ')) {
......
...@@ -42,6 +42,29 @@ converse.plugins.add('converse-emoji-views', { ...@@ -42,6 +42,29 @@ converse.plugins.add('converse-emoji-views', {
this.emoji_dropdown.toggle(); this.emoji_dropdown.toggle();
} }
this.__super__.onEnterPressed.apply(this, arguments); this.__super__.onEnterPressed.apply(this, arguments);
},
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);
}
} }
}, },
...@@ -82,12 +105,16 @@ converse.plugins.add('converse-emoji-views', { ...@@ -82,12 +105,16 @@ converse.plugins.add('converse-emoji-views', {
this.emoji_picker_view.chatview = this; this.emoji_picker_view.chatview = this;
}, },
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;
},
async toggleEmojiMenu (ev) { async toggleEmojiMenu (ev) {
if (this.emoji_dropdown === undefined) { if (this.emoji_dropdown === undefined) {
ev.stopPropagation(); ev.stopPropagation();
const dropdown_el = this.el.querySelector('.toggle-smiley.dropup'); this.createEmojiDropdown();
this.emoji_dropdown = new bootstrap.Dropdown(dropdown_el, true);
this.emoji_dropdown.el = dropdown_el;
this.emoji_dropdown.toggle(); this.emoji_dropdown.toggle();
await _converse.api.waitUntil('emojisInitialized'); await _converse.api.waitUntil('emojisInitialized');
this.emoji_picker_view.render(); this.emoji_picker_view.render();
...@@ -116,7 +143,7 @@ converse.plugins.add('converse-emoji-views', { ...@@ -116,7 +143,7 @@ converse.plugins.add('converse-emoji-views', {
}, },
initialize () { initialize () {
this.debouncedFilter = _.debounce(input => this.filter(input), 50); this.debouncedFilter = _.debounce(input => this.filter(input.value), 50);
this.model.on('change:query', this.render, this); this.model.on('change:query', this.render, this);
this.model.on('change:current_skintone', this.render, this); this.model.on('change:current_skintone', this.render, this);
this.model.on('change:current_category', () => { this.model.on('change:current_category', () => {
...@@ -145,8 +172,16 @@ converse.plugins.add('converse-emoji-views', { ...@@ -145,8 +172,16 @@ converse.plugins.add('converse-emoji-views', {
return html; return html;
}, },
filter (input) { filter (value, set_property) {
this.model.set({'query': input.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;
}
}, },
onKeyDown (ev) { onKeyDown (ev) {
...@@ -154,25 +189,21 @@ converse.plugins.add('converse-emoji-views', { ...@@ -154,25 +189,21 @@ converse.plugins.add('converse-emoji-views', {
ev.preventDefault(); ev.preventDefault();
const match = _.find(_converse.emoji_shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value)); const match = _.find(_converse.emoji_shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
if (match) { if (match) {
// XXX: Ideally we would set `query` on the model and this.filter(match, true);
// then let the view re-render, instead of doing it
// manually here. Snabbdom supports setting properties,
// Backbone.VDOMView doesn't.
ev.target.value = match;
this.filter(ev.target);
} }
} else if (ev.keyCode === _converse.keycodes.ENTER) { } else if (ev.keyCode === _converse.keycodes.ENTER) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
if (_converse.emoji_shortnames.includes(ev.target.value)) { if (_converse.emoji_shortnames.includes(ev.target.value)) {
this.chatview.insertIntoTextArea(ev.target.value); 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);
this.chatview.emoji_dropdown.toggle(); this.chatview.emoji_dropdown.toggle();
// XXX: See above this.filter('', true);
ev.target.value = '';
this.filter(ev.target);
} }
} else { } else {
this.debouncedFilter(ev.target); this.debouncedFilter(ev.target.value);
} }
}, },
...@@ -239,12 +270,12 @@ converse.plugins.add('converse-emoji-views', { ...@@ -239,12 +270,12 @@ converse.plugins.add('converse-emoji-views', {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target; const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
this.chatview.insertIntoTextArea(target.getAttribute('data-emoji')); 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.chatview.emoji_dropdown.toggle();
// XXX: See above this.filter('', true);
const input = this.el.querySelector('.emoji-search');
input.value = '';
this.filter(input);
} }
}); });
......
...@@ -418,11 +418,25 @@ u.siblingIndex = function (el) { ...@@ -418,11 +418,25 @@ u.siblingIndex = function (el) {
return i; return i;
}; };
u.getCurrentWord = function (input, index) { /**
* Returns the current word being written in the input element
* @method u#getCurrentWord
* @param {HTMLElement} input - The HTMLElement in which text is being entered
* @param {integer} [index] - An optional rightmost boundary index. If given, the text
* value of the input element will only be considered up until this index.
* @param {string} [delineator] - An optional string delineator to
* differentiate between words.
* @private
*/
u.getCurrentWord = function (input, index, delineator) {
if (!index) { if (!index) {
index = input.selectionEnd || undefined; index = input.selectionEnd || undefined;
} }
return _.last(input.value.slice(0, index).split(' ')); let [word] = input.value.slice(0, index).split(' ').slice(-1);
if (delineator) {
[word] = word.split(delineator).slice(-1);
}
return word;
}; };
u.replaceCurrentWord = function (input, new_value) { u.replaceCurrentWord = function (input, new_value) {
...@@ -535,6 +549,7 @@ u.getUniqueId = function () { ...@@ -535,6 +549,7 @@ u.getUniqueId = function () {
/** /**
* Clears the specified timeout and interval. * Clears the specified timeout and interval.
* @method u#clearTimers
* @param {number} timeout - Id if the timeout to clear. * @param {number} timeout - Id if the timeout to clear.
* @param {number} interval - Id of the interval to clear. * @param {number} interval - Id of the interval to clear.
* @private * @private
...@@ -550,12 +565,13 @@ function clearTimers(timeout, interval) { ...@@ -550,12 +565,13 @@ function clearTimers(timeout, interval) {
/** /**
* Creates a {@link Promise} that resolves if the passed in function returns a truthy value. * Creates a {@link Promise} that resolves if the passed in function returns a truthy value.
* Rejects if it throws or does not return truthy within the given max_wait. * Rejects if it throws or does not return truthy within the given max_wait.
* @method u#waitUntil
* @param {Function} func - The function called every check_delay, * @param {Function} func - The function called every check_delay,
* and the result of which is the resolved value of the promise. * and the result of which is the resolved value of the promise.
* @param {number} [max_wait=300] - The time to wait before rejecting the promise. * @param {number} [max_wait=300] - The time to wait before rejecting the promise.
* @param {number} [check_delay=3] - The time to wait before each invocation of {func}. * @param {number} [check_delay=3] - The time to wait before each invocation of {func}.
* @returns {Promise} A promise resolved with the value of func, * @returns {Promise} A promise resolved with the value of func,
* or rejected with the exception thrown by it or it times out. * or rejected with the exception thrown by it or it times out.
* @copyright Simen Bekkhus 2016 * @copyright Simen Bekkhus 2016
* @license MIT * @license MIT
*/ */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment