205 lines
5.8 KiB
TypeScript
205 lines
5.8 KiB
TypeScript
import { MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
|
import { useApplication } from '../ApplicationProvider'
|
|
import { Table, TableColumn, TableRow, TableSortBy } from './CommonTypes'
|
|
|
|
type TableSortOptions =
|
|
| {
|
|
sortBy: TableSortBy
|
|
sortReversed: boolean
|
|
onSortChange: (sortBy: TableSortBy, reversed: boolean) => void
|
|
}
|
|
| {
|
|
sortBy?: never
|
|
sortReversed?: never
|
|
onSortChange?: never
|
|
}
|
|
|
|
type TableSelectionOptions =
|
|
| {
|
|
enableRowSelection: boolean
|
|
enableMultipleRowSelection?: boolean
|
|
selectedRowIds?: string[]
|
|
onRowSelectionChange?: (rowIds: string[]) => void
|
|
selectionActions?: (selected: string[]) => ReactNode
|
|
showSelectionActions?: boolean
|
|
}
|
|
| {
|
|
enableRowSelection?: never
|
|
enableMultipleRowSelection?: never
|
|
selectedRowIds?: never
|
|
onRowSelectionChange?: never
|
|
selectionActions?: never
|
|
showSelectionActions?: never
|
|
}
|
|
|
|
type TableRowOptions<Data> = {
|
|
getRowId?: (data: Data) => string
|
|
onRowDoubleClick?: (data: Data) => void
|
|
onRowContextMenu?: (x: number, y: number, data: Data) => void
|
|
rowActions?: (data: Data) => ReactNode
|
|
}
|
|
|
|
export type UseTableOptions<Data> = {
|
|
data: Data[]
|
|
columns: TableColumn<Data>[]
|
|
} & TableRowOptions<Data> &
|
|
TableSortOptions &
|
|
TableSelectionOptions
|
|
|
|
export function useTable<Data>({
|
|
data,
|
|
columns,
|
|
sortBy,
|
|
sortReversed,
|
|
onSortChange,
|
|
getRowId,
|
|
enableRowSelection,
|
|
enableMultipleRowSelection,
|
|
selectedRowIds,
|
|
onRowSelectionChange,
|
|
onRowDoubleClick,
|
|
onRowContextMenu,
|
|
rowActions,
|
|
selectionActions,
|
|
showSelectionActions,
|
|
}: UseTableOptions<Data>): Table<Data> {
|
|
const application = useApplication()
|
|
const [selectedRows, setSelectedRows] = useState<string[]>(selectedRowIds || [])
|
|
|
|
useEffect(() => {
|
|
if (selectedRowIds) {
|
|
setSelectedRows(selectedRowIds)
|
|
}
|
|
}, [selectedRowIds])
|
|
|
|
useEffect(() => {
|
|
if (onRowSelectionChange) {
|
|
onRowSelectionChange(selectedRows)
|
|
}
|
|
}, [selectedRows, onRowSelectionChange])
|
|
|
|
const headers = useMemo(
|
|
() =>
|
|
columns.map((column) => {
|
|
return {
|
|
name: column.name,
|
|
isSorting: sortBy && sortBy === column.sortBy,
|
|
sortBy: column.sortBy,
|
|
sortReversed: sortReversed,
|
|
onSortChange: () => {
|
|
if (!onSortChange || !column.sortBy) {
|
|
return
|
|
}
|
|
onSortChange(column.sortBy, sortBy === column.sortBy ? !sortReversed : false)
|
|
},
|
|
}
|
|
}),
|
|
[columns, onSortChange, sortBy, sortReversed],
|
|
)
|
|
|
|
const rows: TableRow<Data>[] = useMemo(
|
|
() =>
|
|
data.map((rowData, index) => {
|
|
const cells = columns.map((column) => {
|
|
return column.cell(rowData)
|
|
})
|
|
const id = getRowId ? getRowId(rowData) : index.toString()
|
|
const row: TableRow<Data> = {
|
|
id,
|
|
isSelected: enableRowSelection ? selectedRows.includes(id) : false,
|
|
cells,
|
|
rowData,
|
|
rowActions: rowActions ? rowActions(rowData) : undefined,
|
|
}
|
|
return row
|
|
}),
|
|
[columns, data, enableRowSelection, getRowId, rowActions, selectedRows],
|
|
)
|
|
|
|
const handleRowClick = useCallback(
|
|
(id: string) => {
|
|
const handler: MouseEventHandler<HTMLTableRowElement> = (event) => {
|
|
if (!enableRowSelection) {
|
|
return
|
|
}
|
|
const isCmdOrCtrlPressed = application.keyboardService.isMac ? event.metaKey : event.ctrlKey
|
|
if (isCmdOrCtrlPressed && enableMultipleRowSelection) {
|
|
setSelectedRows((prev) => (prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id]))
|
|
} else if (event.shiftKey && enableMultipleRowSelection) {
|
|
const lastSelectedIndex = rows.findIndex((row) => row.id === selectedRows[selectedRows.length - 1])
|
|
const currentIndex = rows.findIndex((row) => row.id === id)
|
|
const start = Math.min(lastSelectedIndex, currentIndex)
|
|
const end = Math.max(lastSelectedIndex, currentIndex)
|
|
const newSelectedRows = rows.slice(start, end + 1).map((row) => row.id)
|
|
setSelectedRows(newSelectedRows)
|
|
} else {
|
|
setSelectedRows([id])
|
|
}
|
|
}
|
|
return handler
|
|
},
|
|
[application.keyboardService.isMac, enableMultipleRowSelection, enableRowSelection, rows, selectedRows],
|
|
)
|
|
|
|
const handleRowDoubleClick = useCallback(
|
|
(id: string) => {
|
|
const handler: MouseEventHandler<HTMLTableRowElement> = () => {
|
|
if (!onRowDoubleClick) {
|
|
return
|
|
}
|
|
const rowData = rows.find((row) => row.id === id)?.rowData
|
|
if (rowData) {
|
|
onRowDoubleClick(rowData)
|
|
}
|
|
}
|
|
return handler
|
|
},
|
|
[onRowDoubleClick, rows],
|
|
)
|
|
|
|
const handleRowContextMenu = useCallback(
|
|
(id: string) => {
|
|
const handler: MouseEventHandler<HTMLTableRowElement> = (event) => {
|
|
if (!onRowContextMenu) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
const rowData = rows.find((row) => row.id === id)?.rowData
|
|
if (rowData) {
|
|
setSelectedRows([id])
|
|
onRowContextMenu(event.clientX, event.clientY, rowData)
|
|
}
|
|
}
|
|
return handler
|
|
},
|
|
[onRowContextMenu, rows],
|
|
)
|
|
|
|
const table: Table<Data> = useMemo(
|
|
() => ({
|
|
headers,
|
|
rows,
|
|
handleRowClick,
|
|
handleRowDoubleClick,
|
|
handleRowContextMenu,
|
|
selectedRows,
|
|
canSelectRows: enableRowSelection || false,
|
|
selectionActions: selectionActions ? selectionActions(selectedRows) : undefined,
|
|
showSelectionActions: showSelectionActions || false,
|
|
}),
|
|
[
|
|
enableRowSelection,
|
|
handleRowClick,
|
|
handleRowContextMenu,
|
|
handleRowDoubleClick,
|
|
headers,
|
|
rows,
|
|
selectedRows,
|
|
selectionActions,
|
|
showSelectionActions,
|
|
],
|
|
)
|
|
|
|
return table
|
|
}
|