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

This commit is contained in:
JC Brand 2019-08-19 15:02:54 +02:00
parent 9099ef89fa
commit e4dc9fa820
3 changed files with 101 additions and 29 deletions

View File

@ -883,6 +883,8 @@ converse.plugins.add('converse-chatview', {
if (ev.keyCode === _converse.keycodes.FORWARD_SLASH) {
// Forward slash is used to run commands. Nothing to do here.
return;
} else if (ev.keyCode === _converse.keycodes.TAB) {
return this.onTabPressed(ev);
} else if (ev.keyCode === _converse.keycodes.ESCAPE) {
return this.onEscapePressed(ev);
} else if (ev.keyCode === _converse.keycodes.ENTER) {
@ -922,6 +924,8 @@ converse.plugins.add('converse-chatview', {
return this.onFormSubmitted(ev);
},
onTabPressed (ev) {}, // noop, overridden in other plugins
onEscapePressed (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting'),
@ -1021,7 +1025,19 @@ converse.plugins.add('converse-chatview', {
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');
if (correcting) {
u.addClass('correcting', textarea);
@ -1029,8 +1045,17 @@ converse.plugins.add('converse-chatview', {
u.removeClass('correcting', textarea);
}
if (replace) {
textarea.value = '';
textarea.value = value;
if (position && typeof replace == 'string') {
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 {
let existing = textarea.value;
if (existing && (existing[existing.length-1] !== ' ')) {

View File

@ -42,6 +42,29 @@ converse.plugins.add('converse-emoji-views', {
this.emoji_dropdown.toggle();
}
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', {
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) {
if (this.emoji_dropdown === undefined) {
ev.stopPropagation();
const dropdown_el = this.el.querySelector('.toggle-smiley.dropup');
this.emoji_dropdown = new bootstrap.Dropdown(dropdown_el, true);
this.emoji_dropdown.el = dropdown_el;
this.createEmojiDropdown();
this.emoji_dropdown.toggle();
await _converse.api.waitUntil('emojisInitialized');
this.emoji_picker_view.render();
@ -116,7 +143,7 @@ converse.plugins.add('converse-emoji-views', {
},
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:current_skintone', this.render, this);
this.model.on('change:current_category', () => {
@ -145,8 +172,16 @@ converse.plugins.add('converse-emoji-views', {
return html;
},
filter (input) {
this.model.set({'query': input.value});
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;
}
},
onKeyDown (ev) {
@ -154,25 +189,21 @@ converse.plugins.add('converse-emoji-views', {
ev.preventDefault();
const match = _.find(_converse.emoji_shortnames, sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
if (match) {
// 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.
ev.target.value = match;
this.filter(ev.target);
this.filter(match, true);
}
} else if (ev.keyCode === _converse.keycodes.ENTER) {
ev.preventDefault();
ev.stopPropagation();
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();
// XXX: See above
ev.target.value = '';
this.filter(ev.target);
this.filter('', true);
}
} else {
this.debouncedFilter(ev.target);
this.debouncedFilter(ev.target.value);
}
},
@ -239,12 +270,12 @@ converse.plugins.add('converse-emoji-views', {
ev.preventDefault();
ev.stopPropagation();
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();
// XXX: See above
const input = this.el.querySelector('.emoji-search');
input.value = '';
this.filter(input);
this.filter('', true);
}
});

View File

@ -418,11 +418,25 @@ u.siblingIndex = function (el) {
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) {
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) {
@ -535,6 +549,7 @@ u.getUniqueId = function () {
/**
* Clears the specified timeout and interval.
* @method u#clearTimers
* @param {number} timeout - Id if the timeout to clear.
* @param {number} interval - Id of the interval to clear.
* @private
@ -550,12 +565,13 @@ function clearTimers(timeout, interval) {
/**
* 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.
* @method u#waitUntil
* @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} [check_delay=3] - The time to wait before each invocation 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
* @license MIT
*/