forensic-pathways/src/utils/clientUtils.ts
overcuriousity d1c297189d cleanup
2025-08-12 22:34:11 +02:00

360 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<string> = 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 `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
}
renderDropdown(): void {
if (this.filteredData.length === 0) {
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
return;
}
this.dropdown.innerHTML = this.filteredData
.map((item, index) => {
const content = this.options.renderFunction!(item);
return `<div class="autocomplete-option" data-index="${index}" style="
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--color-border-light);
transition: background-color 0.15s ease;
">${content}</div>`;
})
.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 => `
<span class="autocomplete-tag" style="
background-color: var(--color-primary);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
">
${this.escapeHtml(item)}
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}" style="
background: none;
border: none;
color: white;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
">×</button>
</span>
`)
.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);
}
}
}