UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

149 lines 6.19 kB
import { ObservableValue } from '@furystack/utils'; /** * Service for managing list state including focus, selection, and keyboard navigation */ export class ListService { options; [Symbol.dispose]() { this.items[Symbol.dispose](); this.selection[Symbol.dispose](); this.searchTerm[Symbol.dispose](); this.hasFocus[Symbol.dispose](); this.focusedItem[Symbol.dispose](); } isSelected = (item) => this.selection.getValue().includes(item); addToSelection = (item) => { this.selection.setValue([...this.selection.getValue(), item]); }; removeFromSelection = (item) => { this.selection.setValue(this.selection.getValue().filter((e) => e !== item)); }; toggleSelection = (item) => { if (this.isSelected(item)) { this.removeFromSelection(item); } else { this.addToSelection(item); } }; items = new ObservableValue([]); focusedItem = new ObservableValue(undefined); /** * Stores the focused item captured on pointerdown, before the focus event * can update focusedItem. Used as the anchor for SHIFT+click range selection. * Call {@link setFocusAnchor} from `onpointerdown` to snapshot the anchor * before focus shifts. */ focusAnchor = undefined; /** Snapshot the current focused item as the anchor for SHIFT+click range selection. */ setFocusAnchor() { this.focusAnchor = this.focusedItem.getValue(); } selection = new ObservableValue([]); searchTerm = new ObservableValue(''); hasFocus = new ObservableValue(false); handleKeyDown(ev) { const items = this.items.getValue(); const hasFocus = this.hasFocus.getValue(); const selectedItems = this.selection.getValue(); const focusedItem = this.focusedItem.getValue(); const searchTerm = this.searchTerm.getValue(); if (hasFocus) { switch (ev.key) { case ' ': ev.preventDefault(); if (focusedItem) { this.selection.setValue(selectedItems.includes(focusedItem) ? selectedItems.filter((e) => e !== focusedItem) : [...selectedItems, focusedItem]); } break; case '*': ev.preventDefault(); this.selection.setValue(items.filter((e) => !selectedItems.includes(e))); break; case '+': ev.preventDefault(); this.selection.setValue(items); break; case '-': ev.preventDefault(); this.selection.setValue([]); break; case 'Insert': ev.preventDefault(); if (focusedItem) { if (this.selection.getValue().includes(focusedItem)) { this.selection.setValue([...this.selection.getValue().filter((e) => e !== focusedItem)]); } else { this.selection.setValue([...this.selection.getValue(), focusedItem]); } this.focusedItem.setValue(items[items.findIndex((e) => e === this.focusedItem.getValue()) + 1]); } break; case 'Home': { ev.preventDefault(); this.focusedItem.setValue(items[0]); break; } case 'End': { ev.preventDefault(); this.focusedItem.setValue(items[items.length - 1]); break; } case 'Escape': { ev.preventDefault(); this.searchTerm.setValue(''); this.selection.setValue([]); break; } default: if (this.options.searchField && ev.key.length === 1) { const newSearchExpression = searchTerm + ev.key; const newFocusedItem = items.find((e) => this.options.searchField && e[this.options.searchField]?.toString().startsWith(newSearchExpression)); this.focusedItem.setValue(newFocusedItem); this.searchTerm.setValue(newSearchExpression); } } } } handleItemClick(item, ev) { const currentSelectionValue = this.selection.getValue(); const lastFocused = this.focusAnchor ?? this.focusedItem.getValue(); this.focusAnchor = undefined; if (ev.ctrlKey) { if (currentSelectionValue.includes(item)) { this.selection.setValue(currentSelectionValue.filter((s) => s !== item)); } else { this.selection.setValue([...currentSelectionValue, item]); } } if (ev.shiftKey) { const items = this.items.getValue(); const lastFocusedIndex = items.findIndex((e) => e === lastFocused); const itemIndex = items.findIndex((e) => e === item); const start = Math.min(lastFocusedIndex, itemIndex); const end = Math.max(lastFocusedIndex, itemIndex); const rangeItems = items.slice(start, end + 1); const newSelection = [...currentSelectionValue]; for (const rangeItem of rangeItems) { if (!newSelection.includes(rangeItem)) { newSelection.push(rangeItem); } } this.selection.setValue(newSelection); } this.focusedItem.setValue(item); } /** * Hook for double-click behavior. No-op in base class; overridden by TreeService for expand/collapse. */ handleItemDoubleClick(_item) { } constructor(options = {}) { this.options = options; } } //# sourceMappingURL=list-service.js.map