mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2025-12-24 07:12:57 +00:00
Removing auto-completion logic
This commit is contained in:
@@ -1,186 +0,0 @@
|
||||
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
|
||||
import {QueryLexer, QuotedTermToken, TermToken, Token} from "$lib/booru/search/QueryLexer.js";
|
||||
|
||||
export class SearchWrapper extends BaseComponent {
|
||||
/** @type {HTMLInputElement|null} */
|
||||
#searchField = null;
|
||||
/** @type {HTMLInputElement|null} */
|
||||
#autoCompleteField = null;
|
||||
/** @type {string|null} */
|
||||
#lastParsedSearchValue = null;
|
||||
/** @type {Token[]} */
|
||||
#cachedParsedQuery = [];
|
||||
|
||||
build() {
|
||||
this.container.classList.add('header__search--completable');
|
||||
|
||||
this.#searchField = this.container.querySelector('input[name=q]');
|
||||
this.#searchField.autocomplete = 'off'; // Browser's auto-complete will get in the way!
|
||||
|
||||
const autoCompleteField = document.createElement('input');
|
||||
autoCompleteField.dataset.ac = 'true';
|
||||
autoCompleteField.dataset.acMinLength = '3';
|
||||
autoCompleteField.dataset.acSource = '/autocomplete/tags?term=';
|
||||
autoCompleteField.classList.add('search-autocomplete-dummy');
|
||||
|
||||
this.#autoCompleteField = autoCompleteField;
|
||||
|
||||
this.container.appendChild(autoCompleteField);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.#searchField.addEventListener('input', this.#updateAutoCompletedFragment.bind(this));
|
||||
this.#searchField.addEventListener('keydown', this.#onSearchFieldKeyPressed.bind(this));
|
||||
this.#searchField.addEventListener('selectionchange', this.#updateAutoCompletedFragment.bind(this));
|
||||
}
|
||||
|
||||
#updateAutoCompletedFragment() {
|
||||
const searchableFragment = this.#findCurrentTagFragment();
|
||||
this.#emitAutoComplete(searchableFragment || '');
|
||||
}
|
||||
|
||||
#getInputUserSelection() {
|
||||
return Math.min(
|
||||
this.#searchField.selectionStart,
|
||||
this.#searchField.selectionEnd
|
||||
);
|
||||
}
|
||||
|
||||
#resolveQueryTokens() {
|
||||
const searchValue = this.#searchField.value;
|
||||
|
||||
if (searchValue === this.#lastParsedSearchValue && this.#cachedParsedQuery) {
|
||||
return this.#cachedParsedQuery;
|
||||
}
|
||||
|
||||
this.#lastParsedSearchValue = searchValue;
|
||||
this.#cachedParsedQuery = new QueryLexer(searchValue).parse();
|
||||
|
||||
return this.#cachedParsedQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
#onSearchFieldKeyPressed(event) {
|
||||
// On enter, attempt to replace the current active tag in the query with autocomplete selection
|
||||
if (event.code === 'Enter') {
|
||||
this.#onEnterPressed(event);
|
||||
}
|
||||
|
||||
this.#autoCompleteField.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
keyCode: event.keyCode
|
||||
})
|
||||
);
|
||||
|
||||
// Similarly to the site's autocomplete logic, we need to prevent the arrows up/down from causing any issues
|
||||
if (event.keyCode === 38 || event.keyCode === 40) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
#onEnterPressed(event) {
|
||||
const autocompleteSelection = document.querySelector('.autocomplete__item--selected');
|
||||
|
||||
if (!autocompleteSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeToken = SearchWrapper.#findActiveSearchTermPosition(
|
||||
this.#resolveQueryTokens(),
|
||||
this.#getInputUserSelection(),
|
||||
);
|
||||
|
||||
if (activeToken instanceof TermToken || activeToken instanceof QuotedTermToken) {
|
||||
const selectionStart = activeToken.index;
|
||||
const selectionEnd = activeToken.index + activeToken.value.length;
|
||||
|
||||
let autocompletedValue = autocompleteSelection.dataset.value;
|
||||
|
||||
if (activeToken instanceof QuotedTermToken) {
|
||||
autocompletedValue = `"${QuotedTermToken.encode(autocompletedValue)}"`;
|
||||
}
|
||||
|
||||
this.#searchField.value = this.#searchField.value.slice(0, selectionStart)
|
||||
+ autocompletedValue
|
||||
+ this.#searchField.value.slice(selectionEnd);
|
||||
|
||||
const newSelectionEnd = selectionStart + autocompletedValue.length;
|
||||
|
||||
// Place the caret at the end of the currently active tag.
|
||||
// Actually, this does not work for some reason. After the tag is sent to the field and selection was changed to
|
||||
// the end of the inserted tag, browser just does not scroll the input to the caret position.
|
||||
this.#searchField.focus();
|
||||
this.#searchField.setSelectionRange(newSelectionEnd, newSelectionEnd);
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string|null}
|
||||
*/
|
||||
#findCurrentTagFragment() {
|
||||
if (!this.#searchField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let searchValue = this.#searchField.value;
|
||||
|
||||
if (!searchValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = SearchWrapper.#findActiveSearchTermPosition(
|
||||
this.#resolveQueryTokens(),
|
||||
this.#getInputUserSelection(),
|
||||
);
|
||||
|
||||
if (token instanceof TermToken) {
|
||||
return token.value;
|
||||
}
|
||||
|
||||
if (token instanceof QuotedTermToken) {
|
||||
return token.decodedValue;
|
||||
}
|
||||
|
||||
return searchValue;
|
||||
}
|
||||
|
||||
#emitAutoComplete(userInputFragment) {
|
||||
this.#autoCompleteField.value = userInputFragment;
|
||||
|
||||
// Should be at least one frame away, since input event always removes autocomplete window
|
||||
requestAnimationFrame(() => {
|
||||
this.#autoCompleteField.dispatchEvent(
|
||||
new InputEvent('input', {bubbles: true})
|
||||
);
|
||||
|
||||
const autocompleteContainer = document.querySelector('.autocomplete');
|
||||
|
||||
if (autocompleteContainer) {
|
||||
autocompleteContainer.style.left = `${this.container.offsetLeft}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loosely estimate where current selected search term is located and return it if found.
|
||||
* @param {Token[]} tokens Search value to find the actively selected term from.
|
||||
* @param {number} userSelectionIndex The index of the user selection.
|
||||
* @return {Token|null} Search term object or NULL if nothing found.
|
||||
*/
|
||||
static #findActiveSearchTermPosition(tokens, userSelectionIndex) {
|
||||
return tokens.find(
|
||||
token => token.index < userSelectionIndex && token.index + token.value.length >= userSelectionIndex
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeSearWrapper(formElement) {
|
||||
new SearchWrapper(formElement).initialize();
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
|
||||
import {SearchWrapper} from "$lib/components/SearchWrapper.js";
|
||||
|
||||
class SiteHeaderWrapper extends BaseComponent {
|
||||
/** @type {SearchWrapper|null} */
|
||||
#searchWrapper = null;
|
||||
|
||||
build() {
|
||||
const searchForm = this.container.querySelector('.header__search');
|
||||
this.#searchWrapper = searchForm && new SearchWrapper(searchForm) || null;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.#searchWrapper) {
|
||||
this.#searchWrapper.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeSiteHeader(siteHeaderElement) {
|
||||
new SiteHeaderWrapper(siteHeaderElement)
|
||||
.initialize();
|
||||
}
|
||||
Reference in New Issue
Block a user