UNPKG

stylescape

Version:

Stylescape is a visual identity framework developed by Scape Agency.

231 lines 8.59 kB
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