Open emojis popup when TAB is pressed on a word starting with :
This commit is contained in:
parent
9099ef89fa
commit
e4dc9fa820
@ -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] !== ' ')) {
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user