UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

195 lines 8.32 kB
import { EventHub, ObservableValue } from '@furystack/utils'; export class CollectionService extends EventHub { options; dataSubscription; [Symbol.dispose]() { this.dataSubscription?.[Symbol.dispose](); this.data[Symbol.dispose](); this.selection[Symbol.dispose](); this.searchTerm[Symbol.dispose](); this.hasFocus[Symbol.dispose](); this.focusedEntry[Symbol.dispose](); super[Symbol.dispose](); } isSelected = (entry) => this.selection.getValue().includes(entry); addToSelection = (entry) => { this.selection.setValue([...this.selection.getValue(), entry]); }; removeFromSelection = (entry) => { this.selection.setValue(this.selection.getValue().filter((e) => e !== entry)); }; toggleSelection = (entry) => { if (this.isSelected(entry)) { this.removeFromSelection(entry); } else { this.addToSelection(entry); } }; data = new ObservableValue({ count: 0, entries: [] }); focusedEntry = new ObservableValue(undefined); /** * Stores the focused entry captured on pointerdown, before the focus event * can update focusedEntry. 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 entry as the anchor for SHIFT+click range selection. */ setFocusAnchor() { this.focusAnchor = this.focusedEntry.getValue(); } selection = new ObservableValue([]); searchTerm = new ObservableValue(''); hasFocus = new ObservableValue(false); handleKeyDown(ev) { const { entries } = this.data.getValue(); const hasFocus = this.hasFocus.getValue(); const selectedEntries = this.selection.getValue(); const focusedEntry = this.focusedEntry.getValue(); const searchTerm = this.searchTerm.getValue(); if (hasFocus) { switch (ev.key) { case ' ': ev.preventDefault(); if (focusedEntry) { this.selection.setValue(selectedEntries.includes(focusedEntry) ? selectedEntries.filter((e) => e !== focusedEntry) : [...selectedEntries, focusedEntry]); } break; case '*': this.selection.setValue(entries.filter((e) => !selectedEntries.includes(e))); break; case '+': this.selection.setValue(entries); break; case '-': this.selection.setValue([]); break; case 'Insert': if (focusedEntry) { if (this.selection.getValue().includes(focusedEntry)) { this.selection.setValue([...this.selection.getValue().filter((e) => e !== focusedEntry)]); } else { this.selection.setValue([...this.selection.getValue(), focusedEntry]); } this.focusedEntry.setValue(entries[entries.findIndex((e) => e === this.focusedEntry.getValue()) + 1]); } break; case 'ArrowDown': { if (focusedEntry !== undefined) { const currentIndex = entries.indexOf(focusedEntry); if (currentIndex >= 0 && currentIndex < entries.length - 1) { ev.preventDefault(); this.focusedEntry.setValue(entries[currentIndex + 1]); } } break; } case 'ArrowUp': { if (focusedEntry !== undefined) { const currentIndex = entries.indexOf(focusedEntry); if (currentIndex > 0) { ev.preventDefault(); this.focusedEntry.setValue(entries[currentIndex - 1]); } } break; } case 'Home': { ev.preventDefault(); this.focusedEntry.setValue(entries[0]); break; } case 'End': { ev.preventDefault(); this.focusedEntry.setValue(entries[entries.length - 1]); break; } case 'Escape': { this.searchTerm.setValue(''); this.selection.setValue([]); break; } default: if (this.options.searchField && ev.key.length === 1) { const newSearchExpression = searchTerm + ev.key; const newFocusedEntry = entries.find((e) => this.options.searchField && e[this.options.searchField]?.toString().startsWith(newSearchExpression)); this.focusedEntry.setValue(newFocusedEntry); this.searchTerm.setValue(newSearchExpression); } } } } handleRowClick(entry, ev) { this.emit('onRowClick', entry); const currentSelectionValue = this.selection.getValue(); const lastFocused = this.focusAnchor ?? this.focusedEntry.getValue(); this.focusAnchor = undefined; if (ev.ctrlKey) { if (currentSelectionValue.includes(entry)) { this.selection.setValue(currentSelectionValue.filter((s) => s !== entry)); } else { this.selection.setValue([...currentSelectionValue, entry]); } } if (ev.shiftKey) { const lastFocusedIndex = this.data.getValue().entries.findIndex((e) => e === lastFocused); const entryIndex = this.data.getValue().entries.findIndex((e) => e === entry); const selection = [...currentSelectionValue]; if (lastFocusedIndex > entryIndex) { for (let i = entryIndex; i <= lastFocusedIndex; i++) { selection.push(this.data.getValue().entries[i]); } } else { for (let i = lastFocusedIndex; i <= entryIndex; i++) { selection.push(this.data.getValue().entries[i]); } } this.selection.setValue(selection); } this.focusedEntry.setValue(entry); } reconcileRefs(entries) { const { idField } = this.options; if (!idField) return; const currentFocused = this.focusedEntry.getValue(); if (currentFocused) { const reconciled = entries.find((e) => e[idField] === currentFocused[idField]); if (reconciled !== currentFocused) { this.focusedEntry.setValue(reconciled); } } if (this.focusAnchor) { const anchor = this.focusAnchor; this.focusAnchor = entries.find((e) => e[idField] === anchor[idField]); } const currentSelection = this.selection.getValue(); if (currentSelection.length > 0) { const entryById = new Map(entries.map((e) => [e[idField], e])); const reconciled = currentSelection.map((s) => entryById.get(s[idField])).filter((e) => e !== undefined); if (reconciled.length !== currentSelection.length || reconciled.some((e, i) => e !== currentSelection[i])) { this.selection.setValue(reconciled); } } } constructor(options = {}) { super(); this.options = options; if (options.idField) { this.dataSubscription = this.data.subscribe(({ entries }) => { this.reconcileRefs(entries); }); } } handleRowDoubleClick(entry) { this.emit('onRowDoubleClick', entry); } } //# sourceMappingURL=collection-service.js.map