Skip to content

Commit 6aea297

Browse files
committed
rename selectionAndAnchor to selection for simplicity
1 parent 7396902 commit 6aea297

File tree

4 files changed

+219
-220
lines changed

4 files changed

+219
-220
lines changed

src/HighTable.tsx

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { ReactNode, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
22
import { DataFrame, Row, asyncRows } from './dataframe.js'
3-
import { SelectionAndAnchor, areAllSelected, extendFromAnchor, isSelected, toggleAll, toggleIndex } from './selection.js'
3+
import { Selection, areAllSelected, extendFromAnchor, isSelected, toggleAll, toggleIndex } from './selection.js'
44
import { OrderBy } from './sort.js'
55
import TableHeader, { cellStyle } from './TableHeader.js'
66
export { AsyncRow, DataFrame, ResolvablePromise, Row, arrayDataFrame, asyncRows, awaitRow, awaitRows, resolvablePromise, resolvableRow, sortableDataFrame, wrapPromise } from './dataframe.js'
77
export { rowCache } from './rowCache.js'
8-
export { SelectionAndAnchor } from './selection.js'
9-
export type { Selection } from './selection.js'
8+
export { Selection } from './selection.js'
109
export { OrderBy } from './sort.js'
1110
export { HighTable }
1211

@@ -75,8 +74,8 @@ export interface TableProps {
7574
onError?: (error: Error) => void
7675
orderBy?: OrderBy // order by column. If undefined, the table is unordered, the sort elements are hidden and the interactions are disabled.
7776
onOrderByChange?: (orderBy: OrderBy) => void // callback to call when a user interaction changes the order. The interactions are disabled if undefined.
78-
selectionAndAnchor?: SelectionAndAnchor // selection and anchor rows. If undefined, the selection is hidden and the interactions are disabled.
79-
onSelectionAndAnchorChange?: (selectionAndAnchor: SelectionAndAnchor) => void // callback to call when a user interaction changes the selection. The interactions are disabled if undefined.
77+
selection?: Selection // selection and anchor rows. If undefined, the selection is hidden and the interactions are disabled.
78+
onSelectionChange?: (selection: Selection) => void // callback to call when a user interaction changes the selection. The interactions are disabled if undefined.
8079
}
8180

8281
function rowLabel(rowIndex?: number): string {
@@ -101,8 +100,8 @@ export default function HighTable({
101100
focus = true,
102101
orderBy: propOrderBy,
103102
onOrderByChange: propOnOrderByChange,
104-
selectionAndAnchor: propSelection,
105-
onSelectionAndAnchorChange: propOnSelectionAndAnchorChange,
103+
selection: propSelection,
104+
onSelectionChange: propOnSelectionChange,
106105
onDoubleClickCell,
107106
onMouseDownCell,
108107
onError = console.error,
@@ -173,76 +172,76 @@ export default function HighTable({
173172

174173
/**
175174
* Four modes:
176-
* - controlled (selection and onSelectionAndAnchorChange are defined): the parent controls the selection and receives the user interactions. No local state.
177-
* - controlled read-only (selection is defined, onSelectionAndAnchorChange is undefined): the parent controls the selection and the user interactions are disabled. No local state.
178-
* - uncontrolled (selection is undefined, onSelectionAndAnchorChange is defined): the component controls the selection and the user interactions. Local state.
179-
* - disabled (selection and onSelectionAndAnchorChange are undefined): the selection is hidden and the user interactions are disabled. No local state.
175+
* - controlled (selection and onSelectionChange are defined): the parent controls the selection and receives the user interactions. No local state.
176+
* - controlled read-only (selection is defined, onSelectionChange is undefined): the parent controls the selection and the user interactions are disabled. No local state.
177+
* - uncontrolled (selection is undefined, onSelectionChange is defined): the component controls the selection and the user interactions. Local state.
178+
* - disabled (selection and onSelectionChange are undefined): the selection is hidden and the user interactions are disabled. No local state.
180179
*/
181-
const [initialSelection] = useState<SelectionAndAnchor | undefined>(propSelection)
182-
const [localSelection, setLocalSelection] = useState<SelectionAndAnchor | undefined>({ selection: [], anchor: undefined })
180+
const [initialSelection] = useState<Selection | undefined>(propSelection)
181+
const [localSelection, setLocalSelection] = useState<Selection | undefined>({ ranges: [], anchor: undefined })
183182
const isSelectionControlled = initialSelection !== undefined
184-
const enableSelectionInteractions = propOnSelectionAndAnchorChange !== undefined
185-
let selectionAndAnchor: SelectionAndAnchor | undefined
183+
const enableSelectionInteractions = propOnSelectionChange !== undefined
184+
let selection: Selection | undefined
186185
if (isSelectionControlled) {
187186
if (propSelection === undefined) {
188187
console.warn('The component selection is controlled (is has no local state) because "selection" was initially defined. "selection" cannot be set to undefined now (it is set back to the initial value).')
189-
selectionAndAnchor = initialSelection
188+
selection = initialSelection
190189
} else {
191-
selectionAndAnchor = propSelection
190+
selection = propSelection
192191
}
193192
} else {
194193
if (propSelection !== undefined) {
195194
console.warn('The component selection is uncontrolled (it only has a local state) because "selection" was initially undefined. "selection" cannot be set to a value now and is ignored.')
196195
}
197-
if (propOnSelectionAndAnchorChange === undefined) {
198-
// The component selection is disabled because onSelectionAndAnchorChange is undefined. If you want to enable selection, you must provide onSelectionAndAnchorChange
199-
selectionAndAnchor = undefined
196+
if (propOnSelectionChange === undefined) {
197+
// The component selection is disabled because onSelectionChange is undefined. If you want to enable selection, you must provide onSelectionChange
198+
selection = undefined
200199
} else {
201-
selectionAndAnchor = localSelection
200+
selection = localSelection
202201
}
203202
}
204-
const onSelectionAndAnchorChange = useCallback((selectionAndAnchor: SelectionAndAnchor) => {
203+
const onSelectionChange = useCallback((selection: Selection) => {
205204
if (!enableSelectionInteractions) {
206205
return
207206
}
208-
propOnSelectionAndAnchorChange?.(selectionAndAnchor)
207+
propOnSelectionChange?.(selection)
209208
if (!isSelectionControlled) {
210-
setLocalSelection(selectionAndAnchor)
209+
setLocalSelection(selection)
211210
}
212-
}, [propOnSelectionAndAnchorChange, isSelectionControlled, enableSelectionInteractions])
211+
}, [propOnSelectionChange, isSelectionControlled, enableSelectionInteractions])
213212

214-
const showSelection = selectionAndAnchor !== undefined
213+
const showSelection = selection !== undefined
215214
const showSelectionControls = showSelection && enableSelectionInteractions
216215
const getOnSelectAllRows = useCallback(() => {
217-
if (!selectionAndAnchor || !onSelectionAndAnchorChange) return
218-
const { selection } = selectionAndAnchor
219-
return () => onSelectionAndAnchorChange({
220-
selection: toggleAll({ selection, length: data.numRows }),
216+
if (!selection || !onSelectionChange) return
217+
const { ranges } = selection
218+
return () => onSelectionChange({
219+
ranges: toggleAll({ ranges, length: data.numRows }),
221220
anchor: undefined,
222221
})
223-
}, [onSelectionAndAnchorChange, data.numRows, selectionAndAnchor])
222+
}, [onSelectionChange, data.numRows, selection])
224223
const getOnSelectRowClick = useCallback((tableIndex: number) => {
225-
if (!selectionAndAnchor || !onSelectionAndAnchorChange) return
226-
const { selection, anchor } = selectionAndAnchor
224+
if (!selection || !onSelectionChange) return
225+
const { ranges, anchor } = selection
227226
return (event: React.MouseEvent) => {
228-
const useAnchor = event.shiftKey && selectionAndAnchor.anchor !== undefined
227+
const useAnchor = event.shiftKey && selection.anchor !== undefined
229228
if (useAnchor) {
230-
onSelectionAndAnchorChange({ selection: extendFromAnchor({ selection, anchor, index: tableIndex }), anchor })
229+
onSelectionChange({ ranges: extendFromAnchor({ ranges, anchor, index: tableIndex }), anchor })
231230
} else {
232-
onSelectionAndAnchorChange({ selection: toggleIndex({ selection, index: tableIndex }), anchor: tableIndex })
231+
onSelectionChange({ ranges: toggleIndex({ ranges, index: tableIndex }), anchor: tableIndex })
233232
}
234233
}
235-
}, [onSelectionAndAnchorChange, selectionAndAnchor])
234+
}, [onSelectionChange, selection])
236235
const allRowsSelected = useMemo(() => {
237-
if (!selectionAndAnchor) return false
238-
const { selection } = selectionAndAnchor
239-
return areAllSelected({ selection, length: data.numRows })
240-
}, [selectionAndAnchor, data.numRows])
236+
if (!selection) return false
237+
const { ranges } = selection
238+
return areAllSelected({ ranges, length: data.numRows })
239+
}, [selection, data.numRows])
241240
const isRowSelected = useCallback((tableIndex: number) => {
242-
if (!selectionAndAnchor) return undefined
243-
const { selection } = selectionAndAnchor
244-
return isSelected({ selection, index: tableIndex })
245-
}, [selectionAndAnchor])
241+
if (!selection) return undefined
242+
const { ranges } = selection
243+
return isSelected({ ranges, index: tableIndex })
244+
}, [selection])
246245

247246
const offsetTopRef = useRef(0)
248247

@@ -261,7 +260,7 @@ export default function HighTable({
261260
dispatch({ type: 'DATA_CHANGED', data })
262261
// if uncontrolled, reset the selection (otherwise, it's the responsibility of the parent to do it if the data changes)
263262
if (!isSelectionControlled) {
264-
onSelectionAndAnchorChange?.({ selection: [], anchor: undefined })
263+
onSelectionChange?.({ ranges: [], anchor: undefined })
265264
}
266265
}
267266

src/selection.ts

Lines changed: 58 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/**
2-
* A selection is an array of ordered and non-overlapping ranges.
2+
* A selection is modelled as an array of ordered and non-overlapping ranges.
33
* The ranges are separated, ie. the end of one range is strictly less than the start of the next range.
44
*/
5-
export type Selection = Array<Range>
5+
export type Ranges = Array<Range>
66

7-
export interface SelectionAndAnchor {
8-
selection: Selection // rows selection. The values are indexes of the virtual table (sorted rows), and thus depend on the order.
7+
export interface Selection {
8+
ranges: Ranges // rows selection. The values are indexes of the virtual table (sorted rows), and thus depend on the order.
99
anchor?: number // anchor row used as a reference for shift+click selection. It's a virtual table index (sorted), and thus depends on the order.
1010
}
1111

@@ -24,162 +24,162 @@ export function isValidRange(range: Range): boolean {
2424
&& range.end > range.start
2525
}
2626

27-
export function isValidSelection(selection: Selection): boolean {
28-
if (selection.length === 0) {
27+
export function areValidRanges(ranges: Ranges): boolean {
28+
if (ranges.length === 0) {
2929
return true
3030
}
31-
if (selection.some(range => !isValidRange(range))) {
31+
if (ranges.some(range => !isValidRange(range))) {
3232
return false
3333
}
34-
for (let i = 0; i < selection.length - 1; i++) {
35-
if (selection[i].end >= selection[i + 1].start) {
34+
for (let i = 0; i < ranges.length - 1; i++) {
35+
if (ranges[i].end >= ranges[i + 1].start) {
3636
return false
3737
}
3838
}
3939
return true
4040
}
4141

42-
export function isSelected({ selection, index }: { selection: Selection, index: number }): boolean {
42+
export function isSelected({ ranges, index }: { ranges: Ranges, index: number }): boolean {
4343
if (!isValidIndex(index)) {
4444
throw new Error('Invalid index')
4545
}
46-
if (!isValidSelection(selection)) {
47-
throw new Error('Invalid selection')
46+
if (!areValidRanges(ranges)) {
47+
throw new Error('Invalid ranges')
4848
}
49-
return selection.some(range => range.start <= index && index < range.end)
49+
return ranges.some(range => range.start <= index && index < range.end)
5050
}
5151

52-
export function areAllSelected({ selection, length }: { selection: Selection, length: number }): boolean {
53-
if (!isValidSelection(selection)) {
54-
throw new Error('Invalid selection')
52+
export function areAllSelected({ ranges, length }: { ranges: Ranges, length: number }): boolean {
53+
if (!areValidRanges(ranges)) {
54+
throw new Error('Invalid ranges')
5555
}
5656
if (length && !isValidIndex(length)) {
5757
throw new Error('Invalid length')
5858
}
59-
return selection.length === 1 && selection[0].start === 0 && selection[0].end === length
59+
return ranges.length === 1 && ranges[0].start === 0 && ranges[0].end === length
6060
}
6161

62-
export function toggleAll({ selection, length }: { selection: Selection, length: number }): Selection {
63-
if (!isValidSelection(selection)) {
64-
throw new Error('Invalid selection')
62+
export function toggleAll({ ranges, length }: { ranges: Ranges, length: number }): Ranges {
63+
if (!areValidRanges(ranges)) {
64+
throw new Error('Invalid ranges')
6565
}
6666
if (length && !isValidIndex(length)) {
6767
throw new Error('Invalid length')
6868
}
69-
if (areAllSelected({ selection, length })) {
69+
if (areAllSelected({ ranges, length })) {
7070
return []
7171
}
7272
return [{ start: 0, end: length }]
7373
}
7474

75-
export function selectRange({ selection, range }: { selection: Selection, range: Range }): Selection {
76-
if (!isValidSelection(selection)) {
77-
throw new Error('Invalid selection')
75+
export function selectRange({ ranges, range }: { ranges: Ranges, range: Range }): Ranges {
76+
if (!areValidRanges(ranges)) {
77+
throw new Error('Invalid ranges')
7878
}
7979
if (!isValidRange(range)) {
8080
throw new Error('Invalid range')
8181
}
82-
const newSelection: Selection = []
82+
const newRanges: Ranges = []
8383
const { start, end } = range
8484
let rangeIndex = 0
8585

8686
// copy the ranges before the new range
87-
while (rangeIndex < selection.length && selection[rangeIndex].end < start) {
88-
newSelection.push({ ...selection[rangeIndex] })
87+
while (rangeIndex < ranges.length && ranges[rangeIndex].end < start) {
88+
newRanges.push({ ...ranges[rangeIndex] })
8989
rangeIndex++
9090
}
9191

9292
// merge with the new range
93-
while (rangeIndex < selection.length && selection[rangeIndex].start <= end) {
94-
range.start = Math.min(range.start, selection[rangeIndex].start)
95-
range.end = Math.max(range.end, selection[rangeIndex].end)
93+
while (rangeIndex < ranges.length && ranges[rangeIndex].start <= end) {
94+
range.start = Math.min(range.start, ranges[rangeIndex].start)
95+
range.end = Math.max(range.end, ranges[rangeIndex].end)
9696
rangeIndex++
9797
}
98-
newSelection.push(range)
98+
newRanges.push(range)
9999

100100
// copy the remaining ranges
101-
while (rangeIndex < selection.length) {
102-
newSelection.push({ ...selection[rangeIndex] })
101+
while (rangeIndex < ranges.length) {
102+
newRanges.push({ ...ranges[rangeIndex] })
103103
rangeIndex++
104104
}
105105

106-
return newSelection
106+
return newRanges
107107
}
108108

109-
export function unselectRange({ selection, range }: { selection: Selection, range: Range }): Selection {
110-
if (!isValidSelection(selection)) {
111-
throw new Error('Invalid selection')
109+
export function unselectRange({ ranges, range }: { ranges: Ranges, range: Range }): Ranges {
110+
if (!areValidRanges(ranges)) {
111+
throw new Error('Invalid ranges')
112112
}
113113
if (!isValidRange(range)) {
114114
throw new Error('Invalid range')
115115
}
116-
const newSelection: Selection = []
116+
const newRanges: Ranges = []
117117
const { start, end } = range
118118
let rangeIndex = 0
119119

120120
// copy the ranges before the new range
121-
while (rangeIndex < selection.length && selection[rangeIndex].end < start) {
122-
newSelection.push({ ...selection[rangeIndex] })
121+
while (rangeIndex < ranges.length && ranges[rangeIndex].end < start) {
122+
newRanges.push({ ...ranges[rangeIndex] })
123123
rangeIndex++
124124
}
125125

126126
// split the ranges intersecting with the new range
127-
while (rangeIndex < selection.length && selection[rangeIndex].start < end) {
128-
if (selection[rangeIndex].start < start) {
129-
newSelection.push({ start: selection[rangeIndex].start, end: start })
127+
while (rangeIndex < ranges.length && ranges[rangeIndex].start < end) {
128+
if (ranges[rangeIndex].start < start) {
129+
newRanges.push({ start: ranges[rangeIndex].start, end: start })
130130
}
131-
if (selection[rangeIndex].end > end) {
132-
newSelection.push({ start: end, end: selection[rangeIndex].end })
131+
if (ranges[rangeIndex].end > end) {
132+
newRanges.push({ start: end, end: ranges[rangeIndex].end })
133133
}
134134
rangeIndex++
135135
}
136136

137137
// copy the remaining ranges
138-
while (rangeIndex < selection.length) {
139-
newSelection.push({ ...selection[rangeIndex] })
138+
while (rangeIndex < ranges.length) {
139+
newRanges.push({ ...ranges[rangeIndex] })
140140
rangeIndex++
141141
}
142142

143-
return newSelection
143+
return newRanges
144144
}
145145

146146
/**
147147
* Extend selection state from anchor to index (selecting or unselecting the range).
148148
* Both bounds are inclusive.
149149
* It will handle the shift+click behavior. anchor is the first index clicked, index is the last index clicked.
150150
*/
151-
export function extendFromAnchor({ selection, anchor, index }: { selection: Selection, anchor?: number, index: number }): Selection {
152-
if (!isValidSelection(selection)) {
153-
throw new Error('Invalid selection')
151+
export function extendFromAnchor({ ranges, anchor, index }: { ranges: Ranges, anchor?: number, index: number }): Ranges {
152+
if (!areValidRanges(ranges)) {
153+
throw new Error('Invalid ranges')
154154
}
155155
if (anchor === undefined) {
156156
// no anchor to start the range, no operation
157-
return selection
157+
return ranges
158158
}
159159
if (!isValidIndex(anchor) || !isValidIndex(index)) {
160160
throw new Error('Invalid index')
161161
}
162162
if (anchor === index) {
163163
// no operation
164-
return selection
164+
return ranges
165165
}
166166
const range = anchor < index ? { start: anchor, end: index + 1 } : { start: index, end: anchor + 1 }
167167
if (!isValidRange(range)) {
168168
throw new Error('Invalid range')
169169
}
170-
if (isSelected({ selection, index: anchor })) {
170+
if (isSelected({ ranges, index: anchor })) {
171171
// select the rest of the range
172-
return selectRange({ selection, range })
172+
return selectRange({ ranges, range })
173173
} else {
174174
// unselect the rest of the range
175-
return unselectRange({ selection, range })
175+
return unselectRange({ ranges, range })
176176
}
177177
}
178178

179-
export function toggleIndex({ selection, index }: { selection: Selection, index: number }): Selection {
179+
export function toggleIndex({ ranges, index }: { ranges: Ranges, index: number }): Ranges {
180180
if (!isValidIndex(index)) {
181181
throw new Error('Invalid index')
182182
}
183183
const range = { start: index, end: index + 1 }
184-
return isSelected({ selection, index }) ? unselectRange({ selection, range }) : selectRange({ selection, range })
184+
return isSelected({ ranges, index }) ? unselectRange({ ranges, range }) : selectRange({ ranges, range })
185185
}

0 commit comments

Comments
 (0)