@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
195 lines • 8.32 kB
JavaScript
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