site: rework search component

Don't reimplement a bunch of reactive stuff that Alpine.js handles by
default.
This commit is contained in:
Lucas Gabriel Vuotto 2025-04-28 12:46:29 +00:00
parent 15a98e03d1
commit 07987b7c0e
3 changed files with 102 additions and 101 deletions

View file

@ -1,4 +1,4 @@
function getCurrentWord(s, cursor) {
function getCurrentWordFromCursor(s, cursor) {
return s.substring(s.lastIndexOf(' ', cursor - 1) + 1, cursor);
}
@ -6,109 +6,105 @@ function mySplice(s, idx, del, n) {
return s.substring(0, idx) + n + s.substring(idx + del);
}
function suggestionIndex(el) {
// el.parentNode is ul#tags-suggestion, which include a template. That's why
// a -1 is needed here.
const idx = [...el.parentNode.children].indexOf(el);
return idx <= 0 ? null : idx - 1;
}
function currentSuggestionElement(idx) {
if (idx === null)
return null;
return document.querySelector("#tags-suggestions").children[idx + 1];
}
document.addEventListener('alpine:init', () => {
Alpine.data('tagsSuggestions', () => ({
suggestions: [],
currentSuggestion: null,
inputCursor: 0,
query: '',
Alpine.data('tagsSuggestions', (initialSearch = '') => ({
items: [],
activeItem: null,
search: initialSearch,
cursor: 0,
currentWord: '',
open: false,
reset() {
this.suggestions = [];
this.currentSuggestion = null;
this.inputCursor = 0;
this.query = '';
get sortedItems() {
return this.items.toSorted((a, b) => b.count - a.count);
},
async fetchSuggestions(evt) {
this.inputCursor = evt.target.selectionStart;
this.query = getCurrentWord(evt.target.value, this.inputCursor);
if (!this.query) {
this.reset();
async fetchSuggestions() {
const currentWord = getCurrentWordFromCursor(this.search, this.cursor);
if (!currentWord)
return;
if (currentWord === this.currentWord) {
/* Show previous items, if any. */
this.open = true;
return;
}
this.currentWord = currentWord;
const params = new URLSearchParams({q: this.query});
const params = new URLSearchParams({q: currentWord});
const response = await fetch(`${endpoints.search}?${params}`);
if (!response.ok) {
this.reset();
this.items = [];
return;
}
this.suggestions = await response.json();
this.items = await response.json();
this.open = true;
},
completeSuggestion() {
if (this.currentSuggestion === null)
fetchSuggestionsOnFocus(evt) {
this.fetchSuggestions();
},
fetchSuggestionsOnInput(evt) {
this.cursor = evt.target.selectionStart;
this.fetchSuggestions();
},
autocompleteSuggestion() {
if (this.items.length === 0)
return;
const value = this.$refs.tags.value;
const pos = Math.max(this.inputCursor - this.query.length, 0);
const tag = this.suggestions[this.currentSuggestion].display;
const newValue = mySplice(value, pos, this.query.length, tag + ' ');
const pos = Math.max(this.cursor - this.currentWord.length, 0);
const tag = this.items[this.activeItem ?? 0].display;
this.reset();
this.$refs.tags.value = newValue;
this.inputCursor =
this.$refs.tags.selectionStart =
this.$refs.tags.selectionEnd =
this.activeItem = null;
this.search = mySplice(this.search, pos, this.currentWord.length,
tag + ' ');
this.cursor =
this.$refs.search.selectionStart =
this.$refs.search.selectionEnd =
pos + tag.length + 1;
this.currentWord = '';
this.open = false;
this.$refs.tags.focus();
this.$refs.search.focus();
},
completeClickedSuggestion(evt) {
this.currentSuggestion = suggestionIndex(evt.currentTarget);
this.completeSuggestion();
autocompleteItem(evt) {
if (this.currentWord !== '') {
this.autocompleteSuggestion();
evt.preventDefault();
}
},
setCurrentSuggestion(idx) {
const len = this.suggestions.length;
if (len === 0)
return;
currentSuggestionElement(this.currentSuggestion)
?.classList.remove("suggestion-hover");
idx = idx % len;
if (idx < 0)
idx += len;
this.currentSuggestion = idx;
currentSuggestionElement(this.currentSuggestion)
?.classList.add("suggestion-hover");
getListItemIndexOf(el) {
return [...this.$refs.suggestions.querySelectorAll('li')].indexOf(el);
},
setPointerSuggestion(evt) {
this.setCurrentSuggestion(suggestionIndex(evt.currentTarget));
autocompletePointedItem(evt) {
this.activeItem = this.getListItemIndexOf(evt.currentTarget);
this.autocompleteSuggestion();
},
moveCurrentSuggestion(amount, defaultValue) {
this.setCurrentSuggestion(this.currentSuggestion === null ?
defaultValue : this.currentSuggestion + amount);
setHoveredItem(evt) {
this.activeItem = this.getListItemIndexOf(evt.currentTarget);
},
nextSuggestion(evt) {
this.moveCurrentSuggestion(1, 0);
previousItem(evt) {
const len = this.items.length;
let item = this.activeItem === null ? -1 : this.activeItem - 1;
item = item % len;
if (item < 0)
item += len;
this.activeItem = item;
},
previousSuggestion(evt) {
this.moveCurrentSuggestion(-1, -1);
nextItem(evt) {
const len = this.items.length;
let item = this.activeItem === null ? 0 : this.activeItem + 1;
item = item % len;
this.activeItem = item;
},
}));
});