Sfoglia il codice sorgente

Avoid excessive evaluations in auto-complete

JC Brand 3 mesi fa
parent
commit
3f453413d9

+ 1 - 0
src/headless/types/utils/index.d.ts

@@ -57,6 +57,7 @@ declare const _default: {
     shouldClearCache(_converse: ConversePrivateGlobal): boolean;
     tearDown(_converse: ConversePrivateGlobal): Promise<any>;
     clearSession(_converse: ConversePrivateGlobal): any;
+    debounce(func: Function, timeout: number): (...args: any[]) => void;
     waitUntil(func: Function, max_wait?: number, check_delay?: number): Promise<any>;
     getOpenPromise: any;
     merge(dst: any, src: any): void;

+ 7 - 0
src/headless/types/utils/promise.d.ts

@@ -1,3 +1,10 @@
+/**
+ * Debounces a function by waiting for the timeout period before calling it.
+ * If the function gets called again, the timeout period resets.
+ * @param {Function} func
+ * @param {number} timeout
+ */
+export function debounce(func: Function, timeout: number): (...args: any[]) => void;
 /**
  * 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.

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

@@ -1,8 +1,24 @@
 import log from "@converse/log";
-import { getOpenPromise } from '@converse/openpromise';
+import { getOpenPromise } from "@converse/openpromise";
 
 export { getOpenPromise };
 
+/**
+ * Debounces a function by waiting for the timeout period before calling it.
+ * If the function gets called again, the timeout period resets.
+ * @param {Function} func
+ * @param {number} timeout
+ */
+export function debounce(func, timeout) {
+    let timer;
+    return function (...args) {
+        clearTimeout(timer);
+        timer = setTimeout(() => {
+            func.apply(this, args);
+        }, timeout);
+    };
+}
+
 /**
  * Clears the specified timeout and interval.
  * @method u#clearTimers
@@ -28,7 +44,7 @@ function clearTimers(timeout, interval) {
  * @copyright Simen Bekkhus 2016
  * @license MIT
  */
-export function waitUntil (func, max_wait=300, check_delay=3) {
+export function waitUntil(func, max_wait = 300, check_delay = 3) {
     // Run the function once without setting up any listeners in case it's already true
     try {
         const result = func();
@@ -42,7 +58,7 @@ export function waitUntil (func, max_wait=300, check_delay=3) {
     const promise = getOpenPromise();
     const timeout_err = new Error();
 
-    function checker () {
+    function checker() {
         try {
             const result = func();
             if (result) {
@@ -57,7 +73,7 @@ export function waitUntil (func, max_wait=300, check_delay=3) {
 
     const interval = setInterval(checker, check_delay);
 
-    function handler () {
+    function handler() {
         clearTimers(max_wait_timeout, interval);
         const err_msg = `Wait until promise timed out: \n\n${timeout_err.stack}`;
         console.trace();

+ 0 - 1
src/plugins/muc-views/message-form.js

@@ -34,7 +34,6 @@ export default class MUCMessageForm extends MessageForm {
     initMentionAutoComplete () {
         this.mention_auto_complete = new AutoComplete(this, {
             auto_first: true,
-            auto_evaluate: false,
             min_chars: api.settings.get('muc_mention_autocomplete_min_chars'),
             match_current_word: true,
             list: () => this.getAutoCompleteList(),

+ 1 - 1
src/plugins/rosterview/tests/add-contact-modal.js

@@ -33,7 +33,7 @@ describe("The 'Add Contact' widget", function () {
 
         const evt = new Event('input');
         input_jid.dispatchEvent(evt);
-        expect(modal.querySelector('.suggestion-box li').textContent).toBe('someone@montague.lit');
+        await u.waitUntil(() => modal.querySelector('.suggestion-box li')?.textContent === 'someone@montague.lit');
         input_jid.value = 'someone@montague.lit';
         input_name.value = 'Someone';
         modal.querySelector('button[type="submit"]').click();

+ 6 - 15
src/shared/autocomplete/autocomplete.js

@@ -23,7 +23,6 @@ export class AutoComplete extends EventEmitter(Object) {
 
         this.suggestions = [];
         this.is_opened = false;
-        this.auto_evaluate = true; // evaluate automatically without any particular key as trigger
         this.match_current_word = false; // Match only the current word, otherwise all input is matched
         this.sort = config.sort === false ? null : SORT_BY_QUERY_POSITION;
         this.filter = FILTER_CONTAINS;
@@ -61,19 +60,14 @@ export class AutoComplete extends EventEmitter(Object) {
     }
 
     bindEvents () {
-        const input = {
-            "blur": () => this.close({'reason': 'blur'})
-        }
-        if (this.auto_evaluate) {
-            input["input"] = (e) => this.evaluate(e);
-        }
-
         this._events = {
-            'input': input,
-            'form': {
+            input: {
+                "blur": () => this.close({'reason': 'blur'})
+            },
+            form: {
                 "submit": () => this.close({'reason': 'submit'})
             },
-            'ul': {
+            ul: {
                 "mousedown": (ev) => this.onMouseDown(ev),
                 "mouseover": (ev) => this.onMouseOver(ev)
             }
@@ -283,10 +277,7 @@ export class AutoComplete extends EventEmitter(Object) {
             ev.key === converse.keycodes.UP_ARROW ||
             ev.key === converse.keycodes.DOWN_ARROW
         );
-
-        if (!this.auto_evaluate && !this.auto_completing || selecting) {
-            return;
-        }
+        if (selecting) return;
 
         let value = this.match_current_word ? u.getCurrentWord(this.input) : this.input.value;
 

+ 42 - 40
src/shared/autocomplete/component.js

@@ -1,8 +1,8 @@
-import AutoComplete from './autocomplete.js';
-import { CustomElement } from 'shared/components/element.js';
-import { FILTER_CONTAINS, FILTER_STARTSWITH } from './utils.js';
-import { api } from '@converse/headless';
-import { html } from 'lit';
+import AutoComplete from "./autocomplete.js";
+import { CustomElement } from "shared/components/element.js";
+import { FILTER_CONTAINS, FILTER_STARTSWITH } from "./utils.js";
+import { api, u } from "@converse/headless";
+import { html } from "lit";
 
 /**
  * A custom element that can be used to add auto-completion suggestions to a form input.
@@ -46,7 +46,7 @@ import { html } from 'lit';
  *     </converse-autocomplete>
  */
 export default class AutoCompleteComponent extends CustomElement {
-    static get properties () {
+    static get properties() {
         return {
             auto_evaluate: { type: Boolean },
             auto_first: { type: Boolean },
@@ -68,29 +68,37 @@ export default class AutoCompleteComponent extends CustomElement {
         };
     }
 
-    constructor () {
+    constructor() {
         super();
         this.auto_evaluate = true;
         this.auto_first = false;
         this.data = (a) => a;
-        this.error_message = '';
-        this.filter = 'contains';
+        this.error_message = "";
+        this.filter = "contains";
         this.getAutoCompleteList = null;
-        this.include_triggers = '';
+        this.include_triggers = "";
         this.list = null;
         this.match_current_word = false; // Match only the current word, otherwise all input is matched
         this.max_items = 10;
         this.min_chars = 1;
-        this.name = '';
-        this.placeholder = '';
-        this.position = 'above';
+        this.name = "";
+        this.placeholder = "";
+        this.position = "above";
         this.required = false;
-        this.triggers = '';
+        this.triggers = "";
         this.validate = null;
-        this.value = '';
+        this.value = "";
+
+        this.evaluate = u.debounce(
+            /** @param {KeyboardEvent} ev */
+            (ev) => {
+                this.auto_evaluate && this.auto_complete.evaluate(ev);
+            },
+            250
+        );
     }
 
-    render () {
+    render() {
         const position_class = `suggestion-box__results--${this.position}`;
         return html`
             <div class="suggestion-box suggestion-box__name">
@@ -101,9 +109,9 @@ export default class AutoCompleteComponent extends CustomElement {
                     ?required=${this.required}
                     @change=${this.onChange}
                     @keydown=${this.onKeyDown}
-                    @keyup=${this.onKeyUp}
+                    @input=${this.evaluate}
                     autocomplete="off"
-                    class="form-control suggestion-box__input ${this.error_message ? 'is-invalid error' : ''}"
+                    class="form-control suggestion-box__input ${this.error_message ? "is-invalid error" : ""}"
                     name="${this.name}"
                     placeholder="${this.placeholder}"
                     type="text"
@@ -116,42 +124,36 @@ export default class AutoCompleteComponent extends CustomElement {
                     aria-relevant="additions"
                 ></span>
             </div>
-            ${this.error_message ? html`<div class="invalid-feedback">${this.error_message}</div>` : ''}
+            ${this.error_message ? html`<div class="invalid-feedback">${this.error_message}</div>` : ""}
         `;
     }
 
-    firstUpdated () {
-        this.auto_complete = new AutoComplete(/** @type HTMLElement */(this.firstElementChild), {
-            'ac_triggers': this.triggers.split(' '),
-            'auto_evaluate': this.auto_evaluate,
-            'auto_first': this.auto_first,
-            'filter': this.filter == 'contains' ? FILTER_CONTAINS : FILTER_STARTSWITH,
-            'include_triggers': [],
-            'list': this.list ?? ((q) => this.getAutoCompleteList(q)),
-            'data': this.data,
-            'match_current_word': true,
-            'max_items': this.max_items,
-            'min_chars': this.min_chars,
+    firstUpdated() {
+        this.auto_complete = new AutoComplete(/** @type HTMLElement */ (this.firstElementChild), {
+            "ac_triggers": this.triggers.split(" "),
+            "auto_first": this.auto_first,
+            "filter": this.filter == "contains" ? FILTER_CONTAINS : FILTER_STARTSWITH,
+            "include_triggers": [],
+            "list": this.list ?? ((q) => this.getAutoCompleteList(q)),
+            "data": this.data,
+            "match_current_word": true,
+            "max_items": this.max_items,
+            "min_chars": this.min_chars,
         });
-        this.auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
+        this.auto_complete.on("suggestion-box-selectcomplete", () => (this.auto_completing = false));
     }
 
     /** @param {KeyboardEvent} ev */
-    onKeyDown (ev) {
+    onKeyDown(ev) {
         this.auto_complete.onKeyDown(ev);
     }
 
-    /** @param {KeyboardEvent} ev */
-    onKeyUp (ev) {
-        this.auto_complete.evaluate(ev);
-    }
-
     async onChange() {
-        const input = this.querySelector('input');
+        const input = this.querySelector("input");
         this.error_message = await this.validate?.(input.value);
         if (this.error_message) this.requestUpdate();
         return this;
     }
 }
 
-api.elements.define('converse-autocomplete', AutoCompleteComponent);
+api.elements.define("converse-autocomplete", AutoCompleteComponent);

+ 0 - 1
src/types/shared/autocomplete/autocomplete.d.ts

@@ -19,7 +19,6 @@ export class AutoComplete extends AutoComplete_base {
     constructor(el: HTMLElement, config?: any);
     suggestions: any[];
     is_opened: boolean;
-    auto_evaluate: boolean;
     match_current_word: boolean;
     sort: (a: any, b: any) => number;
     filter: (text: any, input: any) => boolean;

+ 3 - 4
src/types/shared/autocomplete/component.d.ts

@@ -111,16 +111,15 @@ export default class AutoCompleteComponent extends CustomElement {
     triggers: string;
     validate: any;
     value: string;
+    evaluate: (...args: any[]) => void;
     render(): import("lit").TemplateResult<1>;
     firstUpdated(): void;
     auto_complete: AutoComplete;
     auto_completing: boolean;
     /** @param {KeyboardEvent} ev */
     onKeyDown(ev: KeyboardEvent): void;
-    /** @param {KeyboardEvent} ev */
-    onKeyUp(ev: KeyboardEvent): void;
     onChange(): Promise<this>;
 }
-import { CustomElement } from 'shared/components/element.js';
-import AutoComplete from './autocomplete.js';
+import { CustomElement } from "shared/components/element.js";
+import AutoComplete from "./autocomplete.js";
 //# sourceMappingURL=component.d.ts.map

+ 2 - 0
src/types/utils/index.d.ts

@@ -41,6 +41,7 @@ declare const _default: {
     shouldClearCache(_converse: ConversePrivateGlobal): boolean;
     tearDown(_converse: ConversePrivateGlobal): Promise<any>;
     clearSession(_converse: ConversePrivateGlobal): any;
+    debounce(func: Function, timeout: number): (...args: any[]) => void;
     waitUntil(func: Function, max_wait?: number, check_delay?: number): Promise<any>;
     getOpenPromise: any;
     merge(dst: any, src: any): void;
@@ -150,6 +151,7 @@ declare const _default: {
         shouldClearCache(_converse: ConversePrivateGlobal): boolean;
         tearDown(_converse: ConversePrivateGlobal): Promise<any>;
         clearSession(_converse: ConversePrivateGlobal): any;
+        debounce(func: Function, timeout: number): (...args: any[]) => void;
         waitUntil(func: Function, max_wait?: number, check_delay?: number): Promise<any>;
         getOpenPromise: any;
         merge(dst: any, src: any): void;