@furystack/shades-common-components
Version:
180 lines (159 loc) • 5.66 kB
text/typescript
import { ObservableValue } from '@furystack/utils'
export interface CollectionData<T> {
entries: T[]
count: number
}
export interface CollectionServiceOptions<T> {
/**
* An optional field that can be used for quick search
*/
searchField?: keyof T
/**
* @param entry The clicked entry
* optional callback for row clicks
*/
onRowClick?: (entry: T) => void
/**
* Optional callback for row double clicks
* @param entry The clicked entry
*/
onRowDoubleClick?: (entry: T) => void
}
export class CollectionService<T> implements Disposable {
public [Symbol.dispose]() {
this.data[Symbol.dispose]()
this.selection[Symbol.dispose]()
this.searchTerm[Symbol.dispose]()
this.hasFocus[Symbol.dispose]()
this.focusedEntry[Symbol.dispose]()
}
public isSelected = (entry: T) => this.selection.getValue().includes(entry)
public addToSelection = (entry: T) => {
this.selection.setValue([...this.selection.getValue(), entry])
}
public removeFromSelection = (entry: T) => {
this.selection.setValue(this.selection.getValue().filter((e) => e !== entry))
}
public toggleSelection = (entry: T) => {
if (this.isSelected(entry)) {
this.removeFromSelection(entry)
} else {
this.addToSelection(entry)
}
}
public data = new ObservableValue<CollectionData<T>>({ count: 0, entries: [] })
public focusedEntry = new ObservableValue<T | undefined>(undefined)
public selection = new ObservableValue<T[]>([])
public searchTerm = new ObservableValue('')
public hasFocus = new ObservableValue(false)
public handleKeyDown(ev: KeyboardEvent) {
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 'ArrowUp':
ev.preventDefault()
this.focusedEntry.setValue(entries[Math.max(0, entries.findIndex((e) => e === focusedEntry) - 1)])
break
case 'ArrowDown':
ev.preventDefault()
this.focusedEntry.setValue(
entries[Math.min(entries.length - 1, entries.findIndex((e) => e === focusedEntry) + 1)],
)
break
case 'Home': {
this.focusedEntry.setValue(entries[0])
break
}
case 'End': {
this.focusedEntry.setValue(entries[entries.length - 1])
break
}
case 'Tab': {
this.hasFocus.setValue(!hasFocus)
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] as string)?.toString().startsWith(newSearchExpression),
)
this.focusedEntry.setValue(newFocusedEntry)
this.searchTerm.setValue(newSearchExpression)
}
}
}
}
public handleRowClick(entry: T, ev: MouseEvent) {
this.options.onRowClick?.(entry)
const currentSelectionValue = this.selection.getValue()
const lastFocused = this.focusedEntry.getValue()
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)
}
constructor(private options: CollectionServiceOptions<T> = {}) {}
public handleRowDoubleClick(entry: T) {
this.options.onRowDoubleClick?.(entry)
}
}