stylescape
Version:
Stylescape is a visual identity framework developed by Scape Agency.
231 lines • 8.59 kB
JavaScript
export class AutocompleteManager {
constructor(inputSelectorOrElement, options = {}) {
this.container = null;
this.activeIndex = -1;
this.isOpen = false;
this.debounceTimer = null;
this.currentSuggestions = [];
this.handleInput = async () => {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = window.setTimeout(async () => {
const query = this.input?.value.trim() || "";
if (query.length < this.options.minChars) {
this.close();
return;
}
await this.updateSuggestions(query);
}, this.options.debounce);
};
this.handleKeyDown = (e) => {
if (!this.isOpen) {
if (e.key === "ArrowDown" && this.currentSuggestions.length > 0) {
e.preventDefault();
this.open();
}
return;
}
switch (e.key) {
case "ArrowDown":
e.preventDefault();
this.moveActive(1);
break;
case "ArrowUp":
e.preventDefault();
this.moveActive(-1);
break;
case "Enter":
e.preventDefault();
if (this.activeIndex >= 0) {
this.selectSuggestion(this.activeIndex);
}
break;
case "Escape":
this.close();
break;
case "Tab":
this.close();
break;
}
};
this.handleBlur = () => {
setTimeout(() => this.close(), 150);
};
this.input =
typeof inputSelectorOrElement === "string"
? document.querySelector(inputSelectorOrElement)
: inputSelectorOrElement;
this.options = {
suggestions: options.suggestions ?? [],
fetchSuggestions: options.fetchSuggestions ?? (async () => []),
minChars: options.minChars ?? 1,
maxResults: options.maxResults ?? 10,
debounce: options.debounce ?? 200,
highlight: options.highlight !== false,
containerClass: options.containerClass ?? "autocomplete",
itemClass: options.itemClass ?? "autocomplete__item",
highlightClass: options.highlightClass ?? "autocomplete__highlight",
activeClass: options.activeClass ?? "autocomplete__item--active",
onSelect: options.onSelect ?? (() => { }),
};
if (!this.input) {
console.warn("[Stylescape] AutocompleteManager input not found");
return;
}
this.init();
}
setSuggestions(suggestions) {
this.options.suggestions = suggestions;
}
open() {
if (!this.container || this.currentSuggestions.length === 0)
return;
this.container.style.display = "block";
this.isOpen = true;
this.input?.setAttribute("aria-expanded", "true");
}
close() {
if (!this.container)
return;
this.container.style.display = "none";
this.isOpen = false;
this.activeIndex = -1;
this.input?.setAttribute("aria-expanded", "false");
this.clearActive();
}
destroy() {
this.close();
this.container?.remove();
this.input?.removeEventListener("input", this.handleInput);
this.input?.removeEventListener("keydown", this.handleKeyDown);
this.input?.removeEventListener("blur", this.handleBlur);
this.input = null;
this.container = null;
}
init() {
if (!this.input)
return;
this.createContainer();
this.input.setAttribute("role", "combobox");
this.input.setAttribute("aria-autocomplete", "list");
this.input.setAttribute("aria-expanded", "false");
this.input.setAttribute("aria-haspopup", "listbox");
this.input.addEventListener("input", this.handleInput);
this.input.addEventListener("keydown", this.handleKeyDown);
this.input.addEventListener("blur", this.handleBlur);
this.input.addEventListener("focus", () => {
if (this.currentSuggestions.length > 0)
this.open();
});
}
createContainer() {
this.container = document.createElement("div");
this.container.className = this.options.containerClass;
this.container.setAttribute("role", "listbox");
this.container.style.display = "none";
this.container.style.position = "absolute";
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
if (this.input) {
this.input.parentNode?.insertBefore(wrapper, this.input);
wrapper.appendChild(this.input);
}
wrapper.appendChild(this.container);
}
async updateSuggestions(query) {
let suggestions;
if (this.options.fetchSuggestions &&
this.options.suggestions.length === 0) {
suggestions = await this.options.fetchSuggestions(query);
}
else {
const lowerQuery = query.toLowerCase();
suggestions = this.options.suggestions.filter((s) => s.toLowerCase().includes(lowerQuery));
}
this.currentSuggestions = suggestions.slice(0, this.options.maxResults);
this.renderSuggestions(query);
if (this.currentSuggestions.length > 0) {
this.open();
}
else {
this.close();
}
}
renderSuggestions(query) {
if (!this.container)
return;
this.container.innerHTML = "";
this.activeIndex = -1;
this.currentSuggestions.forEach((suggestion, index) => {
const item = document.createElement("div");
item.className = this.options.itemClass;
item.setAttribute("role", "option");
item.setAttribute("data-index", String(index));
if (this.options.highlight) {
item.innerHTML = this.highlightMatch(suggestion, query);
}
else {
item.textContent = suggestion;
}
item.addEventListener("mousedown", (e) => {
e.preventDefault();
this.selectSuggestion(index);
});
item.addEventListener("mouseenter", () => {
this.setActive(index);
});
this.container?.appendChild(item);
});
}
highlightMatch(text, query) {
const regex = new RegExp(`(${this.escapeRegex(query)})`, "gi");
return text.replace(regex, `<span class="${this.options.highlightClass}">$1</span>`);
}
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
moveActive(delta) {
const newIndex = this.activeIndex + delta;
const maxIndex = this.currentSuggestions.length - 1;
if (newIndex < 0) {
this.setActive(maxIndex);
}
else if (newIndex > maxIndex) {
this.setActive(0);
}
else {
this.setActive(newIndex);
}
}
setActive(index) {
this.clearActive();
this.activeIndex = index;
const item = this.container?.querySelector(`[data-index="${index}"]`);
if (item) {
item.classList.add(this.options.activeClass);
item.setAttribute("aria-selected", "true");
item.scrollIntoView({ block: "nearest" });
}
}
clearActive() {
this.container
?.querySelectorAll(`.${this.options.activeClass}`)
.forEach((el) => {
el.classList.remove(this.options.activeClass);
el.setAttribute("aria-selected", "false");
});
}
selectSuggestion(index) {
const value = this.currentSuggestions[index];
if (!value || !this.input)
return;
this.input.value = value;
this.close();
const item = this.container?.querySelector(`[data-index="${index}"]`);
this.options.onSelect(value, item);
this.input.dispatchEvent(new Event("change", { bubbles: true }));
}
}
export default AutocompleteManager;
//# sourceMappingURL=AutocompleteManager.js.map