// src/utils/clientUtils.ts export function createToolSlug(toolName: string): string { if (!toolName || typeof toolName !== 'string') { console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName); return ''; } return toolName.toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } export function findToolByIdentifier(tools: any[], identifier: string): any | undefined { if (!identifier || !Array.isArray(tools)) return undefined; return tools.find((tool: any) => tool.name === identifier || createToolSlug(tool.name) === identifier.toLowerCase() ); } export function isToolHosted(tool: any): boolean { return tool.projectUrl !== undefined && tool.projectUrl !== null && tool.projectUrl !== "" && tool.projectUrl.trim() !== ""; } interface AutocompleteOptions { minLength?: number; maxResults?: number; placeholder?: string; allowMultiple?: boolean; separator?: string; filterFunction?: (query: string) => any[]; renderFunction?: (item: any) => string; hiddenInput?: HTMLInputElement; } export class AutocompleteManager { public input: HTMLInputElement; public dataSource: any[]; public options: AutocompleteOptions; public isOpen: boolean = false; public selectedIndex: number = -1; public filteredData: any[] = []; public selectedItems: Set = new Set(); public dropdown!: HTMLElement; public selectedContainer!: HTMLElement; constructor(inputElement: HTMLInputElement, dataSource: any[], options: AutocompleteOptions = {}) { this.input = inputElement; this.dataSource = dataSource; this.options = { minLength: 1, maxResults: 10, placeholder: 'Type to search...', allowMultiple: false, separator: ', ', filterFunction: this.defaultFilter.bind(this), renderFunction: this.defaultRender.bind(this), ...options }; this.init(); } init(): void { this.createDropdown(); this.bindEvents(); if (this.options.allowMultiple) { this.initMultipleMode(); } } createDropdown(): void { this.dropdown = document.createElement('div'); this.dropdown.className = 'autocomplete-dropdown'; this.dropdown.style.cssText = ` position: absolute; top: 100%; left: 0; right: 0; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.375rem; box-shadow: var(--shadow-lg); max-height: 200px; overflow-y: auto; z-index: 1000; display: none; `; const parentElement = this.input.parentNode as HTMLElement; parentElement.style.position = 'relative'; parentElement.insertBefore(this.dropdown, this.input.nextSibling); } bindEvents(): void { this.input.addEventListener('input', (e) => { this.handleInput((e.target as HTMLInputElement).value); }); this.input.addEventListener('keydown', (e) => { this.handleKeydown(e); }); this.input.addEventListener('focus', () => { if (this.input.value.length >= (this.options.minLength || 1)) { this.showDropdown(); } }); this.input.addEventListener('blur', () => { setTimeout(() => { const activeElement = document.activeElement; if (!activeElement || !this.dropdown.contains(activeElement)) { this.hideDropdown(); } }, 150); }); document.addEventListener('click', (e) => { const target = e.target as Node; if (!this.input.contains(target) && !this.dropdown.contains(target)) { this.hideDropdown(); } }); } initMultipleMode(): void { this.selectedContainer = document.createElement('div'); this.selectedContainer.className = 'autocomplete-selected'; this.selectedContainer.style.cssText = ` display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem; min-height: 1.5rem; `; const parentElement = this.input.parentNode as HTMLElement; parentElement.insertBefore(this.selectedContainer, this.input); this.updateSelectedDisplay(); } handleInput(value: string): void { if (value.length >= (this.options.minLength || 1)) { this.filteredData = this.options.filterFunction!(value); this.selectedIndex = -1; this.renderDropdown(); this.showDropdown(); } else { this.hideDropdown(); } } handleKeydown(e: KeyboardEvent): void { if (!this.isOpen) return; switch (e.key) { case 'ArrowDown': e.preventDefault(); this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1); this.updateHighlight(); break; case 'ArrowUp': e.preventDefault(); this.selectedIndex = Math.max(this.selectedIndex - 1, -1); this.updateHighlight(); break; case 'Enter': e.preventDefault(); if (this.selectedIndex >= 0) { this.selectItem(this.filteredData[this.selectedIndex]); } break; case 'Escape': this.hideDropdown(); break; } } defaultFilter(query: string): any[] { const searchTerm = query.toLowerCase(); return this.dataSource .filter(item => { const text = typeof item === 'string' ? item : item.name || item.label || item.toString(); return text.toLowerCase().includes(searchTerm) && (!this.options.allowMultiple || !this.selectedItems.has(text)); }) .slice(0, this.options.maxResults || 10); } defaultRender(item: any): string { const text = typeof item === 'string' ? item : item.name || item.label || item.toString(); return `
${this.escapeHtml(text)}
`; } renderDropdown(): void { if (this.filteredData.length === 0) { this.dropdown.innerHTML = '
No results found
'; return; } this.dropdown.innerHTML = this.filteredData .map((item, index) => { const content = this.options.renderFunction!(item); return `
${content}
`; }) .join(''); this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => { option.addEventListener('click', () => { this.selectItem(this.filteredData[index]); }); option.addEventListener('mouseenter', () => { this.selectedIndex = index; this.updateHighlight(); }); }); } updateHighlight(): void { this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => { (option as HTMLElement).style.backgroundColor = index === this.selectedIndex ? 'var(--color-bg-secondary)' : 'transparent'; }); } selectItem(item: any): void { const text = typeof item === 'string' ? item : item.name || item.label || item.toString(); if (this.options.allowMultiple) { this.selectedItems.add(text); this.updateSelectedDisplay(); this.updateInputValue(); this.input.value = ''; } else { this.input.value = text; this.hideDropdown(); } this.input.dispatchEvent(new CustomEvent('autocomplete:select', { detail: { item, text, selectedItems: Array.from(this.selectedItems) } })); } removeItem(text: string): void { if (this.options.allowMultiple) { this.selectedItems.delete(text); this.updateSelectedDisplay(); this.updateInputValue(); } } updateSelectedDisplay(): void { if (!this.options.allowMultiple || !this.selectedContainer) return; this.selectedContainer.innerHTML = Array.from(this.selectedItems) .map(item => ` ${this.escapeHtml(item)} `) .join(''); this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); this.removeItem((btn as HTMLElement).getAttribute('data-item')!); }); }); } updateInputValue(): void { if (this.options.allowMultiple && this.options.hiddenInput) { this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator || ', '); } } showDropdown(): void { this.dropdown.style.display = 'block'; this.isOpen = true; } hideDropdown(): void { this.dropdown.style.display = 'none'; this.isOpen = false; this.selectedIndex = -1; } escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } setDataSource(newDataSource: any[]): void { this.dataSource = newDataSource; } getSelectedItems(): string[] { return Array.from(this.selectedItems); } setSelectedItems(items: string[]): void { this.selectedItems = new Set(items); if (this.options.allowMultiple) { this.updateSelectedDisplay(); this.updateInputValue(); } } destroy(): void { if (this.dropdown && this.dropdown.parentNode) { this.dropdown.parentNode.removeChild(this.dropdown); } if (this.selectedContainer && this.selectedContainer.parentNode) { this.selectedContainer.parentNode.removeChild(this.selectedContainer); } } }