浏览代码

autocomplete: Use `lit` instead of native methods

JC Brand 2 月之前
父节点
当前提交
431629dc8b

+ 18 - 25
src/plugins/muc-views/utils.js

@@ -142,18 +142,15 @@ export function getChatRoomBodyTemplate(model) {
  * @param {MUC} muc
  * @param {Suggestion} text
  * @param {string} input
- * @returns {HTMLLIElement}
+ * @returns {import('lit').TemplateResult} The rendered HTML for the item.
  */
 export function getAutoCompleteListItem(muc, text, input) {
     input = input.trim();
-    const li = document.createElement('li');
-    li.setAttribute('aria-selected', 'false');
-
-    if (api.settings.get('muc_mention_autocomplete_show_avatar')) {
+    let avatar_model;
+    const show_avatar = api.settings.get('muc_mention_autocomplete_show_avatar');
+    if (show_avatar) {
         const t = text.label.toLowerCase();
-        const avatar_el = /** @type {Avatar} */ (document.createElement('converse-avatar'));
-
-        avatar_el.model = muc.occupants.findWhere((o) => {
+        avatar_model = muc.occupants.findWhere((o) => {
             if (o.getDisplayName()?.toLowerCase()?.startsWith(t)) {
                 return o;
             } else if (o.get('nickname')?.toLowerCase()?.startsWith(t)) {
@@ -162,27 +159,23 @@ export function getAutoCompleteListItem(muc, text, input) {
                 return o;
             }
         });
-        avatar_el.setAttribute('name', avatar_el.model.getDisplayName());
-        avatar_el.setAttribute('height', '22');
-        avatar_el.setAttribute('width', '22');
-        avatar_el.setAttribute('class', 'avatar avatar-autocomplete');
-        li.appendChild(avatar_el);
     }
 
     const regex = new RegExp('(' + input + ')', 'ig');
     const parts = input ? text.split(regex) : [text];
-
-    parts.forEach((txt) => {
-        if (input && txt.match(regex)) {
-            const match = document.createElement('mark');
-            match.textContent = txt;
-            li.appendChild(match);
-        } else {
-            li.appendChild(document.createTextNode(txt));
-        }
-    });
-
-    return li;
+    return html`
+        <li aria-selected="false">
+            ${parts.map((txt) => (input && txt.match(regex) ? html`<mark>${txt}</mark>` : txt))}
+            ${show_avatar
+                ? html`<converse-avatar
+                      name="${avatar_model.getDisplayName()}"
+                      height="22"
+                      width="22"
+                      class="avatar avatar-autocomplete"
+                  ></converse-avatar>`
+                : ''}
+        </li>
+    `;
 }
 
 export async function getAutoCompleteList() {

+ 12 - 9
src/shared/autocomplete/autocomplete.js

@@ -4,10 +4,11 @@
  * @license Mozilla Public License (MPLv2)
  */
 
+import { render, html } from 'lit';
 import { EventEmitter } from '@converse/skeletor';
 import { converse, u } from '@converse/headless';
 import Suggestion from './suggestion.js';
-import { helpers, FILTER_CONTAINS, ITEM, SORT_BY_QUERY_POSITION } from './utils.js';
+import { helpers, getAutoCompleteItem, FILTER_CONTAINS, SORT_BY_QUERY_POSITION } from './utils.js';
 
 const { siblingIndex } = u;
 
@@ -30,7 +31,7 @@ export class AutoComplete extends EventEmitter(Object) {
         this.max_items = 10;
         this.auto_first = false; // Should the first element be automatically selected?
         this.data = (a, _v) => a;
-        this.item = ITEM;
+        this.item = getAutoCompleteItem;
 
         if (u.hasClass('suggestion-box', el)) {
             this.container = el;
@@ -40,7 +41,7 @@ export class AutoComplete extends EventEmitter(Object) {
         this.input = /** @type {HTMLInputElement} */ (this.container.querySelector('.suggestion-box__input'));
         this.input.setAttribute('aria-autocomplete', 'list');
 
-        this.ul = this.container.querySelector('.suggestion-box__results');
+        this.ul = /** @type {HTMLElement} */(this.container.querySelector('.suggestion-box__results'));
         this.status = this.container.querySelector('.suggestion-box__additions');
 
         Object.assign(this, config);
@@ -191,7 +192,7 @@ export class AutoComplete extends EventEmitter(Object) {
     }
 
     /**
-     * @param {Element} selected
+     * @param {Element} [selected]
      */
     select(selected) {
         if (selected) {
@@ -204,7 +205,10 @@ export class AutoComplete extends EventEmitter(Object) {
             this.insertValue(suggestion);
             this.close({ reason: 'select' });
             this.auto_completing = false;
-            this.trigger('suggestion-box-selectcomplete', { text: suggestion });
+            this.trigger('suggestion-box-selectcomplete', {
+                text: suggestion, // DEPRECATED
+                suggestion
+            });
         }
     }
 
@@ -311,8 +315,6 @@ export class AutoComplete extends EventEmitter(Object) {
             }
 
             this.index = -1;
-            this.ul.innerHTML = '';
-
             this.suggestions = list
                 .map(
                     /** @param {import('./types').XHRResultItem} item */ (item) =>
@@ -324,9 +326,10 @@ export class AutoComplete extends EventEmitter(Object) {
                 this.suggestions = this.suggestions.sort(this.sort);
             }
             this.suggestions = this.suggestions.slice(0, this.max_items);
-            this.suggestions.forEach((text) => this.ul.appendChild(this.item(text, value)));
 
-            if (this.ul.children.length === 0) {
+            render(html`${this.suggestions.map((text) => this.item(text, value))}`, this.ul);
+
+            if (this.suggestions.length === 0) {
                 this.close({ reason: 'nomatches' });
             } else {
                 this.open();

+ 8 - 1
src/shared/autocomplete/component.js

@@ -140,7 +140,14 @@ export default class AutoCompleteComponent extends CustomElement {
             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", ({ suggestion }) => {
+            this.auto_completing = false;
+            this.dispatchEvent(new CustomEvent('autocomplete-select', {
+                detail: { suggestion },
+                bubbles: true,
+                composed: true
+            }));
+        });
     }
 
     /** @param {KeyboardEvent} ev */

+ 0 - 1
src/shared/autocomplete/styles/_autocomplete.scss

@@ -62,7 +62,6 @@
                 padding: 1em;
                 position: relative;
                 text-overflow: ellipsis;
-                white-space: pre;
                 &:hover {
                     mark {
                         background-color: unset;

+ 28 - 22
src/shared/autocomplete/utils.js

@@ -1,3 +1,4 @@
+import { html } from 'lit';
 import { converse } from '@converse/headless';
 
 const u = converse.env.utils;
@@ -31,22 +32,28 @@ export const helpers = {
         }
     },
 
-    regExpEscape(s) {
-        return s.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
-    },
-
     isMention(word, ac_triggers) {
         return ac_triggers.includes(word[0]) || (u.isMentionBoundary(word[0]) && ac_triggers.includes(word[1]));
     },
 };
 
+/**
+ * Escapes special characters in a string to be used in a regular expression.
+ * This function takes a string and returns a new string with all special characters
+ * escaped, ensuring that the string can be safely used in a RegExp constructor.
+ * @param {string} s - The string to escape.
+ */
+export function regExpEscape(s) {
+    return s.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
 /**
  * @param {string} text
  * @param {string} input
  * @returns {boolean}
  */
 export function FILTER_CONTAINS(text, input) {
-    return RegExp(helpers.regExpEscape(input.trim()), 'i').test(text);
+    return RegExp(regExpEscape(input.trim()), 'i').test(text);
 }
 
 /**
@@ -55,7 +62,7 @@ export function FILTER_CONTAINS(text, input) {
  * @returns {boolean}
  */
 export function FILTER_STARTSWITH(text, input) {
-    return RegExp('^' + helpers.regExpEscape(input.trim()), 'i').test(text);
+    return RegExp('^' + regExpEscape(input.trim()), 'i').test(text);
 }
 
 /**
@@ -81,21 +88,20 @@ export const SORT_BY_QUERY_POSITION = function (a, b) {
     return (x === -1 ? Infinity : x) < (y === -1 ? Infinity : y) ? -1 : 1;
 };
 
-export const ITEM = (text, input) => {
+/**
+ * Renders an item for display in a list.
+ * @param {string} text - The text to display.
+ * @param {string} input - The input string to highlight.
+ * @returns {import('lit').TemplateResult} The rendered HTML for the item.
+ */
+export function getAutoCompleteItem(text, input) {
     input = input.trim();
-    const element = document.createElement('li');
-    element.setAttribute('aria-selected', 'false');
-
-    const regex = new RegExp('(' + input + ')', 'ig');
+    const regex = new RegExp('(' + regExpEscape(input) + ')', 'ig');
     const parts = input ? text.split(regex) : [text];
-    parts.forEach((txt) => {
-        if (input && txt.match(regex)) {
-            const match = document.createElement('mark');
-            match.textContent = txt;
-            element.appendChild(match);
-        } else {
-            element.appendChild(document.createTextNode(txt));
-        }
-    });
-    return element;
-};
+
+    return html`
+        <li aria-selected="false">
+            ${parts.map((txt) => (input && txt.match(regex) ? html`<mark>${txt}</mark>` : txt))}
+        </li>
+    `;
+}

+ 2 - 2
src/types/plugins/muc-views/utils.d.ts

@@ -28,9 +28,9 @@ export function getChatRoomBodyTemplate(model: MUC): import("lit-html").Template
  * @param {MUC} muc
  * @param {Suggestion} text
  * @param {string} input
- * @returns {HTMLLIElement}
+ * @returns {import('lit').TemplateResult} The rendered HTML for the item.
  */
-export function getAutoCompleteListItem(muc: MUC, text: Suggestion, input: string): HTMLLIElement;
+export function getAutoCompleteListItem(muc: MUC, text: Suggestion, input: string): import("lit").TemplateResult;
 export function getAutoCompleteList(): Promise<any[]>;
 /**
  * @param {MUC} muc

+ 1 - 1
src/types/plugins/rosterview/modals/accept-contact-request.d.ts

@@ -1,4 +1,4 @@
-export default class AcceptContactRequest extends BaseModal {
+export default class AcceptContactRequestModal extends BaseModal {
     contact: any;
     renderModal(): import("lit-html").TemplateResult<1>;
     getModalTitle(): any;

+ 1 - 4
src/types/plugins/rosterview/utils.d.ts

@@ -68,10 +68,7 @@ export function getJIDsAutoCompleteList(): any[];
 /**
  * @param {string} query
  */
-export function getNamesAutoCompleteList(query: string): Promise<{
-    label: string;
-    value: string;
-}[]>;
+export function getNamesAutoCompleteList(query: string): Promise<any[]>;
 export type Model = import("@converse/skeletor").Model;
 export type RosterContact = import("@converse/headless").RosterContact;
 export type RosterContacts = import("@converse/headless").RosterContacts;

+ 7 - 3
src/types/shared/autocomplete/autocomplete.d.ts

@@ -28,10 +28,10 @@ export class AutoComplete extends AutoComplete_base {
     max_items: number;
     auto_first: boolean;
     data: (a: any, _v: any) => any;
-    item: (text: any, input: any) => HTMLLIElement;
+    item: typeof getAutoCompleteItem;
     container: Element | HTMLElement;
     input: HTMLInputElement;
-    ul: Element;
+    ul: HTMLElement;
     status: Element;
     index: number;
     set list(list: any);
@@ -57,7 +57,10 @@ export class AutoComplete extends AutoComplete_base {
      * @param {boolean} scroll=true
      */
     goto(i: number, scroll?: boolean): void;
-    select(selected: any): void;
+    /**
+     * @param {Element} [selected]
+     */
+    select(selected?: Element): void;
     auto_completing: boolean;
     /**
      * @param {Event} ev
@@ -78,5 +81,6 @@ export class AutoComplete extends AutoComplete_base {
 }
 export default AutoComplete;
 import { FILTER_CONTAINS } from './utils.js';
+import { getAutoCompleteItem } from './utils.js';
 import Suggestion from './suggestion.js';
 //# sourceMappingURL=autocomplete.d.ts.map

+ 3 - 2
src/types/shared/autocomplete/suggestion.d.ts

@@ -4,14 +4,15 @@ export default Suggestion;
  */
 declare class Suggestion extends String {
     /**
-     * @param { any } data - The auto-complete data. Ideally an object e.g. { label, value },
+     * @param {any} data - The auto-complete data. Ideally an object e.g. { label, value },
      *      which specifies the value and human-presentable label of the suggestion.
-     * @param { string } query - The query string being auto-completed
+     * @param {string} query - The query string being auto-completed
      */
     constructor(data: any, query: string);
     label: any;
     value: any;
     query: string;
+    data: any;
     get lenth(): any;
 }
 //# sourceMappingURL=suggestion.d.ts.map

+ 14 - 2
src/types/shared/autocomplete/utils.d.ts

@@ -1,3 +1,10 @@
+/**
+ * Escapes special characters in a string to be used in a regular expression.
+ * This function takes a string and returns a new string with all special characters
+ * escaped, ensuring that the string can be safely used in a RegExp constructor.
+ * @param {string} s - The string to escape.
+ */
+export function regExpEscape(s: string): string;
 /**
  * @param {string} text
  * @param {string} input
@@ -16,13 +23,18 @@ export function FILTER_STARTSWITH(text: string, input: string): boolean;
  * @returns {number}
  */
 export function SORT_BY_LENGTH(a: string, b: string): number;
+/**
+ * Renders an item for display in a list.
+ * @param {string} text - The text to display.
+ * @param {string} input - The input string to highlight.
+ * @returns {import('lit').TemplateResult} The rendered HTML for the item.
+ */
+export function getAutoCompleteItem(text: string, input: string): import("lit").TemplateResult;
 export namespace helpers {
     function getElement(expr: any, el: any): any;
     function bind(element: any, o: any): void;
     function unbind(element: any, o: any): void;
-    function regExpEscape(s: any): any;
     function isMention(word: any, ac_triggers: any): any;
 }
 export function SORT_BY_QUERY_POSITION(a: any, b: any): number;
-export function ITEM(text: any, input: any): HTMLLIElement;
 //# sourceMappingURL=utils.d.ts.map