Rename ac to autocomplete consistently across all files

This commit is contained in:
MareStare 2025-02-12 22:37:52 +00:00
parent 237d2ec0c2
commit 20035f6411
7 changed files with 46 additions and 38 deletions

View file

@ -15,13 +15,16 @@ import {
TermSuggestion, TermSuggestion,
} from './utils/suggestions'; } from './utils/suggestions';
type AcEnabledInputElement = HTMLInputElement | HTMLTextAreaElement; type AutocompletableInputElement = HTMLInputElement | HTMLTextAreaElement;
function hasAcEnabled(element: unknown): element is AcEnabledInputElement { function hasAutocompleteEnabled(element: unknown): element is AutocompletableInputElement {
return (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && Boolean(element.dataset.ac); 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 originalTerm: string | undefined;
let originalQuery: string | undefined; let originalQuery: string | undefined;
let selectedTerm: TermContext | null = null; let selectedTerm: TermContext | null = null;
@ -29,7 +32,7 @@ let selectedTerm: TermContext | null = null;
const popup = new SuggestionsPopup(); const popup = new SuggestionsPopup();
function isSearchField(targetInput: HTMLElement): boolean { function isSearchField(targetInput: HTMLElement): boolean {
return targetInput.dataset.acMode === 'search'; return targetInput.dataset.autocompleteMode === 'search';
} }
function restoreOriginalValue() { 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; if (targetInput.selectionStart === null || targetInput.selectionEnd === null) return null;
const selectionIndex = Math.min(targetInput.selectionStart, targetInput.selectionEnd); const selectionIndex = Math.min(targetInput.selectionStart, targetInput.selectionEnd);
@ -151,11 +154,15 @@ function findSelectedTerm(targetInput: AcEnabledInputElement, searchQuery: strin
function toggleSearchNativeAutocomplete() { function toggleSearchNativeAutocomplete() {
const enable = store.get('enable_search_ac'); const enable = store.get('enable_search_ac');
for (const searchField of $$<AcEnabledInputElement>(':is(input, textarea)[data-ac][data-ac-mode=search]')) { const searchFields = $$<AutocompletableInputElement>(
':is(input, textarea)[data-autocomplete][data-autocomplete-mode=search]',
);
for (const searchField of searchFields) {
if (enable) { if (enable) {
searchField.autocomplete = 'off'; searchField.autocomplete = 'off';
} else { } else {
searchField.removeAttribute('data-ac'); searchField.removeAttribute('data-autocomplete');
searchField.autocomplete = 'on'; 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 * event listeners to the `document`. This pattern is described in more detail
* here: https://javascript.info/event-delegation * here: https://javascript.info/event-delegation
*/ */
export function listenAutocomplete() { export function listenAutocomplete() {
let serverSideSuggestionsTimeout: number | undefined; let serverSideSuggestionsTimeout: number | undefined;
let localAc: LocalAutocompleter | null = null; let localAutocomplete: LocalAutocompleter | null = null;
document.addEventListener('focusin', loadAutocompleteFromEvent); document.addEventListener('focusin', loadAutocompleteFromEvent);
@ -182,13 +189,13 @@ export function listenAutocomplete() {
loadAutocompleteFromEvent(event); loadAutocompleteFromEvent(event);
window.clearTimeout(serverSideSuggestionsTimeout); window.clearTimeout(serverSideSuggestionsTimeout);
if (!hasAcEnabled(event.target)) return; if (!hasAutocompleteEnabled(event.target)) return;
const targetedInput = event.target; const targetedInput = event.target;
targetedInput.addEventListener('keydown', keydownHandler as EventListener); targetedInput.addEventListener('keydown', keydownHandler as EventListener);
if (localAc !== null) { if (localAutocomplete !== null) {
inputField = targetedInput; inputField = targetedInput;
let suggestionsCount = 5; let suggestionsCount = 5;
@ -207,7 +214,7 @@ export function listenAutocomplete() {
originalTerm = inputField.value.toLowerCase(); originalTerm = inputField.value.toLowerCase();
} }
const suggestions = localAc const suggestions = localAutocomplete
.matchPrefix(trimPrefixes(originalTerm), suggestionsCount) .matchPrefix(trimPrefixes(originalTerm), suggestionsCount)
.map(formatLocalAutocompleteResult); .map(formatLocalAutocompleteResult);
@ -247,16 +254,16 @@ export function listenAutocomplete() {
} }
}); });
// Lazy-load the local AC index from the server only once. // Lazy-load the local autocomplete index from the server only once.
let localAcFetchInitiated = false; let localAutocompleteFetchNeeded = true;
async function loadAutocompleteFromEvent(event: Event) { async function loadAutocompleteFromEvent(event: Event) {
if (!hasAcEnabled(event.target) || localAcFetchInitiated) { if (!localAutocompleteFetchNeeded || !hasAutocompleteEnabled(event.target)) {
return; return;
} }
localAcFetchInitiated = true; localAutocompleteFetchNeeded = false;
localAc = await fetchLocalAutocomplete(); localAutocomplete = await fetchLocalAutocomplete();
} }
toggleSearchNativeAutocomplete(); toggleSearchNativeAutocomplete();

View file

@ -47,10 +47,10 @@ describe('Local Autocompleter', () => {
describe('topK', () => { describe('topK', () => {
const termStem = ['f', 'o'].join(''); const termStem = ['f', 'o'].join('');
let localAc: LocalAutocompleter; let localAutocomplete: LocalAutocompleter;
beforeAll(() => { beforeAll(() => {
localAc = new LocalAutocompleter(mockData); localAutocomplete = new LocalAutocompleter(mockData);
}); });
beforeEach(() => { beforeEach(() => {
@ -58,17 +58,17 @@ describe('Local Autocompleter', () => {
}); });
it('should return suggestions for exact tag name match', () => { 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 })]); expect(result).toEqual([expect.objectContaining({ aliasName: 'safe', name: 'safe', imageCount: 6 })]);
}); });
it('should return suggestion for original tag when passed an alias', () => { 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 })]); expect(result).toEqual([expect.objectContaining({ aliasName: 'flowers', name: 'flower', imageCount: 1 })]);
}); });
it('should return suggestions sorted by image count', () => { it('should return suggestions sorted by image count', () => {
const result = localAc.matchPrefix(termStem, defaultK); const result = localAutocomplete.matchPrefix(termStem, defaultK);
expect(result).toEqual([ expect(result).toEqual([
expect.objectContaining({ aliasName: 'forest', name: 'forest', imageCount: 3 }), expect.objectContaining({ aliasName: 'forest', name: 'forest', imageCount: 3 }),
expect.objectContaining({ aliasName: 'fog', name: 'fog', imageCount: 1 }), expect.objectContaining({ aliasName: 'fog', name: 'fog', imageCount: 1 }),
@ -77,25 +77,25 @@ describe('Local Autocompleter', () => {
}); });
it('should return namespaced suggestions without including namespace', () => { it('should return namespaced suggestions without including namespace', () => {
const result = localAc.matchPrefix('test', defaultK); const result = localAutocomplete.matchPrefix('test', defaultK);
expect(result).toEqual([ expect(result).toEqual([
expect.objectContaining({ aliasName: 'artist:test', name: 'artist:test', imageCount: 1 }), expect.objectContaining({ aliasName: 'artist:test', name: 'artist:test', imageCount: 1 }),
]); ]);
}); });
it('should return only the required number of suggestions', () => { 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 })]); expect(result).toEqual([expect.objectContaining({ aliasName: 'forest', name: 'forest', imageCount: 3 })]);
}); });
it('should NOT return suggestions associated with hidden tags', () => { it('should NOT return suggestions associated with hidden tags', () => {
window.booru.hiddenTagList = [1]; window.booru.hiddenTagList = [1];
const result = localAc.matchPrefix(termStem, defaultK); const result = localAutocomplete.matchPrefix(termStem, defaultK);
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
it('should return empty array for empty prefix', () => { it('should return empty array for empty prefix', () => {
const result = localAc.matchPrefix('', defaultK); const result = localAutocomplete.matchPrefix('', defaultK);
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
}); });

View file

@ -26,7 +26,7 @@
.field .field
= label f, :spoilered_complex_str, "Complex Spoiler Filter" = label f, :spoilered_complex_str, "Complex Spoiler Filter"
br 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 br
= error_tag f, :spoilered_complex_str = error_tag f, :spoilered_complex_str
.fieldlabel .fieldlabel
@ -51,7 +51,7 @@
.field .field
= label f, :hidden_complex_str, "Complex Hide Filter" = label f, :hidden_complex_str, "Complex Hide Filter"
br 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 br
= error_tag f, :hidden_complex_str = error_tag f, :hidden_complex_str
.fieldlabel .fieldlabel

View file

@ -10,6 +10,7 @@ header.header
' Derpibooru ' Derpibooru
a.header__link.hide-mobile href="/images/new" title="Upload" a.header__link.hide-mobile href="/images/new" title="Upload"
i.fa.fa-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 -> = form_for @conn, ~p"/search", [method: "get", class: "header__search flex flex--no-wrap flex--centered", enforce_utf8: false], fn f ->
- title = \ - title = \
"For terms all required, separate with ',' or 'AND'; also supports 'OR' " <> \ "For terms all required, separate with ',' or 'AND'; also supports 'OR' " <> \
@ -22,9 +23,9 @@ header.header
value=@conn.params["q"] value=@conn.params["q"]
placeholder="Search" placeholder="Search"
autocapitalize="none" autocapitalize="none"
data-ac="true" data-autocomplete="true"
data-ac-min-length="3" data-autocomplete-min-length="3"
data-ac-mode="search" data-autocomplete-mode="search"
] ]
= if present?(@conn.params["sf"]) do = if present?(@conn.params["sf"]) do

View file

@ -7,10 +7,10 @@
.field .field
p p
label for="tag_name" 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 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. ' 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 = error_tag f, :tag
.field .field

View file

@ -1,7 +1,7 @@
h1 Search h1 Search
= form_for :search, ~p"/search", [id: "searchform", method: "get", class: "js-search-form", enforce_utf8: false], fn f -> = 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
.block__header.flex .block__header.flex

View file

@ -16,9 +16,9 @@ elixir:
placeholder="add a tag" placeholder="add a tag"
autocomplete="off" autocomplete="off"
autocapitalize="none" autocapitalize="none"
data-ac="true" data-autocomplete="true"
data-ac-min-length="3" data-autocomplete-min-length="3"
data-ac-source="/autocomplete/tags?term=" data-autocomplete-source="/autocomplete/tags?term="
] ]
button.button.button--state-primary.button--bold[ button.button.button--state-primary.button--bold[
class="js-taginput-show" class="js-taginput-show"