From a9d42683eea78f5c4709426dfebf808e6a3455c7 Mon Sep 17 00:00:00 2001 From: MareStare Date: Wed, 12 Feb 2025 02:59:15 +0000 Subject: [PATCH 1/4] Refactoring of autocomplete and tag inputs --- assets/js/autocomplete.ts | 61 +++++++++++-------- assets/js/utils/suggestions.ts | 5 ++ .../templates/layout/_header.html.slime | 17 +++++- .../templates/tag/_tag_editor.html.slime | 30 +++++++-- 4 files changed, 80 insertions(+), 33 deletions(-) diff --git a/assets/js/autocomplete.ts b/assets/js/autocomplete.ts index d940794c..b8b7f260 100644 --- a/assets/js/autocomplete.ts +++ b/assets/js/autocomplete.ts @@ -15,17 +15,21 @@ import { TermSuggestion, } from './utils/suggestions'; -type InputFieldElement = HTMLInputElement | HTMLTextAreaElement; +type AcEnabledInputElement = HTMLInputElement | HTMLTextAreaElement; -let inputField: InputFieldElement | null = null, - originalTerm: string | undefined, - originalQuery: string | undefined, - selectedTerm: TermContext | null = null; +function hasAcEnabled(element: unknown): element is AcEnabledInputElement { + return (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && Boolean(element.dataset.ac); +} + +let inputField: AcEnabledInputElement | null = null; +let originalTerm: string | undefined; +let originalQuery: string | undefined; +let selectedTerm: TermContext | null = null; const popup = new SuggestionsPopup(); function isSearchField(targetInput: HTMLElement): boolean { - return targetInput && targetInput.dataset.acMode === 'search'; + return targetInput.dataset.acMode === 'search'; } function restoreOriginalValue() { @@ -113,7 +117,7 @@ function keydownHandler(event: KeyboardEvent) { } } -function findSelectedTerm(targetInput: InputFieldElement, searchQuery: string): TermContext | null { +function findSelectedTerm(targetInput: AcEnabledInputElement, searchQuery: string): TermContext | null { if (targetInput.selectionStart === null || targetInput.selectionEnd === null) return null; const selectionIndex = Math.min(targetInput.selectionStart, targetInput.selectionEnd); @@ -139,10 +143,15 @@ function findSelectedTerm(targetInput: InputFieldElement, searchQuery: string): return term; } -function toggleSearchAutocomplete() { +/** + * Our custom autocomplete isn't compatible with the native browser autocomplete, + * so we have to turn it off if our autocomplete is enabled, or turn it back on + * if it's disabled. + */ +function toggleSearchNativeAutocomplete() { const enable = store.get('enable_search_ac'); - for (const searchField of $$(':is(input, textarea)[data-ac-mode=search]')) { + for (const searchField of $$(':is(input, textarea)[data-ac][data-ac-mode=search]')) { if (enable) { searchField.autocomplete = 'off'; } else { @@ -156,11 +165,15 @@ function trimPrefixes(targetTerm: string): string { return targetTerm.trim().replace(/^-/, ''); } -function listenAutocomplete() { +/** + * We control the autocomplete with `data-ac*` attributes in HTML, and subscribe + * event listeners to the `document`. This pattern is described in more detail + * here: https://javascript.info/event-delegation + */ +export function listenAutocomplete() { let serverSideSuggestionsTimeout: number | undefined; let localAc: LocalAutocompleter | null = null; - let isLocalLoading = false; document.addEventListener('focusin', loadAutocompleteFromEvent); @@ -169,12 +182,10 @@ function listenAutocomplete() { loadAutocompleteFromEvent(event); window.clearTimeout(serverSideSuggestionsTimeout); - if (!(event.target instanceof HTMLInputElement) && !(event.target instanceof HTMLTextAreaElement)) return; + if (!hasAcEnabled(event.target)) return; const targetedInput = event.target; - if (!targetedInput.dataset.ac) return; - targetedInput.addEventListener('keydown', keydownHandler as EventListener); if (localAc !== null) { @@ -193,7 +204,7 @@ function listenAutocomplete() { originalTerm = selectedTerm[1].toLowerCase(); } else { - originalTerm = `${inputField.value}`.toLowerCase(); + originalTerm = inputField.value.toLowerCase(); } const suggestions = localAc @@ -236,19 +247,19 @@ function listenAutocomplete() { } }); - function loadAutocompleteFromEvent(event: Event) { - if (!(event.target instanceof HTMLInputElement) && !(event.target instanceof HTMLTextAreaElement)) return; + // Lazy-load the local AC index from the server only once. + let localAcFetchInitiated = false; - if (!isLocalLoading && event.target.dataset.ac) { - isLocalLoading = true; - - fetchLocalAutocomplete().then(autocomplete => { - localAc = autocomplete; - }); + async function loadAutocompleteFromEvent(event: Event) { + if (!hasAcEnabled(event.target) || localAcFetchInitiated) { + return; } + + localAcFetchInitiated = true; + localAc = await fetchLocalAutocomplete(); } - toggleSearchAutocomplete(); + toggleSearchNativeAutocomplete(); popup.onItemSelected((event: CustomEvent) => { if (!event.detail || !inputField) return; @@ -272,5 +283,3 @@ function listenAutocomplete() { ); }); } - -export { listenAutocomplete }; diff --git a/assets/js/utils/suggestions.ts b/assets/js/utils/suggestions.ts index 099c9860..b50e770b 100644 --- a/assets/js/utils/suggestions.ts +++ b/assets/js/utils/suggestions.ts @@ -75,6 +75,11 @@ export class SuggestionsPopup { } private watchItem(listItem: HTMLElement, suggestion: TermSuggestion) { + // This makes sure the item isn't selected if the mouse pointer happens to + // be right on top of the item when the list is rendered. So, the item may + // only be selected on the first `mousemove` event occurring on the element. + // See more details about this problem in the PR description: + // https://github.com/philomena-dev/philomena/pull/350 mouseMoveThenOver(listItem, () => this.updateSelection(listItem)); listItem.addEventListener('mouseout', () => this.clearSelection()); diff --git a/lib/philomena_web/templates/layout/_header.html.slime b/lib/philomena_web/templates/layout/_header.html.slime index 0cf9fc2e..9d6ef883 100644 --- a/lib/philomena_web/templates/layout/_header.html.slime +++ b/lib/philomena_web/templates/layout/_header.html.slime @@ -10,9 +10,22 @@ header.header ' Derpibooru a.header__link.hide-mobile href="/images/new" title="Upload" i.fa.fa-upload - = form_for @conn, ~p"/search", [method: "get", class: "header__search flex flex--no-wrap flex--centered", enforce_utf8: false], fn f -> - input.input.header__input.header__input--search#q name="q" title="For terms all required, separate with ',' or 'AND'; also supports 'OR' for optional terms and '-' or 'NOT' for negation. Search with a blank query for more options or click the ? for syntax help." value=@conn.params["q"] placeholder="Search" autocapitalize="none" data-ac="true" data-ac-min-length="3" data-ac-mode="search" + - title = \ + "For terms all required, separate with ',' or 'AND'; also supports 'OR' " <> \ + "for optional terms and '-' or 'NOT' for negation. Search with a blank " <> \ + "query for more options or click the ? for syntax help" + + input.input.header__input.header__input--search#q[ + name="q" + title=title + value=@conn.params["q"] + placeholder="Search" + autocapitalize="none" + data-ac="true" + data-ac-min-length="3" + data-ac-mode="search" + ] = if present?(@conn.params["sf"]) do input type="hidden" name="sf" value=@conn.params["sf"] diff --git a/lib/philomena_web/templates/tag/_tag_editor.html.slime b/lib/philomena_web/templates/tag/_tag_editor.html.slime index a45aecf5..7c157c58 100644 --- a/lib/philomena_web/templates/tag/_tag_editor.html.slime +++ b/lib/philomena_web/templates/tag/_tag_editor.html.slime @@ -9,8 +9,28 @@ elixir: .js-tag-block class="fancy-tag-#{@type}" = textarea @f, @name, html_options .js-taginput.input.input--wide.tagsinput.hidden class="js-taginput-fancy" data-click-focus=".js-taginput-input.js-taginput-#{@name}" - input.input class="js-taginput-input js-taginput-#{@name}" id="taginput-fancy-#{@name}" type="text" placeholder="add a tag" autocomplete="off" autocapitalize="none" data-ac="true" data-ac-min-length="3" data-ac-source="/autocomplete/tags?term=" -button.button.button--state-primary.button--bold class="js-taginput-show" data-click-show=".js-taginput-fancy,.js-taginput-hide" data-click-hide=".js-taginput-plain,.js-taginput-show" data-click-focus=".js-taginput-input.js-taginput-#{@name}" - ' Fancy Editor -button.hidden.button.button--state-primary.button--bold class="js-taginput-hide" data-click-show=".js-taginput-plain,.js-taginput-show" data-click-hide=".js-taginput-fancy,.js-taginput-hide" data-click-focus=".js-taginput-plain.js-taginput-#{@name}" - ' Plain Editor + input.input [ + class="js-taginput-input js-taginput-#{@name}" + id="taginput-fancy-#{@name}" + type="text" + placeholder="add a tag" + autocomplete="off" + autocapitalize="none" + data-ac="true" + data-ac-min-length="3" + data-ac-source="/autocomplete/tags?term=" + ] +button.button.button--state-primary.button--bold[ + class="js-taginput-show" + data-click-show=".js-taginput-fancy,.js-taginput-hide" + data-click-hide=".js-taginput-plain,.js-taginput-show" + data-click-focus=".js-taginput-input.js-taginput-#{@name}" +] + | Fancy Editor +button.hidden.button.button--state-primary.button--bold [ + class="js-taginput-hide" + data-click-show=".js-taginput-plain,.js-taginput-show" + data-click-hide=".js-taginput-fancy,.js-taginput-hide" + data-click-focus=".js-taginput-plain.js-taginput-#{@name}" +] + | Plain Editor From 237d2ec0c2fd9220a927a681c79f01d77790aae3 Mon Sep 17 00:00:00 2001 From: MareStare Date: Wed, 12 Feb 2025 05:11:16 +0200 Subject: [PATCH 2/4] Remove extra whitespace from attributes [] syntax Co-authored-by: liamwhite --- lib/philomena_web/templates/tag/_tag_editor.html.slime | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/philomena_web/templates/tag/_tag_editor.html.slime b/lib/philomena_web/templates/tag/_tag_editor.html.slime index 7c157c58..d645fd43 100644 --- a/lib/philomena_web/templates/tag/_tag_editor.html.slime +++ b/lib/philomena_web/templates/tag/_tag_editor.html.slime @@ -9,7 +9,7 @@ elixir: .js-tag-block class="fancy-tag-#{@type}" = textarea @f, @name, html_options .js-taginput.input.input--wide.tagsinput.hidden class="js-taginput-fancy" data-click-focus=".js-taginput-input.js-taginput-#{@name}" - input.input [ + input.input[ class="js-taginput-input js-taginput-#{@name}" id="taginput-fancy-#{@name}" type="text" @@ -27,7 +27,7 @@ button.button.button--state-primary.button--bold[ data-click-focus=".js-taginput-input.js-taginput-#{@name}" ] | Fancy Editor -button.hidden.button.button--state-primary.button--bold [ +button.hidden.button.button--state-primary.button--bold[ class="js-taginput-hide" data-click-show=".js-taginput-plain,.js-taginput-show" data-click-hide=".js-taginput-fancy,.js-taginput-hide" From 20035f6411ca02ab2acd5a0fcaba45617dcc670c Mon Sep 17 00:00:00 2001 From: MareStare Date: Wed, 12 Feb 2025 22:37:52 +0000 Subject: [PATCH 3/4] Rename `ac` to `autocomplete` consistently across all files --- assets/js/autocomplete.ts | 43 +++++++++++-------- .../__tests__/local-autocompleter.spec.ts | 18 ++++---- .../templates/filter/_form.html.slime | 4 +- .../templates/layout/_header.html.slime | 7 +-- .../profile/artist_link/_form.html.slime | 4 +- .../templates/search/_form.html.slime | 2 +- .../templates/tag/_tag_editor.html.slime | 6 +-- 7 files changed, 46 insertions(+), 38 deletions(-) diff --git a/assets/js/autocomplete.ts b/assets/js/autocomplete.ts index b8b7f260..12b9c05f 100644 --- a/assets/js/autocomplete.ts +++ b/assets/js/autocomplete.ts @@ -15,13 +15,16 @@ import { TermSuggestion, } from './utils/suggestions'; -type AcEnabledInputElement = HTMLInputElement | HTMLTextAreaElement; +type AutocompletableInputElement = HTMLInputElement | HTMLTextAreaElement; -function hasAcEnabled(element: unknown): element is AcEnabledInputElement { - return (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && Boolean(element.dataset.ac); +function hasAutocompleteEnabled(element: unknown): element is AutocompletableInputElement { + return ( + (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && + Boolean(element.dataset.autocomplete) + ); } -let inputField: AcEnabledInputElement | null = null; +let inputField: AutocompletableInputElement | null = null; let originalTerm: string | undefined; let originalQuery: string | undefined; let selectedTerm: TermContext | null = null; @@ -29,7 +32,7 @@ let selectedTerm: TermContext | null = null; const popup = new SuggestionsPopup(); function isSearchField(targetInput: HTMLElement): boolean { - return targetInput.dataset.acMode === 'search'; + return targetInput.dataset.autocompleteMode === 'search'; } function restoreOriginalValue() { @@ -117,7 +120,7 @@ function keydownHandler(event: KeyboardEvent) { } } -function findSelectedTerm(targetInput: AcEnabledInputElement, searchQuery: string): TermContext | null { +function findSelectedTerm(targetInput: AutocompletableInputElement, searchQuery: string): TermContext | null { if (targetInput.selectionStart === null || targetInput.selectionEnd === null) return null; const selectionIndex = Math.min(targetInput.selectionStart, targetInput.selectionEnd); @@ -151,11 +154,15 @@ function findSelectedTerm(targetInput: AcEnabledInputElement, searchQuery: strin function toggleSearchNativeAutocomplete() { const enable = store.get('enable_search_ac'); - for (const searchField of $$(':is(input, textarea)[data-ac][data-ac-mode=search]')) { + const searchFields = $$( + ':is(input, textarea)[data-autocomplete][data-autocomplete-mode=search]', + ); + + for (const searchField of searchFields) { if (enable) { searchField.autocomplete = 'off'; } else { - searchField.removeAttribute('data-ac'); + searchField.removeAttribute('data-autocomplete'); searchField.autocomplete = 'on'; } } @@ -166,14 +173,14 @@ function trimPrefixes(targetTerm: string): string { } /** - * We control the autocomplete with `data-ac*` attributes in HTML, and subscribe + * We control the autocomplete with `data-autocomplete*` attributes in HTML, and subscribe * event listeners to the `document`. This pattern is described in more detail * here: https://javascript.info/event-delegation */ export function listenAutocomplete() { let serverSideSuggestionsTimeout: number | undefined; - let localAc: LocalAutocompleter | null = null; + let localAutocomplete: LocalAutocompleter | null = null; document.addEventListener('focusin', loadAutocompleteFromEvent); @@ -182,13 +189,13 @@ export function listenAutocomplete() { loadAutocompleteFromEvent(event); window.clearTimeout(serverSideSuggestionsTimeout); - if (!hasAcEnabled(event.target)) return; + if (!hasAutocompleteEnabled(event.target)) return; const targetedInput = event.target; targetedInput.addEventListener('keydown', keydownHandler as EventListener); - if (localAc !== null) { + if (localAutocomplete !== null) { inputField = targetedInput; let suggestionsCount = 5; @@ -207,7 +214,7 @@ export function listenAutocomplete() { originalTerm = inputField.value.toLowerCase(); } - const suggestions = localAc + const suggestions = localAutocomplete .matchPrefix(trimPrefixes(originalTerm), suggestionsCount) .map(formatLocalAutocompleteResult); @@ -247,16 +254,16 @@ export function listenAutocomplete() { } }); - // Lazy-load the local AC index from the server only once. - let localAcFetchInitiated = false; + // Lazy-load the local autocomplete index from the server only once. + let localAutocompleteFetchNeeded = true; async function loadAutocompleteFromEvent(event: Event) { - if (!hasAcEnabled(event.target) || localAcFetchInitiated) { + if (!localAutocompleteFetchNeeded || !hasAutocompleteEnabled(event.target)) { return; } - localAcFetchInitiated = true; - localAc = await fetchLocalAutocomplete(); + localAutocompleteFetchNeeded = false; + localAutocomplete = await fetchLocalAutocomplete(); } toggleSearchNativeAutocomplete(); diff --git a/assets/js/utils/__tests__/local-autocompleter.spec.ts b/assets/js/utils/__tests__/local-autocompleter.spec.ts index 4dc4ec03..8ff5c01c 100644 --- a/assets/js/utils/__tests__/local-autocompleter.spec.ts +++ b/assets/js/utils/__tests__/local-autocompleter.spec.ts @@ -47,10 +47,10 @@ describe('Local Autocompleter', () => { describe('topK', () => { const termStem = ['f', 'o'].join(''); - let localAc: LocalAutocompleter; + let localAutocomplete: LocalAutocompleter; beforeAll(() => { - localAc = new LocalAutocompleter(mockData); + localAutocomplete = new LocalAutocompleter(mockData); }); beforeEach(() => { @@ -58,17 +58,17 @@ describe('Local Autocompleter', () => { }); it('should return suggestions for exact tag name match', () => { - const result = localAc.matchPrefix('safe', defaultK); + const result = localAutocomplete.matchPrefix('safe', defaultK); expect(result).toEqual([expect.objectContaining({ aliasName: 'safe', name: 'safe', imageCount: 6 })]); }); it('should return suggestion for original tag when passed an alias', () => { - const result = localAc.matchPrefix('flowers', defaultK); + const result = localAutocomplete.matchPrefix('flowers', defaultK); expect(result).toEqual([expect.objectContaining({ aliasName: 'flowers', name: 'flower', imageCount: 1 })]); }); it('should return suggestions sorted by image count', () => { - const result = localAc.matchPrefix(termStem, defaultK); + const result = localAutocomplete.matchPrefix(termStem, defaultK); expect(result).toEqual([ expect.objectContaining({ aliasName: 'forest', name: 'forest', imageCount: 3 }), expect.objectContaining({ aliasName: 'fog', name: 'fog', imageCount: 1 }), @@ -77,25 +77,25 @@ describe('Local Autocompleter', () => { }); it('should return namespaced suggestions without including namespace', () => { - const result = localAc.matchPrefix('test', defaultK); + const result = localAutocomplete.matchPrefix('test', defaultK); expect(result).toEqual([ expect.objectContaining({ aliasName: 'artist:test', name: 'artist:test', imageCount: 1 }), ]); }); it('should return only the required number of suggestions', () => { - const result = localAc.matchPrefix(termStem, 1); + const result = localAutocomplete.matchPrefix(termStem, 1); expect(result).toEqual([expect.objectContaining({ aliasName: 'forest', name: 'forest', imageCount: 3 })]); }); it('should NOT return suggestions associated with hidden tags', () => { window.booru.hiddenTagList = [1]; - const result = localAc.matchPrefix(termStem, defaultK); + const result = localAutocomplete.matchPrefix(termStem, defaultK); expect(result).toEqual([]); }); it('should return empty array for empty prefix', () => { - const result = localAc.matchPrefix('', defaultK); + const result = localAutocomplete.matchPrefix('', defaultK); expect(result).toEqual([]); }); }); diff --git a/lib/philomena_web/templates/filter/_form.html.slime b/lib/philomena_web/templates/filter/_form.html.slime index 2482f71f..95eca5dd 100644 --- a/lib/philomena_web/templates/filter/_form.html.slime +++ b/lib/philomena_web/templates/filter/_form.html.slime @@ -26,7 +26,7 @@ .field = label f, :spoilered_complex_str, "Complex Spoiler Filter" br - = textarea f, :spoilered_complex_str, class: "input input--wide", autocapitalize: "none", data: [ac: "true", ac_min_length: 3, ac_mode: "search"] + = textarea f, :spoilered_complex_str, class: "input input--wide", autocapitalize: "none", data: [autocomplete: "true", autocomplete_min_length: 3, autocomplete_mode: "search"] br = error_tag f, :spoilered_complex_str .fieldlabel @@ -51,7 +51,7 @@ .field = label f, :hidden_complex_str, "Complex Hide Filter" br - = textarea f, :hidden_complex_str, class: "input input--wide", autocapitalize: "none", data: [ac: "true", ac_min_length: 3, ac_mode: "search"] + = textarea f, :hidden_complex_str, class: "input input--wide", autocapitalize: "none", data: [autocomplete: "true", autocomplete_min_length: 3, autocomplete_mode: "search"] br = error_tag f, :hidden_complex_str .fieldlabel diff --git a/lib/philomena_web/templates/layout/_header.html.slime b/lib/philomena_web/templates/layout/_header.html.slime index 9d6ef883..b839e6b8 100644 --- a/lib/philomena_web/templates/layout/_header.html.slime +++ b/lib/philomena_web/templates/layout/_header.html.slime @@ -10,6 +10,7 @@ header.header ' Derpibooru a.header__link.hide-mobile href="/images/new" title="Upload" i.fa.fa-upload + = form_for @conn, ~p"/search", [method: "get", class: "header__search flex flex--no-wrap flex--centered", enforce_utf8: false], fn f -> - title = \ "For terms all required, separate with ',' or 'AND'; also supports 'OR' " <> \ @@ -22,9 +23,9 @@ header.header value=@conn.params["q"] placeholder="Search" autocapitalize="none" - data-ac="true" - data-ac-min-length="3" - data-ac-mode="search" + data-autocomplete="true" + data-autocomplete-min-length="3" + data-autocomplete-mode="search" ] = if present?(@conn.params["sf"]) do diff --git a/lib/philomena_web/templates/profile/artist_link/_form.html.slime b/lib/philomena_web/templates/profile/artist_link/_form.html.slime index cfe75981..00ea7cd7 100644 --- a/lib/philomena_web/templates/profile/artist_link/_form.html.slime +++ b/lib/philomena_web/templates/profile/artist_link/_form.html.slime @@ -7,10 +7,10 @@ .field p label for="tag_name" - ' Artist Link validation is intended for artists. Validating your link will give you control over your content on the site, allowing you to create a + ' Artist Link validation is intended for artists. Validating your link will give you control over your content on the site, allowing you to create a a> href="/commissions" commissions ' listing and request takedowns or DNPs. Do not request a link if the source contains no artwork which you have created. - = text_input f, :tag_name, value: assigns[:tag_name], class: "input", autocomplete: "off", placeholder: "artist:your-name", data: [ac: "true", ac_min_length: "3", ac_source: "/autocomplete/tags?term="] + = text_input f, :tag_name, value: assigns[:tag_name], class: "input", autocomplete: "off", placeholder: "artist:your-name", data: [autocomplete: "true", autocomplete_min_length: "3", autocomplete_source: "/autocomplete/tags?term="] = error_tag f, :tag .field diff --git a/lib/philomena_web/templates/search/_form.html.slime b/lib/philomena_web/templates/search/_form.html.slime index 7d0f080a..adb789bf 100644 --- a/lib/philomena_web/templates/search/_form.html.slime +++ b/lib/philomena_web/templates/search/_form.html.slime @@ -1,7 +1,7 @@ h1 Search = form_for :search, ~p"/search", [id: "searchform", method: "get", class: "js-search-form", enforce_utf8: false], fn f -> - = text_input f, :q, class: "input input--wide js-search-field", placeholder: "Search terms are chained with commas", autocapitalize: "none", name: "q", value: @conn.params["q"], data: [ac: "true", ac_min_length: 3, ac_mode: "search"] + = text_input f, :q, class: "input input--wide js-search-field", placeholder: "Search terms are chained with commas", autocapitalize: "none", name: "q", value: @conn.params["q"], data: [autocomplete: "true", autocomplete_min_length: 3, autocomplete_mode: "search"] .block .block__header.flex diff --git a/lib/philomena_web/templates/tag/_tag_editor.html.slime b/lib/philomena_web/templates/tag/_tag_editor.html.slime index d645fd43..821ff12d 100644 --- a/lib/philomena_web/templates/tag/_tag_editor.html.slime +++ b/lib/philomena_web/templates/tag/_tag_editor.html.slime @@ -16,9 +16,9 @@ elixir: placeholder="add a tag" autocomplete="off" autocapitalize="none" - data-ac="true" - data-ac-min-length="3" - data-ac-source="/autocomplete/tags?term=" + data-autocomplete="true" + data-autocomplete-min-length="3" + data-autocomplete-source="/autocomplete/tags?term=" ] button.button.button--state-primary.button--bold[ class="js-taginput-show" From 9b077ae1950884c3cd57fe6ba2dfc23cca2bed3d Mon Sep 17 00:00:00 2001 From: MareStare Date: Sat, 15 Feb 2025 05:35:18 +0000 Subject: [PATCH 4/4] Fix missing ac -> autocomplete renaming --- assets/js/autocomplete.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/autocomplete.ts b/assets/js/autocomplete.ts index 12b9c05f..191dce14 100644 --- a/assets/js/autocomplete.ts +++ b/assets/js/autocomplete.ts @@ -224,7 +224,7 @@ export function listenAutocomplete() { } } - const { acMinLength: minTermLength, acSource: endpointUrl } = targetedInput.dataset; + const { autocompleteMinLength: minTermLength, autocompleteSource: endpointUrl } = targetedInput.dataset; if (!endpointUrl) return;