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"