Преглед на файлове

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

JC Brand преди 5 години
родител
ревизия
e4dc9fa820
променени са 3 файла, в които са добавени 101 реда и са изтрити 29 реда
  1. 28 3
      src/converse-chatview.js
  2. 53 22
      src/converse-emoji-views.js
  3. 20 4
      src/headless/utils/core.js

+ 28 - 3
src/converse-chatview.js

@@ -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] !== ' ')) {

+ 53 - 22
src/converse-emoji-views.js

@@ -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);
             }
         });
 

+ 20 - 4
src/headless/utils/core.js

@@ -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
  */