• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

glideapps / glide-data-grid / 7312551980

24 Dec 2023 04:45AM UTC coverage: 90.577% (+4.2%) from 86.42%
7312551980

Pull #810

github

web-flow
Allow control of cell activation behavior (#829)

* Allow control of cell activation behavior

* Add unit tests

* Cleanup

* Fix double click with touch

* Improve comment
Pull Request #810: 6.0.0

2590 of 3242 branches covered (0.0%)

3348 of 3816 new or added lines in 62 files covered. (87.74%)

265 existing lines in 12 files now uncovered.

15745 of 17383 relevant lines covered (90.58%)

3077.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.36
/packages/core/src/data-editor/data-editor.tsx
1
/* eslint-disable sonarjs/no-duplicate-string */
1✔
2
import * as React from "react";
1✔
3
import { assert, assertNever, maybe } from "../common/support.js";
1✔
4
import clamp from "lodash/clamp.js";
1✔
5
import uniq from "lodash/uniq.js";
1✔
6
import flatten from "lodash/flatten.js";
1✔
7
import range from "lodash/range.js";
1✔
8
import debounce from "lodash/debounce.js";
1✔
9
import {
1✔
10
    type EditableGridCell,
1✔
11
    type GridCell,
1✔
12
    GridCellKind,
1✔
13
    type GridSelection,
1✔
14
    isEditableGridCell,
1✔
15
    type Rectangle,
1✔
16
    isReadWriteCell,
1✔
17
    type InnerGridCell,
1✔
18
    InnerGridCellKind,
1✔
19
    CompactSelection,
1✔
20
    type Slice,
1✔
21
    isInnerOnlyCell,
1✔
22
    type ProvideEditorCallback,
1✔
23
    type GridColumn,
1✔
24
    isObjectEditorCallbackResult,
1✔
25
    type Item,
1✔
26
    type MarkerCell,
1✔
27
    headerCellUnheckedMarker,
1✔
28
    headerCellCheckedMarker,
1✔
29
    headerCellIndeterminateMarker,
1✔
30
    type ValidatedGridCell,
1✔
31
    type ImageEditorType,
1✔
32
    type CustomCell,
1✔
33
    BooleanEmpty,
1✔
34
    BooleanIndeterminate,
1✔
35
    type FillHandleDirection,
1✔
36
    type EditListItem,
1✔
37
} from "../internal/data-grid/data-grid-types.js";
1✔
38
import DataGridSearch, { type DataGridSearchProps } from "../internal/data-grid-search/data-grid-search.js";
1✔
39
import { browserIsOSX } from "../common/browser-detect.js";
1✔
40
import {
1✔
41
    getDataEditorTheme,
1✔
42
    makeCSSStyle,
1✔
43
    type FullTheme,
1✔
44
    type Theme,
1✔
45
    ThemeContext,
1✔
46
    mergeAndRealizeTheme,
1✔
47
} from "../common/styles.js";
1✔
48
import type { DataGridRef } from "../internal/data-grid/data-grid.js";
1✔
49
import { getScrollBarWidth, useEventListener, useStateWithReactiveInput, whenDefined } from "../common/utils.js";
1✔
50
import {
1✔
51
    isGroupEqual,
1✔
52
    itemsAreEqual,
1✔
53
    itemIsInRect,
1✔
54
    gridSelectionHasItem,
1✔
55
} from "../internal/data-grid/data-grid-lib.js";
1✔
56
import { GroupRename } from "./group-rename.js";
1✔
57
import { measureColumn, useColumnSizer } from "./use-column-sizer.js";
1✔
58
import { isHotkey } from "../common/is-hotkey.js";
1✔
59
import { type SelectionBlending, useSelectionBehavior } from "../internal/data-grid/use-selection-behavior.js";
1✔
60
import { useCellsForSelection } from "./use-cells-for-selection.js";
1✔
61
import { unquote, expandSelection, copyToClipboard, toggleBoolean } from "./data-editor-fns.js";
1✔
62
import { DataEditorContainer } from "../internal/data-editor-container/data-grid-container.js";
1✔
63
import { useAutoscroll } from "./use-autoscroll.js";
1✔
64
import type { CustomRenderer, CellRenderer, InternalCellRenderer } from "../cells/cell-types.js";
1✔
65
import { decodeHTML, type CopyBuffer } from "./copy-paste.js";
1✔
66
import { useRemAdjuster } from "./use-rem-adjuster.js";
1✔
67
import { type Highlight } from "../internal/data-grid/data-grid-render.js";
1✔
68
import { withAlpha } from "../internal/data-grid/color-parser.js";
1✔
69
import { combineRects, getClosestRect } from "../common/math.js";
1✔
70
import {
1✔
71
    type HeaderClickedEventArgs,
1✔
72
    type GroupHeaderClickedEventArgs,
1✔
73
    type CellClickedEventArgs,
1✔
74
    type FillPatternEventArgs,
1✔
75
    type GridMouseEventArgs,
1✔
76
    groupHeaderKind,
1✔
77
    outOfBoundsKind,
1✔
78
    type GridMouseCellEventArgs,
1✔
79
    headerKind,
1✔
80
    type GridDragEventArgs,
1✔
81
    mouseEventArgsAreEqual,
1✔
82
    type GridKeyEventArgs,
1✔
83
} from "../internal/data-grid/event-args.js";
1✔
84

1✔
85
const DataGridOverlayEditor = React.lazy(
1✔
86
    async () => await import("../internal/data-grid-overlay-editor/data-grid-overlay-editor.js")
1✔
87
);
1✔
88

1✔
89
let idCounter = 0;
1✔
90

1✔
91
interface MouseState {
1✔
92
    readonly previousSelection?: GridSelection;
1✔
93
    readonly fillHandle?: boolean;
1✔
94
}
1✔
95

1✔
96
type Props = Partial<
1✔
97
    Omit<
1✔
98
        DataGridSearchProps,
1✔
99
        | "accessibilityHeight"
1✔
100
        | "canvasRef"
1✔
101
        | "cellXOffset"
1✔
102
        | "cellYOffset"
1✔
103
        | "className"
1✔
104
        | "clientSize"
1✔
105
        | "columns"
1✔
106
        | "disabledRows"
1✔
107
        | "enableGroups"
1✔
108
        | "firstColAccessible"
1✔
109
        | "firstColSticky"
1✔
110
        | "freezeColumns"
1✔
111
        | "getCellContent"
1✔
112
        | "getCellRenderer"
1✔
113
        | "getCellsForSelection"
1✔
114
        | "gridRef"
1✔
115
        | "groupHeaderHeight"
1✔
116
        | "headerHeight"
1✔
117
        | "isFilling"
1✔
118
        | "isFocused"
1✔
119
        | "imageWindowLoader"
1✔
120
        | "lockColumns"
1✔
121
        | "maxColumnWidth"
1✔
122
        | "minColumnWidth"
1✔
123
        | "nonGrowWidth"
1✔
124
        | "onCanvasBlur"
1✔
125
        | "onCanvasFocused"
1✔
126
        | "onCellFocused"
1✔
127
        | "onContextMenu"
1✔
128
        | "onDragEnd"
1✔
129
        | "onMouseDown"
1✔
130
        | "onMouseMove"
1✔
131
        | "onMouseUp"
1✔
132
        | "onVisibleRegionChanged"
1✔
133
        | "rowHeight"
1✔
134
        | "rows"
1✔
135
        | "scrollRef"
1✔
136
        | "searchInputRef"
1✔
137
        | "selectedColumns"
1✔
138
        | "selection"
1✔
139
        | "theme"
1✔
140
        | "trailingRowType"
1✔
141
        | "translateX"
1✔
142
        | "translateY"
1✔
143
        | "verticalBorder"
1✔
144
    >
1✔
145
>;
1✔
146

1✔
147
type EmitEvents = "copy" | "paste" | "delete" | "fill-right" | "fill-down";
1✔
148

1✔
149
function getSpanStops(cells: readonly (readonly GridCell[])[]): number[] {
5✔
150
    return uniq(
5✔
151
        flatten(
5✔
152
            flatten(cells)
5✔
153
                .filter(c => c.span !== undefined)
5✔
154
                .map(c => range((c.span?.[0] ?? 0) + 1, (c.span?.[1] ?? 0) + 1))
5✔
155
        )
5✔
156
    );
5✔
157
}
5✔
158

1✔
159
function shiftSelection(input: GridSelection, offset: number): GridSelection {
283✔
160
    if (input === undefined || offset === 0 || (input.columns.length === 0 && input.current === undefined))
283✔
161
        return input;
283✔
162

35✔
163
    return {
35✔
164
        current:
35✔
165
            input.current === undefined
35✔
166
                ? undefined
1✔
167
                : {
34✔
168
                      cell: [input.current.cell[0] + offset, input.current.cell[1]],
34✔
169
                      range: {
34✔
170
                          ...input.current.range,
34✔
171
                          x: input.current.range.x + offset,
34✔
172
                      },
34✔
173
                      rangeStack: input.current.rangeStack.map(r => ({
34✔
UNCOV
174
                          ...r,
×
UNCOV
175
                          x: r.x + offset,
×
176
                      })),
34✔
177
                  },
34✔
178
        rows: input.rows,
283✔
179
        columns: input.columns.offset(offset),
283✔
180
    };
283✔
181
}
283✔
182

1✔
183
export interface Keybinds {
1✔
184
    readonly selectAll: boolean;
1✔
185
    readonly selectRow: boolean;
1✔
186
    readonly selectColumn: boolean;
1✔
187
    readonly downFill: boolean;
1✔
188
    readonly rightFill: boolean;
1✔
189
    readonly pageUp: boolean;
1✔
190
    readonly pageDown: boolean;
1✔
191
    readonly clear: boolean;
1✔
192
    readonly copy: boolean;
1✔
193
    readonly paste: boolean;
1✔
194
    readonly cut: boolean;
1✔
195
    readonly search: boolean;
1✔
196
    readonly first: boolean;
1✔
197
    readonly last: boolean;
1✔
198
}
1✔
199

1✔
200
const keybindingDefaults: Keybinds = {
1✔
201
    selectAll: true,
1✔
202
    selectRow: true,
1✔
203
    selectColumn: true,
1✔
204
    downFill: false,
1✔
205
    rightFill: false,
1✔
206
    pageUp: true,
1✔
207
    pageDown: true,
1✔
208
    clear: true,
1✔
209
    copy: true,
1✔
210
    paste: true,
1✔
211
    cut: true,
1✔
212
    search: false,
1✔
213
    first: true,
1✔
214
    last: true,
1✔
215
};
1✔
216

1✔
217
/**
1✔
218
 * @category DataEditor
1✔
219
 */
1✔
220
export interface DataEditorProps extends Props, Pick<DataGridSearchProps, "imageWindowLoader"> {
1✔
221
    /** Emitted whenever the user has requested the deletion of the selection.
1✔
222
     * @group Editing
1✔
223
     */
1✔
224
    readonly onDelete?: (selection: GridSelection) => boolean | GridSelection;
1✔
225
    /** Emitted whenever a cell edit is completed.
1✔
226
     * @group Editing
1✔
227
     */
1✔
228
    readonly onCellEdited?: (cell: Item, newValue: EditableGridCell) => void;
1✔
229
    /** Emitted whenever a cell mutation is completed and provides all edits inbound as a single batch.
1✔
230
     * @group Editing
1✔
231
     */
1✔
232
    readonly onCellsEdited?: (newValues: readonly EditListItem[]) => boolean | void;
1✔
233
    /** Emitted whenever a row append operation is requested. Append location can be set in callback.
1✔
234
     * @group Editing
1✔
235
     */
1✔
236
    readonly onRowAppended?: () => Promise<"top" | "bottom" | number | undefined> | void;
1✔
237
    /** Emitted when a column header should show a context menu. Usually right click.
1✔
238
     * @group Events
1✔
239
     */
1✔
240
    readonly onHeaderClicked?: (colIndex: number, event: HeaderClickedEventArgs) => void;
1✔
241
    /** Emitted when a group header is clicked.
1✔
242
     * @group Events
1✔
243
     */
1✔
244
    readonly onGroupHeaderClicked?: (colIndex: number, event: GroupHeaderClickedEventArgs) => void;
1✔
245
    /** Emitted whe the user wishes to rename a group.
1✔
246
     * @group Events
1✔
247
     */
1✔
248
    readonly onGroupHeaderRenamed?: (groupName: string, newVal: string) => void;
1✔
249
    /** Emitted when a cell is clicked.
1✔
250
     * @group Events
1✔
251
     */
1✔
252
    readonly onCellClicked?: (cell: Item, event: CellClickedEventArgs) => void;
1✔
253
    /** Emitted when a cell is activated, by pressing Enter, Space or double clicking it.
1✔
254
     * @group Events
1✔
255
     */
1✔
256
    readonly onCellActivated?: (cell: Item) => void;
1✔
257

1✔
258
    /**
1✔
259
     * Emitted whenever the user initiats a pattern fill using the fill handle. This event provides both
1✔
260
     * a patternSource region and a fillDestination region, and can be prevented.
1✔
261
     * @group Editing
1✔
262
     */
1✔
263
    readonly onFillPattern?: (event: FillPatternEventArgs) => void;
1✔
264
    /** Emitted when editing has finished, regardless of data changing or not.
1✔
265
     * @group Editing
1✔
266
     */
1✔
267
    readonly onFinishedEditing?: (newValue: GridCell | undefined, movement: Item) => void;
1✔
268
    /** Emitted when a column header should show a context menu. Usually right click.
1✔
269
     * @group Events
1✔
270
     */
1✔
271
    readonly onHeaderContextMenu?: (colIndex: number, event: HeaderClickedEventArgs) => void;
1✔
272
    /** Emitted when a group header should show a context menu. Usually right click.
1✔
273
     * @group Events
1✔
274
     */
1✔
275
    readonly onGroupHeaderContextMenu?: (colIndex: number, event: GroupHeaderClickedEventArgs) => void;
1✔
276
    /** Emitted when a cell should show a context menu. Usually right click.
1✔
277
     * @group Events
1✔
278
     */
1✔
279
    readonly onCellContextMenu?: (cell: Item, event: CellClickedEventArgs) => void;
1✔
280
    /** Used for validating cell values during editing.
1✔
281
     * @group Editing
1✔
282
     * @param cell The cell which is being validated.
1✔
283
     * @param newValue The new value being proposed.
1✔
284
     * @param prevValue The previous value before the edit.
1✔
285
     * @returns A return of false indicates the value will not be accepted. A value of
1✔
286
     * true indicates the value will be accepted. Returning a new GridCell will immediately coerce the value to match.
1✔
287
     */
1✔
288
    readonly validateCell?: (
1✔
289
        cell: Item,
1✔
290
        newValue: EditableGridCell,
1✔
291
        prevValue: GridCell
1✔
292
    ) => boolean | ValidatedGridCell;
1✔
293

1✔
294
    /** The columns to display in the data grid.
1✔
295
     * @group Data
1✔
296
     */
1✔
297
    readonly columns: readonly GridColumn[];
1✔
298

1✔
299
    /** Controls the trailing row used to insert new data into the grid.
1✔
300
     * @group Editing
1✔
301
     */
1✔
302
    readonly trailingRowOptions?: {
1✔
303
        /** If the trailing row should be tinted */
1✔
304
        readonly tint?: boolean;
1✔
305
        /** A hint string displayed on hover. Usually something like "New row" */
1✔
306
        readonly hint?: string;
1✔
307
        /** When set to true, the trailing row is always visible. */
1✔
308
        readonly sticky?: boolean;
1✔
309
        /** The icon to use for the cell. Either a GridColumnIcon or a member of the passed headerIcons */
1✔
310
        readonly addIcon?: string;
1✔
311
        /** Overrides the column to focus when a new row is created. */
1✔
312
        readonly targetColumn?: number | GridColumn;
1✔
313
    };
1✔
314
    /** Controls the height of the header row
1✔
315
     * @defaultValue 36
1✔
316
     * @group Style
1✔
317
     */
1✔
318
    readonly headerHeight?: number;
1✔
319
    /** Controls the header of the group header row
1✔
320
     * @defaultValue `headerHeight`
1✔
321
     * @group Style
1✔
322
     */
1✔
323
    readonly groupHeaderHeight?: number;
1✔
324

1✔
325
    /**
1✔
326
     * The number of rows in the grid.
1✔
327
     * @group Data
1✔
328
     */
1✔
329
    readonly rows: number;
1✔
330

1✔
331
    /** Determines if row markers should be automatically added to the grid.
1✔
332
     * Interactive row markers allow the user to select a row.
1✔
333
     *
1✔
334
     * - "clickable-number" renders a number that can be clicked to
1✔
335
     *   select the row
1✔
336
     * - "both" causes the row marker to show up as a number but
1✔
337
     *   reveal a checkbox when the marker is hovered.
1✔
338
     *
1✔
339
     * @defaultValue `none`
1✔
340
     * @group Style
1✔
341
     */
1✔
342
    readonly rowMarkers?: "checkbox" | "number" | "clickable-number" | "checkbox-visible" | "both" | "none";
1✔
343
    /**
1✔
344
     * Sets the width of row markers in pixels, if unset row markers will automatically size.
1✔
345
     * @group Style
1✔
346
     */
1✔
347
    readonly rowMarkerWidth?: number;
1✔
348
    /** Changes the starting index for row markers.
1✔
349
     * @defaultValue 1
1✔
350
     * @group Style
1✔
351
     */
1✔
352
    readonly rowMarkerStartIndex?: number;
1✔
353

1✔
354
    /** Changes the theme of the row marker column
1✔
355
     * @group Style
1✔
356
     */
1✔
357
    readonly rowMarkerTheme?: Partial<Theme>;
1✔
358

1✔
359
    /** Sets the width of the data grid.
1✔
360
     * @group Style
1✔
361
     */
1✔
362
    readonly width?: number | string;
1✔
363
    /** Sets the height of the data grid.
1✔
364
     * @group Style
1✔
365
     */
1✔
366
    readonly height?: number | string;
1✔
367
    /** Custom classname for data grid wrapper.
1✔
368
     * @group Style
1✔
369
     */
1✔
370
    readonly className?: string;
1✔
371

1✔
372
    /** If set to `default`, `gridSelection` will be coerced to always include full spans.
1✔
373
     * @group Selection
1✔
374
     * @defaultValue `default`
1✔
375
     */
1✔
376
    readonly spanRangeBehavior?: "default" | "allowPartial";
1✔
377

1✔
378
    /** Controls which types of selections can exist at the same time in the grid. If selection blending is set to
1✔
379
     * exclusive, the grid will clear other types of selections when the exclusive selection is made. By default row,
1✔
380
     * column, and range selections are exclusive.
1✔
381
     * @group Selection
1✔
382
     * @defaultValue `exclusive`
1✔
383
     * */
1✔
384
    readonly rangeSelectionBlending?: SelectionBlending;
1✔
385
    /** {@inheritDoc rangeSelectionBlending}
1✔
386
     * @group Selection
1✔
387
     */
1✔
388
    readonly columnSelectionBlending?: SelectionBlending;
1✔
389
    /** {@inheritDoc rangeSelectionBlending}
1✔
390
     * @group Selection
1✔
391
     */
1✔
392
    readonly rowSelectionBlending?: SelectionBlending;
1✔
393
    /** Controls if multi-selection is allowed. If disabled, shift/ctrl/command clicking will work as if no modifiers
1✔
394
     * are pressed.
1✔
395
     *
1✔
396
     * When range select is set to cell, only one cell may be selected at a time. When set to rect one one rect at a
1✔
397
     * time. The multi variants allow for multiples of the rect or cell to be selected.
1✔
398
     * @group Selection
1✔
399
     * @defaultValue `rect`
1✔
400
     */
1✔
401
    readonly rangeSelect?: "none" | "cell" | "rect" | "multi-cell" | "multi-rect";
1✔
402
    /** {@inheritDoc rangeSelect}
1✔
403
     * @group Selection
1✔
404
     * @defaultValue `multi`
1✔
405
     */
1✔
406
    readonly columnSelect?: "none" | "single" | "multi";
1✔
407
    /** {@inheritDoc rangeSelect}
1✔
408
     * @group Selection
1✔
409
     * @defaultValue `multi`
1✔
410
     */
1✔
411
    readonly rowSelect?: "none" | "single" | "multi";
1✔
412

1✔
413
    /** Sets the initial scroll Y offset.
1✔
414
     * @see {@link scrollOffsetX}
1✔
415
     * @group Advanced
1✔
416
     */
1✔
417
    readonly scrollOffsetY?: number;
1✔
418
    /** Sets the initial scroll X offset
1✔
419
     * @see {@link scrollOffsetY}
1✔
420
     * @group Advanced
1✔
421
     */
1✔
422
    readonly scrollOffsetX?: number;
1✔
423

1✔
424
    /** Determins the height of each row.
1✔
425
     * @group Style
1✔
426
     * @defaultValue 34
1✔
427
     */
1✔
428
    readonly rowHeight?: DataGridSearchProps["rowHeight"];
1✔
429
    /** Fires whenever the mouse moves
1✔
430
     * @group Events
1✔
431
     * @param args
1✔
432
     */
1✔
433
    readonly onMouseMove?: DataGridSearchProps["onMouseMove"];
1✔
434

1✔
435
    /**
1✔
436
     * The minimum width a column can be resized to.
1✔
437
     * @defaultValue 50
1✔
438
     * @group Style
1✔
439
     */
1✔
440
    readonly minColumnWidth?: DataGridSearchProps["minColumnWidth"];
1✔
441
    /**
1✔
442
     * The maximum width a column can be resized to.
1✔
443
     * @defaultValue 500
1✔
444
     * @group Style
1✔
445
     */
1✔
446
    readonly maxColumnWidth?: DataGridSearchProps["maxColumnWidth"];
1✔
447
    /**
1✔
448
     * The maximum width a column can be automatically sized to.
1✔
449
     * @defaultValue `maxColumnWidth`
1✔
450
     * @group Style
1✔
451
     */
1✔
452
    readonly maxColumnAutoWidth?: number;
1✔
453

1✔
454
    /**
1✔
455
     * Used to provide an override to the default image editor for the data grid. `provideEditor` may be a better
1✔
456
     * choice for most people.
1✔
457
     * @group Advanced
1✔
458
     * */
1✔
459
    readonly imageEditorOverride?: ImageEditorType;
1✔
460
    /**
1✔
461
     * If specified, it will be used to render Markdown, instead of the default Markdown renderer used by the Grid.
1✔
462
     * You'll want to use this if you need to process your Markdown for security purposes, or if you want to use a
1✔
463
     * renderer with different Markdown features.
1✔
464
     * @group Advanced
1✔
465
     */
1✔
466
    readonly markdownDivCreateNode?: (content: string) => DocumentFragment;
1✔
467

1✔
468
    /** Callback for providing a custom editor for a cell.
1✔
469
     * @group Editing
1✔
470
     */
1✔
471
    readonly provideEditor?: ProvideEditorCallback<GridCell>;
1✔
472
    /**
1✔
473
     * Allows coercion of pasted values.
1✔
474
     * @group Editing
1✔
475
     * @param val The pasted value
1✔
476
     * @param cell The cell being pasted into
1✔
477
     * @returns `undefined` to accept default behavior or a `GridCell` which should be used to represent the pasted value.
1✔
478
     */
1✔
479
    readonly coercePasteValue?: (val: string, cell: GridCell) => GridCell | undefined;
1✔
480

1✔
481
    /**
1✔
482
     * Emitted when the grid selection is cleared.
1✔
483
     * @group Selection
1✔
484
     */
1✔
485
    readonly onSelectionCleared?: () => void;
1✔
486

1✔
487
    /**
1✔
488
     * The current selection of the data grid. Contains all selected cells, ranges, rows, and columns.
1✔
489
     * Used in conjunction with {@link onGridSelectionChange}
1✔
490
     * method to implement a controlled selection.
1✔
491
     * @group Selection
1✔
492
     */
1✔
493
    readonly gridSelection?: GridSelection;
1✔
494
    /**
1✔
495
     * Emitted whenever the grid selection changes. Specifying
1✔
496
     * this function will make the grid’s selection controlled, so
1✔
497
     * so you will need to specify {@link gridSelection} as well. See
1✔
498
     * the "Controlled Selection" example for details.
1✔
499
     *
1✔
500
     * @param newSelection The new gridSelection as created by user input.
1✔
501
     * @group Selection
1✔
502
     */
1✔
503
    readonly onGridSelectionChange?: (newSelection: GridSelection) => void;
1✔
504
    /**
1✔
505
     * Emitted whenever the visible cells change, usually due to scrolling.
1✔
506
     * @group Events
1✔
507
     * @param range An inclusive range of all visible cells. May include cells obscured by UI elements such
1✔
508
     * as headers.
1✔
509
     * @param tx The x transform of the cell region.
1✔
510
     * @param ty The y transform of the cell region.
1✔
511
     * @param extras Contains information about the selected cell and
1✔
512
     * any visible freeze columns.
1✔
513
     */
1✔
514
    readonly onVisibleRegionChanged?: (
1✔
515
        range: Rectangle,
1✔
516
        tx: number,
1✔
517
        ty: number,
1✔
518
        extras: {
1✔
519
            /** The selected item if visible */
1✔
520
            selected?: Item;
1✔
521
            /** A selection of visible freeze columns */
1✔
522
            freezeRegion?: Rectangle;
1✔
523
        }
1✔
524
    ) => void;
1✔
525

1✔
526
    /**
1✔
527
     * The primary callback for getting cell data into the data grid.
1✔
528
     * @group Data
1✔
529
     * @param cell The location of the cell being requested.
1✔
530
     * @returns A valid GridCell to be rendered by the Grid.
1✔
531
     */
1✔
532
    readonly getCellContent: (cell: Item) => GridCell;
1✔
533
    /**
1✔
534
     * Determines if row selection requires a modifier key to enable multi-selection or not. In auto mode it adapts to
1✔
535
     * touch or mouse environments automatically, in multi-mode it always acts as if the multi key (Ctrl) is pressed.
1✔
536
     * @group Editing
1✔
537
     * @defaultValue `auto`
1✔
538
     */
1✔
539
    readonly rowSelectionMode?: "auto" | "multi";
1✔
540

1✔
541
    /**
1✔
542
     * Add table headers to copied data.
1✔
543
     * @group Editing
1✔
544
     * @defaultValue `false`
1✔
545
     */
1✔
546
    readonly copyHeaders?: boolean;
1✔
547

1✔
548
    /**
1✔
549
     * Determins which keybindings are enabled.
1✔
550
     * @group Editing
1✔
551
     * @defaultValue is
1✔
552

1✔
553
            {
1✔
554
                selectAll: true,
1✔
555
                selectRow: true,
1✔
556
                selectColumn: true,
1✔
557
                downFill: false,
1✔
558
                rightFill: false,
1✔
559
                pageUp: false,
1✔
560
                pageDown: false,
1✔
561
                clear: true,
1✔
562
                copy: true,
1✔
563
                paste: true,
1✔
564
                search: false,
1✔
565
                first: true,
1✔
566
                last: true,
1✔
567
            }
1✔
568
     */
1✔
569
    readonly keybindings?: Partial<Keybinds>;
1✔
570

1✔
571
    /**
1✔
572
     * Used to fetch large amounts of cells at once. Used for copy/paste, if unset copy will not work.
1✔
573
     *
1✔
574
     * `getCellsForSelection` is called when the user copies a selection to the clipboard or the data editor needs to
1✔
575
     * inspect data which may be outside the curently visible range. It must return a two-dimensional array (an array of
1✔
576
     * rows, where each row is an array of cells) of the cells in the selection's rectangle. Note that the rectangle can
1✔
577
     * include cells that are not currently visible.
1✔
578
     *
1✔
579
     * If `true` is passed instead of a callback, the data grid will internally use the `getCellContent` callback to
1✔
580
     * provide a basic implementation of `getCellsForSelection`. This can make it easier to light up more data grid
1✔
581
     * functionality, but may have negative side effects if your data source is not able to handle being queried for
1✔
582
     * data outside the normal window.
1✔
583
     *
1✔
584
     * If `getCellsForSelection` returns a thunk, the data may be loaded asynchronously, however the data grid may be
1✔
585
     * unable to properly react to column spans when performing range selections. Copying large amounts of data out of
1✔
586
     * the grid will depend on the performance of the thunk as well.
1✔
587
     * @group Data
1✔
588
     * @param {Rectangle} selection The range of requested cells
1✔
589
     * @param {AbortSignal} abortSignal A signal indicating the requested cells are no longer needed
1✔
590
     * @returns A row-major collection of cells or an async thunk which returns a row-major collection.
1✔
591
     */
1✔
592
    readonly getCellsForSelection?: DataGridSearchProps["getCellsForSelection"] | true;
1✔
593

1✔
594
    /** The number of columns which should remain in place when scrolling horizontally. The row marker column, if
1✔
595
     * enabled is always frozen and is not included in this count.
1✔
596
     * @defaultValue 0
1✔
597
     * @group Style
1✔
598
     */
1✔
599
    readonly freezeColumns?: DataGridSearchProps["freezeColumns"];
1✔
600

1✔
601
    /**
1✔
602
     * Controls the drawing of the left hand vertical border of a column. If set to a boolean value it controls all
1✔
603
     * borders.
1✔
604
     * @defaultValue `true`
1✔
605
     * @group Style
1✔
606
     */
1✔
607
    readonly verticalBorder?: DataGridSearchProps["verticalBorder"] | boolean;
1✔
608

1✔
609
    /**
1✔
610
     * Called when data is pasted into the grid. If left undefined, the `DataEditor` will operate in a
1✔
611
     * fallback mode and attempt to paste the text buffer into the current cell assuming the current cell is not
1✔
612
     * readonly and can accept the data type. If `onPaste` is set to false or the function returns false, the grid will
1✔
613
     * simply ignore paste. If `onPaste` evaluates to true the grid will attempt to split the data by tabs and newlines
1✔
614
     * and paste into available cells.
1✔
615
     *
1✔
616
     * The grid will not attempt to add additional rows if more data is pasted then can fit. In that case it is
1✔
617
     * advisable to simply return false from onPaste and handle the paste manually.
1✔
618
     * @group Editing
1✔
619
     */
1✔
620
    readonly onPaste?: ((target: Item, values: readonly (readonly string[])[]) => boolean) | boolean;
1✔
621

1✔
622
    /**
1✔
623
     * The theme used by the data grid to get all color and font information
1✔
624
     * @group Style
1✔
625
     */
1✔
626
    readonly theme?: Partial<Theme>;
1✔
627

1✔
628
    readonly renderers?: readonly InternalCellRenderer<InnerGridCell>[];
1✔
629

1✔
630
    /**
1✔
631
     * An array of custom renderers which can be used to extend the data grid.
1✔
632
     * @group Advanced
1✔
633
     */
1✔
634
    readonly customRenderers?: readonly CustomRenderer<any>[];
1✔
635

1✔
636
    /**
1✔
637
     * Scales most elements in the theme to match rem scaling automatically
1✔
638
     * @defaultValue false
1✔
639
     */
1✔
640
    readonly scaleToRem?: boolean;
1✔
641

1✔
642
    /**
1✔
643
     * Custom predicate function to decide whether the click event occurred outside the grid
1✔
644
     * Especially used when custom editor is opened with the portal and is outside the grid, but there is no possibility
1✔
645
     * to add a class "click-outside-ignore"
1✔
646
     * If this function is supplied and returns false, the click event is ignored
1✔
647
     */
1✔
648
    readonly isOutsideClick?: (e: MouseEvent | TouchEvent) => boolean;
1✔
649

1✔
650
    /**
1✔
651
     * Controls which directions fill is allowed in.
1✔
652
     */
1✔
653
    readonly allowedFillDirections?: FillHandleDirection;
1✔
654

1✔
655
    /**
1✔
656
     * Determines when a cell is considered activated and will emit the `onCellActivated` event. Generally an activated
1✔
657
     * cell will open to edit mode.
1✔
658
     */
1✔
659
    readonly cellActivationBehavior?: "double-click" | "single-click" | "second-click";
1✔
660
}
1✔
661

1✔
662
type ScrollToFn = (
1✔
663
    col: number | { amount: number; unit: "cell" | "px" },
1✔
664
    row: number | { amount: number; unit: "cell" | "px" },
1✔
665
    dir?: "horizontal" | "vertical" | "both",
1✔
666
    paddingX?: number,
1✔
667
    paddingY?: number,
1✔
668
    options?: {
1✔
669
        hAlign?: "start" | "center" | "end";
1✔
670
        vAlign?: "start" | "center" | "end";
1✔
671
    }
1✔
672
) => void;
1✔
673

1✔
674
/** @category DataEditor */
1✔
675
export interface DataEditorRef {
1✔
676
    /**
1✔
677
     * Programatically appends a row.
1✔
678
     * @param col The column index to focus in the new row.
1✔
679
     * @returns A promise which waits for the append to complete.
1✔
680
     */
1✔
681
    appendRow: (col: number, openOverlay?: boolean) => Promise<void>;
1✔
682
    /**
1✔
683
     * Triggers cells to redraw.
1✔
684
     */
1✔
685
    updateCells: DataGridRef["damage"];
1✔
686
    /**
1✔
687
     * Gets the screen space bounds of the requested item.
1✔
688
     */
1✔
689
    getBounds: DataGridRef["getBounds"];
1✔
690
    /**
1✔
691
     * Triggers the data grid to focus itself or the correct accessibility element.
1✔
692
     */
1✔
693
    focus: DataGridRef["focus"];
1✔
694
    /**
1✔
695
     * Generic API for emitting events as if they had been triggered via user interaction.
1✔
696
     */
1✔
697
    emit: (eventName: EmitEvents) => Promise<void>;
1✔
698
    /**
1✔
699
     * Scrolls to the desired cell or location in the grid.
1✔
700
     */
1✔
701
    scrollTo: ScrollToFn;
1✔
702
    /**
1✔
703
     * Causes the columns in the selection to have their natural size recomputed and re-emitted as a resize event.
1✔
704
     */
1✔
705
    remeasureColumns: (cols: CompactSelection) => void;
1✔
706
}
1✔
707

1✔
708
const loadingCell: GridCell = {
1✔
709
    kind: GridCellKind.Loading,
1✔
710
    allowOverlay: false,
1✔
711
};
1✔
712

1✔
713
const emptyGridSelection: GridSelection = {
1✔
714
    columns: CompactSelection.empty(),
1✔
715
    rows: CompactSelection.empty(),
1✔
716
    current: undefined,
1✔
717
};
1✔
718

1✔
719
const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorProps> = (p, forwardedRef) => {
1✔
720
    const [gridSelectionInner, setGridSelectionInner] = React.useState<GridSelection>(emptyGridSelection);
678✔
721
    const [overlay, setOverlay] = React.useState<{
678✔
722
        target: Rectangle;
678✔
723
        content: GridCell;
678✔
724
        theme: FullTheme;
678✔
725
        initialValue: string | undefined;
678✔
726
        cell: Item;
678✔
727
        highlight: boolean;
678✔
728
        forceEditMode: boolean;
678✔
729
    }>();
678✔
730
    const searchInputRef = React.useRef<HTMLInputElement | null>(null);
678✔
731
    const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
678✔
732
    const [mouseState, setMouseState] = React.useState<MouseState>();
678✔
733
    const scrollRef = React.useRef<HTMLDivElement | null>(null);
678✔
734
    const lastSent = React.useRef<[number, number]>();
678✔
735

678✔
736
    const safeWindow = typeof window === "undefined" ? null : window;
678!
737

678✔
738
    const {
678✔
739
        rowMarkers = "none",
678✔
740
        rowMarkerWidth: rowMarkerWidthRaw,
678✔
741
        imageEditorOverride,
678✔
742
        getRowThemeOverride,
678✔
743
        markdownDivCreateNode,
678✔
744
        width,
678✔
745
        height,
678✔
746
        columns: columnsIn,
678✔
747
        rows,
678✔
748
        getCellContent,
678✔
749
        onCellClicked,
678✔
750
        onCellActivated,
678✔
751
        onFillPattern,
678✔
752
        onFinishedEditing,
678✔
753
        coercePasteValue,
678✔
754
        drawHeader: drawHeaderIn,
678✔
755
        drawCell: drawCellIn,
678✔
756
        onHeaderClicked,
678✔
757
        onColumnProposeMove,
678✔
758
        spanRangeBehavior = "default",
678✔
759
        onGroupHeaderClicked,
678✔
760
        onCellContextMenu,
678✔
761
        className,
678✔
762
        onHeaderContextMenu,
678✔
763
        getCellsForSelection: getCellsForSelectionIn,
678✔
764
        onGroupHeaderContextMenu,
678✔
765
        onGroupHeaderRenamed,
678✔
766
        onCellEdited,
678✔
767
        onCellsEdited,
678✔
768
        onSearchResultsChanged: onSearchResultsChangedIn,
678✔
769
        searchResults,
678✔
770
        onSearchValueChange,
678✔
771
        searchValue,
678✔
772
        onKeyDown: onKeyDownIn,
678✔
773
        onKeyUp: onKeyUpIn,
678✔
774
        keybindings: keybindingsIn,
678✔
775
        onRowAppended,
678✔
776
        onColumnMoved,
678✔
777
        validateCell: validateCellIn,
678✔
778
        highlightRegions: highlightRegionsIn,
678✔
779
        rangeSelect = "rect",
678✔
780
        columnSelect = "multi",
678✔
781
        rowSelect = "multi",
678✔
782
        rangeSelectionBlending = "exclusive",
678✔
783
        columnSelectionBlending = "exclusive",
678✔
784
        rowSelectionBlending = "exclusive",
678✔
785
        onDelete: onDeleteIn,
678✔
786
        onDragStart,
678✔
787
        onMouseMove,
678✔
788
        onPaste,
678✔
789
        copyHeaders = false,
678✔
790
        freezeColumns = 0,
678✔
791
        cellActivationBehavior = "second-click",
678✔
792
        rowSelectionMode = "auto",
678✔
793
        rowMarkerStartIndex = 1,
678✔
794
        rowMarkerTheme,
678✔
795
        onHeaderMenuClick,
678✔
796
        getGroupDetails,
678✔
797
        onSearchClose: onSearchCloseIn,
678✔
798
        onItemHovered,
678✔
799
        onSelectionCleared,
678✔
800
        showSearch: showSearchIn,
678✔
801
        onVisibleRegionChanged,
678✔
802
        gridSelection: gridSelectionOuter,
678✔
803
        onGridSelectionChange,
678✔
804
        minColumnWidth: minColumnWidthIn = 50,
678✔
805
        maxColumnWidth: maxColumnWidthIn = 500,
678✔
806
        maxColumnAutoWidth: maxColumnAutoWidthIn,
678✔
807
        provideEditor,
678✔
808
        trailingRowOptions,
678✔
809
        allowedFillDirections = "orthogonal",
678✔
810
        scrollOffsetX,
678✔
811
        scrollOffsetY,
678✔
812
        verticalBorder,
678✔
813
        onDragOverCell,
678✔
814
        onDrop,
678✔
815
        onColumnResize: onColumnResizeIn,
678✔
816
        onColumnResizeEnd: onColumnResizeEndIn,
678✔
817
        onColumnResizeStart: onColumnResizeStartIn,
678✔
818
        customRenderers: additionalRenderers,
678✔
819
        fillHandle,
678✔
820
        drawFocusRing,
678✔
821
        experimental,
678✔
822
        fixedShadowX,
678✔
823
        fixedShadowY,
678✔
824
        headerIcons,
678✔
825
        imageWindowLoader,
678✔
826
        initialSize,
678✔
827
        isDraggable,
678✔
828
        onDragLeave,
678✔
829
        onRowMoved,
678✔
830
        overscrollX: overscrollXIn,
678✔
831
        overscrollY: overscrollYIn,
678✔
832
        preventDiagonalScrolling,
678✔
833
        rightElement,
678✔
834
        rightElementProps,
678✔
835
        smoothScrollX,
678✔
836
        smoothScrollY,
678✔
837
        scaleToRem = false,
678✔
838
        rowHeight: rowHeightIn = 34,
678✔
839
        headerHeight: headerHeightIn = 36,
678✔
840
        groupHeaderHeight: groupHeaderHeightIn = headerHeightIn,
678✔
841
        theme: themeIn,
678✔
842
        isOutsideClick,
678✔
843
        renderers,
678✔
844
    } = p;
678✔
845

678✔
846
    const minColumnWidth = Math.max(minColumnWidthIn, 20);
678✔
847
    const maxColumnWidth = Math.max(maxColumnWidthIn, minColumnWidth);
678✔
848
    const maxColumnAutoWidth = Math.max(maxColumnAutoWidthIn ?? maxColumnWidth, minColumnWidth);
678✔
849

678✔
850
    const docStyle = React.useMemo(() => {
678✔
851
        if (typeof window === "undefined") return { fontSize: "16px" };
137!
852
        return window.getComputedStyle(document.documentElement);
137✔
853
    }, []);
678✔
854

678✔
855
    const fontSizeStr = docStyle.fontSize;
678✔
856

678✔
857
    const remSize = React.useMemo(() => Number.parseFloat(fontSizeStr), [fontSizeStr]);
678✔
858

678✔
859
    const { rowHeight, headerHeight, groupHeaderHeight, theme, overscrollX, overscrollY } = useRemAdjuster({
678✔
860
        groupHeaderHeight: groupHeaderHeightIn,
678✔
861
        headerHeight: headerHeightIn,
678✔
862
        overscrollX: overscrollXIn,
678✔
863
        overscrollY: overscrollYIn,
678✔
864
        remSize,
678✔
865
        rowHeight: rowHeightIn,
678✔
866
        scaleToRem,
678✔
867
        theme: themeIn,
678✔
868
    });
678✔
869

678✔
870
    const keybindings = React.useMemo(() => {
678✔
871
        return keybindingsIn === undefined
137✔
872
            ? keybindingDefaults
134✔
873
            : {
3✔
874
                  ...keybindingDefaults,
3✔
875
                  ...keybindingsIn,
3✔
876
              };
3✔
877
    }, [keybindingsIn]);
678✔
878

678✔
879
    const rowMarkerWidth = rowMarkerWidthRaw ?? (rows > 10_000 ? 48 : rows > 1000 ? 44 : rows > 100 ? 36 : 32);
678!
880
    const hasRowMarkers = rowMarkers !== "none";
678✔
881
    const rowMarkerOffset = hasRowMarkers ? 1 : 0;
678✔
882
    const showTrailingBlankRow = onRowAppended !== undefined;
678✔
883
    const lastRowSticky = trailingRowOptions?.sticky === true;
678✔
884

678✔
885
    const [showSearchInner, setShowSearchInner] = React.useState(false);
678✔
886
    const showSearch = showSearchIn ?? showSearchInner;
678✔
887

678✔
888
    const onSearchClose = React.useCallback(() => {
678✔
889
        if (onSearchCloseIn !== undefined) {
2✔
890
            onSearchCloseIn();
2✔
891
        } else {
2!
892
            setShowSearchInner(false);
×
UNCOV
893
        }
×
894
    }, [onSearchCloseIn]);
678✔
895

678✔
896
    const gridSelectionOuterMangled: GridSelection | undefined = React.useMemo((): GridSelection | undefined => {
678✔
897
        return gridSelectionOuter === undefined ? undefined : shiftSelection(gridSelectionOuter, rowMarkerOffset);
262✔
898
    }, [gridSelectionOuter, rowMarkerOffset]);
678✔
899
    const gridSelection = gridSelectionOuterMangled ?? gridSelectionInner;
678✔
900

678✔
901
    const abortControllerRef = React.useRef(new AbortController());
678✔
902
    React.useEffect(() => {
678✔
903
        return () => {
137✔
904
            // eslint-disable-next-line react-hooks/exhaustive-deps
137✔
905
            abortControllerRef?.current.abort();
137✔
906
        };
137✔
907
    }, []);
678✔
908

678✔
909
    const [getCellsForSelection, getCellsForSeletionDirect] = useCellsForSelection(
678✔
910
        getCellsForSelectionIn,
678✔
911
        getCellContent,
678✔
912
        rowMarkerOffset,
678✔
913
        abortControllerRef.current,
678✔
914
        rows
678✔
915
    );
678✔
916

678✔
917
    const validateCell = React.useCallback<NonNullable<typeof validateCellIn>>(
678✔
918
        (cell, newValue, prevValue) => {
678✔
919
            if (validateCellIn === undefined) return true;
12✔
920
            const item: Item = [cell[0] - rowMarkerOffset, cell[1]];
1✔
921
            return validateCellIn?.(item, newValue, prevValue);
1✔
922
        },
12✔
923
        [rowMarkerOffset, validateCellIn]
678✔
924
    );
678✔
925

678✔
926
    const expectedExternalGridSelection = React.useRef<GridSelection | undefined>(gridSelectionOuter);
678✔
927
    const setGridSelection = React.useCallback(
678✔
928
        (newVal: GridSelection, expand: boolean): void => {
678✔
929
            if (expand) {
172✔
930
                newVal = expandSelection(
130✔
931
                    newVal,
130✔
932
                    getCellsForSelection,
130✔
933
                    rowMarkerOffset,
130✔
934
                    spanRangeBehavior,
130✔
935
                    abortControllerRef.current
130✔
936
                );
130✔
937
            }
130✔
938
            if (onGridSelectionChange !== undefined) {
172✔
939
                expectedExternalGridSelection.current = shiftSelection(newVal, -rowMarkerOffset);
129✔
940
                onGridSelectionChange(expectedExternalGridSelection.current);
129✔
941
            } else {
172✔
942
                setGridSelectionInner(newVal);
43✔
943
            }
43✔
944
        },
172✔
945
        [onGridSelectionChange, getCellsForSelection, rowMarkerOffset, spanRangeBehavior]
678✔
946
    );
678✔
947

678✔
948
    const onColumnResize = whenDefined(
678✔
949
        onColumnResizeIn,
678✔
950
        React.useCallback<NonNullable<typeof onColumnResizeIn>>(
678✔
951
            (_, w, ind, wg) => {
678✔
952
                onColumnResizeIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
11✔
953
            },
11✔
954
            [onColumnResizeIn, rowMarkerOffset, columnsIn]
678✔
955
        )
678✔
956
    );
678✔
957

678✔
958
    const onColumnResizeEnd = whenDefined(
678✔
959
        onColumnResizeEndIn,
678✔
960
        React.useCallback<NonNullable<typeof onColumnResizeEndIn>>(
678✔
961
            (_, w, ind, wg) => {
678✔
962
                onColumnResizeEndIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
2✔
963
            },
2✔
964
            [onColumnResizeEndIn, rowMarkerOffset, columnsIn]
678✔
965
        )
678✔
966
    );
678✔
967

678✔
968
    const onColumnResizeStart = whenDefined(
678✔
969
        onColumnResizeStartIn,
678✔
970
        React.useCallback<NonNullable<typeof onColumnResizeStartIn>>(
678✔
971
            (_, w, ind, wg) => {
678✔
972
                onColumnResizeStartIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
×
UNCOV
973
            },
×
974
            [onColumnResizeStartIn, rowMarkerOffset, columnsIn]
678✔
975
        )
678✔
976
    );
678✔
977

678✔
978
    const drawHeader = whenDefined(
678✔
979
        drawHeaderIn,
678✔
980
        React.useCallback<NonNullable<typeof drawHeaderIn>>(
678✔
981
            (args, draw) => {
678✔
NEW
982
                return drawHeaderIn?.({ ...args, columnIndex: args.columnIndex - rowMarkerOffset }, draw) ?? false;
×
UNCOV
983
            },
×
984
            [drawHeaderIn, rowMarkerOffset]
678✔
985
        )
678✔
986
    );
678✔
987

678✔
988
    const drawCell = whenDefined(
678✔
989
        drawCellIn,
678✔
990
        React.useCallback<NonNullable<typeof drawCellIn>>(
678✔
991
            (args, draw) => {
678✔
NEW
992
                return drawCellIn?.({ ...args, col: args.col - rowMarkerOffset }, draw) ?? false;
×
NEW
993
            },
×
994
            [drawCellIn, rowMarkerOffset]
678✔
995
        )
678✔
996
    );
678✔
997

678✔
998
    const onDelete = React.useCallback<NonNullable<DataEditorProps["onDelete"]>>(
678✔
999
        sel => {
678✔
1000
            if (onDeleteIn !== undefined) {
9✔
1001
                const result = onDeleteIn(shiftSelection(sel, -rowMarkerOffset));
5✔
1002
                if (typeof result === "boolean") {
5!
1003
                    return result;
×
UNCOV
1004
                }
×
1005
                return shiftSelection(result, rowMarkerOffset);
5✔
1006
            }
5✔
1007
            return true;
4✔
1008
        },
9✔
1009
        [onDeleteIn, rowMarkerOffset]
678✔
1010
    );
678✔
1011

678✔
1012
    const [setCurrent, setSelectedRows, setSelectedColumns] = useSelectionBehavior(
678✔
1013
        gridSelection,
678✔
1014
        setGridSelection,
678✔
1015
        rangeSelectionBlending,
678✔
1016
        columnSelectionBlending,
678✔
1017
        rowSelectionBlending,
678✔
1018
        rangeSelect
678✔
1019
    );
678✔
1020

678✔
1021
    const mergedTheme = React.useMemo(() => {
678✔
1022
        return mergeAndRealizeTheme(getDataEditorTheme(), theme);
137✔
1023
    }, [theme]);
678✔
1024

678✔
1025
    const [clientSize, setClientSize] = React.useState<readonly [number, number, number]>([10, 10, 0]);
678✔
1026

678✔
1027
    const rendererMap = React.useMemo(() => {
678✔
1028
        if (renderers === undefined) return {};
137!
1029
        const result: Partial<Record<InnerGridCellKind | GridCellKind, InternalCellRenderer<InnerGridCell>>> = {};
137✔
1030
        for (const r of renderers) {
137✔
1031
            result[r.kind] = r;
1,781✔
1032
        }
1,781✔
1033
        return result;
137✔
1034
    }, [renderers]);
678✔
1035

678✔
1036
    const getCellRenderer: <T extends InnerGridCell>(cell: T) => CellRenderer<T> | undefined = React.useCallback(
678✔
1037
        <T extends InnerGridCell>(cell: T) => {
678✔
1038
            if (cell.kind !== GridCellKind.Custom) {
139,127✔
1039
                return rendererMap[cell.kind] as unknown as CellRenderer<T>;
135,397✔
1040
            }
135,397✔
1041
            return additionalRenderers?.find(x => x.isMatch(cell)) as CellRenderer<T>;
139,127✔
1042
        },
139,127✔
1043
        [additionalRenderers, rendererMap]
678✔
1044
    );
678✔
1045

678✔
1046
    // eslint-disable-next-line prefer-const
678✔
1047
    let { sizedColumns: columns, nonGrowWidth } = useColumnSizer(
678✔
1048
        columnsIn,
678✔
1049
        rows,
678✔
1050
        getCellsForSeletionDirect,
678✔
1051
        clientSize[0] - (rowMarkerOffset === 0 ? 0 : rowMarkerWidth) - clientSize[2],
678✔
1052
        minColumnWidth,
678✔
1053
        maxColumnAutoWidth,
678✔
1054
        mergedTheme,
678✔
1055
        getCellRenderer,
678✔
1056
        abortControllerRef.current
678✔
1057
    );
678✔
1058
    if (rowMarkers !== "none") nonGrowWidth += rowMarkerWidth;
678✔
1059

677✔
1060
    const enableGroups = React.useMemo(() => {
677✔
1061
        return columns.some(c => c.group !== undefined);
137✔
1062
    }, [columns]);
677✔
1063

677✔
1064
    const totalHeaderHeight = enableGroups ? headerHeight + groupHeaderHeight : headerHeight;
678✔
1065

678✔
1066
    const numSelectedRows = gridSelection.rows.length;
678✔
1067
    const rowMarkerHeader =
678✔
1068
        rowMarkers === "none"
678✔
1069
            ? ""
547✔
1070
            : numSelectedRows === 0
130✔
1071
            ? headerCellUnheckedMarker
86✔
1072
            : numSelectedRows === rows
44✔
1073
            ? headerCellCheckedMarker
6✔
1074
            : headerCellIndeterminateMarker;
38✔
1075

678✔
1076
    const mangledCols = React.useMemo(() => {
678✔
1077
        if (rowMarkers === "none") return columns;
151✔
1078
        return [
45✔
1079
            {
45✔
1080
                title: rowMarkerHeader,
45✔
1081
                width: rowMarkerWidth,
45✔
1082
                icon: undefined,
45✔
1083
                hasMenu: false,
45✔
1084
                style: "normal" as const,
45✔
1085
                themeOverride: rowMarkerTheme,
45✔
1086
            },
45✔
1087
            ...columns,
45✔
1088
        ];
45✔
1089
    }, [columns, rowMarkerWidth, rowMarkers, rowMarkerHeader, rowMarkerTheme]);
678✔
1090

678✔
1091
    const [visibleRegionY, visibleRegionTy] = React.useMemo(() => {
678✔
1092
        return [
137✔
1093
            scrollOffsetY !== undefined && typeof rowHeight === "number" ? Math.floor(scrollOffsetY / rowHeight) : 0,
137!
1094
            scrollOffsetY !== undefined && typeof rowHeight === "number" ? -(scrollOffsetY % rowHeight) : 0,
137!
1095
        ];
137✔
1096
    }, [scrollOffsetY, rowHeight]);
678✔
1097

678✔
1098
    type VisibleRegion = Rectangle & {
678✔
1099
        /** value in px */
678✔
1100
        tx?: number;
678✔
1101
        /** value in px */
678✔
1102
        ty?: number;
678✔
1103
        extras?: {
678✔
1104
            selected?: Item;
678✔
1105
            freezeRegion?: Rectangle;
678✔
1106
        };
678✔
1107
    };
678✔
1108

678✔
1109
    const visibleRegionRef = React.useRef<VisibleRegion>({
678✔
1110
        height: 1,
678✔
1111
        width: 1,
678✔
1112
        x: 0,
678✔
1113
        y: 0,
678✔
1114
    });
678✔
1115
    const visibleRegionInput = React.useMemo<VisibleRegion>(
678✔
1116
        () => ({
678✔
1117
            x: visibleRegionRef.current.x,
137✔
1118
            y: visibleRegionY,
137✔
1119
            width: visibleRegionRef.current.width ?? 1,
137!
1120
            height: visibleRegionRef.current.height ?? 1,
137!
1121
            // tx: 'TODO',
137✔
1122
            ty: visibleRegionTy,
137✔
1123
        }),
137✔
1124
        [visibleRegionTy, visibleRegionY]
678✔
1125
    );
678✔
1126

678✔
1127
    const hasJustScrolled = React.useRef(false);
678✔
1128

678✔
1129
    const [visibleRegion, setVisibleRegion, empty] = useStateWithReactiveInput<VisibleRegion>(visibleRegionInput);
678✔
1130

678✔
1131
    const vScrollReady = (visibleRegion.height ?? 1) > 1;
678!
1132
    React.useLayoutEffect(() => {
678✔
1133
        if (scrollOffsetY !== undefined && scrollRef.current !== null && vScrollReady) {
274!
1134
            if (scrollRef.current.scrollTop === scrollOffsetY) return;
×
1135
            scrollRef.current.scrollTop = scrollOffsetY;
×
UNCOV
1136
            if (scrollRef.current.scrollTop !== scrollOffsetY) {
×
1137
                empty();
×
UNCOV
1138
            }
×
1139
            hasJustScrolled.current = true;
×
UNCOV
1140
        }
×
1141
    }, [scrollOffsetY, vScrollReady, empty]);
678✔
1142

678✔
1143
    const hScrollReady = (visibleRegion.width ?? 1) > 1;
678!
1144
    React.useLayoutEffect(() => {
678✔
1145
        if (scrollOffsetX !== undefined && scrollRef.current !== null && hScrollReady) {
274!
1146
            if (scrollRef.current.scrollLeft === scrollOffsetX) return;
×
1147
            scrollRef.current.scrollLeft = scrollOffsetX;
×
UNCOV
1148
            if (scrollRef.current.scrollLeft !== scrollOffsetX) {
×
1149
                empty();
×
UNCOV
1150
            }
×
1151
            hasJustScrolled.current = true;
×
UNCOV
1152
        }
×
1153
    }, [scrollOffsetX, hScrollReady, empty]);
678✔
1154

678✔
1155
    const cellXOffset = visibleRegion.x + rowMarkerOffset;
678✔
1156
    const cellYOffset = visibleRegion.y;
678✔
1157

678✔
1158
    const gridRef = React.useRef<DataGridRef | null>(null);
678✔
1159

678✔
1160
    const focus = React.useCallback((immediate?: boolean) => {
678✔
1161
        if (immediate === true) {
125✔
1162
            gridRef.current?.focus();
7✔
1163
        } else {
125✔
1164
            window.requestAnimationFrame(() => {
118✔
1165
                gridRef.current?.focus();
117✔
1166
            });
118✔
1167
        }
118✔
1168
    }, []);
678✔
1169

678✔
1170
    const mangledRows = showTrailingBlankRow ? rows + 1 : rows;
678!
1171

678✔
1172
    const mangledOnCellsEdited = React.useCallback<NonNullable<typeof onCellsEdited>>(
678✔
1173
        (items: readonly EditListItem[]) => {
678✔
1174
            const mangledItems =
27✔
1175
                rowMarkerOffset === 0
27✔
1176
                    ? items
22✔
1177
                    : items.map(x => ({
5✔
1178
                          ...x,
29✔
1179
                          location: [x.location[0] - rowMarkerOffset, x.location[1]] as const,
29✔
1180
                      }));
5✔
1181
            const r = onCellsEdited?.(mangledItems);
27✔
1182

27✔
1183
            if (r !== true) {
27✔
1184
                for (const i of mangledItems) onCellEdited?.(i.location, i.value);
26✔
1185
            }
26✔
1186

27✔
1187
            return r;
27✔
1188
        },
27✔
1189
        [onCellEdited, onCellsEdited, rowMarkerOffset]
678✔
1190
    );
678✔
1191

678✔
1192
    const [fillHighlightRegion, setFillHighlightRegion] = React.useState<Rectangle | undefined>();
678✔
1193

678✔
1194
    // this will generally be undefined triggering the memo less often
678✔
1195
    const highlightRange =
678✔
1196
        gridSelection.current !== undefined &&
678✔
1197
        gridSelection.current.range.width * gridSelection.current.range.height > 1
321✔
1198
            ? gridSelection.current.range
41✔
1199
            : undefined;
636✔
1200
    const highlightRegions = React.useMemo(() => {
678✔
1201
        if (
172✔
1202
            (highlightRegionsIn === undefined || highlightRegionsIn.length === 0) &&
172✔
1203
            highlightRange === undefined &&
171✔
1204
            fillHighlightRegion === undefined
144✔
1205
        )
172✔
1206
            return undefined;
172✔
1207

33✔
1208
        const regions: Highlight[] = [];
33✔
1209

33✔
1210
        if (highlightRegionsIn !== undefined) {
166✔
1211
            for (const r of highlightRegionsIn) {
1✔
1212
                const maxWidth = mangledCols.length - r.range.x - rowMarkerOffset;
1✔
1213
                if (maxWidth > 0) {
1✔
1214
                    regions.push({
1✔
1215
                        color: r.color,
1✔
1216
                        range: {
1✔
1217
                            ...r.range,
1✔
1218
                            x: r.range.x + rowMarkerOffset,
1✔
1219
                            width: Math.min(maxWidth, r.range.width),
1✔
1220
                        },
1✔
1221
                        style: r.style,
1✔
1222
                    });
1✔
1223
                }
1✔
1224
            }
1✔
1225
        }
1✔
1226

33✔
1227
        if (fillHighlightRegion !== undefined) {
166✔
1228
            regions.push({
5✔
1229
                color: withAlpha(mergedTheme.accentColor, 0),
5✔
1230
                range: fillHighlightRegion,
5✔
1231
                style: "dashed",
5✔
1232
            });
5✔
1233
        }
5✔
1234

33✔
1235
        if (highlightRange !== undefined) {
166✔
1236
            regions.push({
27✔
1237
                color: withAlpha(mergedTheme.accentColor, 0.5),
27✔
1238
                range: highlightRange,
27✔
1239
                style: "solid-outline",
27✔
1240
            });
27✔
1241
        }
27✔
1242

33✔
1243
        return regions.length > 0 ? regions : undefined;
172!
1244
    }, [
678✔
1245
        fillHighlightRegion,
678✔
1246
        highlightRange,
678✔
1247
        highlightRegionsIn,
678✔
1248
        mangledCols.length,
678✔
1249
        mergedTheme.accentColor,
678✔
1250
        rowMarkerOffset,
678✔
1251
    ]);
678✔
1252

678✔
1253
    const mangledColsRef = React.useRef(mangledCols);
678✔
1254
    mangledColsRef.current = mangledCols;
678✔
1255
    const getMangledCellContent = React.useCallback(
678✔
1256
        ([col, row]: Item, forceStrict: boolean = false): InnerGridCell => {
678✔
1257
            const isTrailing = showTrailingBlankRow && row === mangledRows - 1;
140,239✔
1258
            const isRowMarkerCol = col === 0 && hasRowMarkers;
140,239✔
1259
            if (isRowMarkerCol) {
140,239✔
1260
                if (isTrailing) {
2,186✔
1261
                    return loadingCell;
98✔
1262
                }
98✔
1263
                return {
2,088✔
1264
                    kind: InnerGridCellKind.Marker,
2,088✔
1265
                    allowOverlay: false,
2,088✔
1266
                    checked: gridSelection?.rows.hasIndex(row) === true,
2,186✔
1267
                    markerKind: rowMarkers === "clickable-number" ? "number" : rowMarkers,
2,186!
1268
                    row: rowMarkerStartIndex + row,
2,186✔
1269
                    drawHandle: onRowMoved !== undefined,
2,186✔
1270
                    cursor: rowMarkers === "clickable-number" ? "pointer" : undefined,
2,186!
1271
                };
2,186✔
1272
            } else if (isTrailing) {
140,239✔
1273
                //If the grid is empty, we will return text
3,883✔
1274
                const isFirst = col === rowMarkerOffset;
3,883✔
1275

3,883✔
1276
                const maybeFirstColumnHint = isFirst ? trailingRowOptions?.hint ?? "" : "";
3,883✔
1277
                const c = mangledColsRef.current[col];
3,883✔
1278

3,883✔
1279
                if (c?.trailingRowOptions?.disabled === true) {
3,883!
1280
                    return loadingCell;
×
1281
                } else {
3,883✔
1282
                    const hint = c?.trailingRowOptions?.hint ?? maybeFirstColumnHint;
3,883!
1283
                    const icon = c?.trailingRowOptions?.addIcon ?? trailingRowOptions?.addIcon;
3,883!
1284
                    return {
3,883✔
1285
                        kind: InnerGridCellKind.NewRow,
3,883✔
1286
                        hint,
3,883✔
1287
                        allowOverlay: false,
3,883✔
1288
                        icon,
3,883✔
1289
                    };
3,883✔
1290
                }
3,883✔
1291
            } else {
138,053✔
1292
                const outerCol = col - rowMarkerOffset;
134,170✔
1293
                if (forceStrict || experimental?.strict === true) {
134,170✔
1294
                    const vr = visibleRegionRef.current;
24,901✔
1295
                    const isOutsideMainArea =
24,901✔
1296
                        vr.x > outerCol ||
24,901✔
1297
                        outerCol > vr.x + vr.width ||
24,901✔
1298
                        vr.y > row ||
24,901✔
1299
                        row > vr.y + vr.height ||
24,901✔
1300
                        row >= rowsRef.current;
24,901✔
1301
                    const isSelected = outerCol === vr.extras?.selected?.[0] && row === vr.extras?.selected[1];
24,901!
1302
                    const isOutsideFreezeArea =
24,901✔
1303
                        vr.extras?.freezeRegion === undefined ||
24,901!
UNCOV
1304
                        vr.extras.freezeRegion.x > outerCol ||
×
UNCOV
1305
                        outerCol > vr.extras.freezeRegion.x + vr.extras.freezeRegion.width ||
×
UNCOV
1306
                        vr.extras.freezeRegion.y > row ||
×
UNCOV
1307
                        row > vr.extras.freezeRegion.y + vr.extras.freezeRegion.height;
×
1308
                    if (isOutsideMainArea && !isSelected && isOutsideFreezeArea) {
24,901!
NEW
1309
                        return loadingCell;
×
UNCOV
1310
                    }
×
1311
                }
24,901✔
1312
                let result = getCellContent([outerCol, row]);
134,170✔
1313
                if (rowMarkerOffset !== 0 && result.span !== undefined) {
134,170!
1314
                    result = {
×
UNCOV
1315
                        ...result,
×
UNCOV
1316
                        span: [result.span[0] + rowMarkerOffset, result.span[1] + rowMarkerOffset],
×
UNCOV
1317
                    };
×
UNCOV
1318
                }
×
1319
                return result;
134,170✔
1320
            }
134,170✔
1321
        },
140,239✔
1322
        [
678✔
1323
            showTrailingBlankRow,
678✔
1324
            mangledRows,
678✔
1325
            hasRowMarkers,
678✔
1326
            gridSelection?.rows,
678✔
1327
            onRowMoved,
678✔
1328
            rowMarkers,
678✔
1329
            rowMarkerOffset,
678✔
1330
            trailingRowOptions?.hint,
678✔
1331
            trailingRowOptions?.addIcon,
678✔
1332
            experimental?.strict,
678✔
1333
            getCellContent,
678✔
1334
            rowMarkerStartIndex,
678✔
1335
        ]
678✔
1336
    );
678✔
1337

678✔
1338
    const mangledGetGroupDetails = React.useCallback<NonNullable<DataEditorProps["getGroupDetails"]>>(
678✔
1339
        group => {
678✔
1340
            let result = getGroupDetails?.(group) ?? { name: group };
8,272✔
1341
            if (onGroupHeaderRenamed !== undefined && group !== "") {
8,272✔
1342
                result = {
91✔
1343
                    icon: result.icon,
91✔
1344
                    name: result.name,
91✔
1345
                    overrideTheme: result.overrideTheme,
91✔
1346
                    actions: [
91✔
1347
                        ...(result.actions ?? []),
91✔
1348
                        {
91✔
1349
                            title: "Rename",
91✔
1350
                            icon: "renameIcon",
91✔
1351
                            onClick: e =>
91✔
1352
                                setRenameGroup({
2✔
1353
                                    group: result.name,
2✔
1354
                                    bounds: e.bounds,
2✔
1355
                                }),
2✔
1356
                        },
91✔
1357
                    ],
91✔
1358
                };
91✔
1359
            }
91✔
1360
            return result;
8,272✔
1361
        },
8,272✔
1362
        [getGroupDetails, onGroupHeaderRenamed]
678✔
1363
    );
678✔
1364

678✔
1365
    const setOverlaySimple = React.useCallback(
678✔
1366
        (val: Omit<NonNullable<typeof overlay>, "theme">) => {
678✔
1367
            const [col, row] = val.cell;
19✔
1368
            const column = mangledCols[col];
19✔
1369
            const groupTheme =
19✔
1370
                column?.group !== undefined ? mangledGetGroupDetails(column.group)?.overrideTheme : undefined;
19!
1371
            const colTheme = column?.themeOverride;
19✔
1372
            const rowTheme = getRowThemeOverride?.(row);
19!
1373

19✔
1374
            setOverlay({
19✔
1375
                ...val,
19✔
1376
                theme: mergeAndRealizeTheme(mergedTheme, groupTheme, colTheme, rowTheme, val.content.themeOverride),
19✔
1377
            });
19✔
1378
        },
19✔
1379
        [getRowThemeOverride, mangledCols, mangledGetGroupDetails, mergedTheme]
678✔
1380
    );
678✔
1381

678✔
1382
    const reselect = React.useCallback(
678✔
1383
        (bounds: Rectangle, fromKeyboard: boolean, initialValue?: string) => {
678✔
1384
            if (gridSelection.current === undefined) return;
19!
1385

19✔
1386
            const [col, row] = gridSelection.current.cell;
19✔
1387
            const c = getMangledCellContent([col, row]);
19✔
1388
            if (c.kind !== GridCellKind.Boolean && c.allowOverlay) {
19✔
1389
                let content = c;
18✔
1390
                if (initialValue !== undefined) {
18✔
1391
                    switch (content.kind) {
7✔
1392
                        case GridCellKind.Number: {
7!
1393
                            const d = maybe(() => (initialValue === "-" ? -0 : Number.parseFloat(initialValue)), 0);
×
1394
                            content = {
×
UNCOV
1395
                                ...content,
×
UNCOV
1396
                                data: Number.isNaN(d) ? 0 : d,
×
UNCOV
1397
                            };
×
1398
                            break;
×
UNCOV
1399
                        }
×
1400
                        case GridCellKind.Text:
7✔
1401
                        case GridCellKind.Markdown:
7✔
1402
                        case GridCellKind.Uri:
7✔
1403
                            content = {
7✔
1404
                                ...content,
7✔
1405
                                data: initialValue,
7✔
1406
                            };
7✔
1407
                            break;
7✔
1408
                    }
7✔
1409
                }
7✔
1410

18✔
1411
                setOverlaySimple({
18✔
1412
                    target: bounds,
18✔
1413
                    content,
18✔
1414
                    initialValue,
18✔
1415
                    cell: [col, row],
18✔
1416
                    highlight: initialValue === undefined,
18✔
1417
                    forceEditMode: initialValue !== undefined,
18✔
1418
                });
18✔
1419
            } else if (c.kind === GridCellKind.Boolean && fromKeyboard && c.readonly !== true) {
19!
1420
                mangledOnCellsEdited([
×
UNCOV
1421
                    {
×
UNCOV
1422
                        location: gridSelection.current.cell,
×
UNCOV
1423
                        value: {
×
UNCOV
1424
                            ...c,
×
UNCOV
1425
                            data: toggleBoolean(c.data),
×
UNCOV
1426
                        },
×
UNCOV
1427
                    },
×
UNCOV
1428
                ]);
×
1429
                gridRef.current?.damage([{ cell: gridSelection.current.cell }]);
×
UNCOV
1430
            }
×
1431
        },
19✔
1432
        [getMangledCellContent, gridSelection, mangledOnCellsEdited, setOverlaySimple]
678✔
1433
    );
678✔
1434

678✔
1435
    const focusOnRowFromTrailingBlankRow = React.useCallback(
678✔
1436
        (col: number, row: number) => {
678✔
1437
            const bounds = gridRef.current?.getBounds(col, row);
1✔
1438
            if (bounds === undefined || scrollRef.current === null) {
1!
1439
                return;
×
UNCOV
1440
            }
×
1441

1✔
1442
            const content = getMangledCellContent([col, row]);
1✔
1443
            if (!content.allowOverlay) {
1!
1444
                return;
×
UNCOV
1445
            }
×
1446

1✔
1447
            setOverlaySimple({
1✔
1448
                target: bounds,
1✔
1449
                content,
1✔
1450
                initialValue: undefined,
1✔
1451
                highlight: true,
1✔
1452
                cell: [col, row],
1✔
1453
                forceEditMode: true,
1✔
1454
            });
1✔
1455
        },
1✔
1456
        [getMangledCellContent, setOverlaySimple]
678✔
1457
    );
678✔
1458

678✔
1459
    const scrollTo = React.useCallback<ScrollToFn>(
678✔
1460
        (col, row, dir = "both", paddingX = 0, paddingY = 0, options = undefined): void => {
678✔
1461
            if (scrollRef.current !== null) {
43✔
1462
                const grid = gridRef.current;
43✔
1463
                const canvas = canvasRef.current;
43✔
1464

43✔
1465
                const trueCol = typeof col !== "number" ? (col.unit === "cell" ? col.amount : undefined) : col;
43!
1466
                const trueRow = typeof row !== "number" ? (row.unit === "cell" ? row.amount : undefined) : row;
43!
1467
                const desiredX = typeof col !== "number" && col.unit === "px" ? col.amount : undefined;
43!
1468
                const desiredY = typeof row !== "number" && row.unit === "px" ? row.amount : undefined;
43✔
1469
                if (grid !== null && canvas !== null) {
43✔
1470
                    let targetRect: Rectangle = {
43✔
1471
                        x: 0,
43✔
1472
                        y: 0,
43✔
1473
                        width: 0,
43✔
1474
                        height: 0,
43✔
1475
                    };
43✔
1476

43✔
1477
                    let scrollX = 0;
43✔
1478
                    let scrollY = 0;
43✔
1479

43✔
1480
                    if (trueCol !== undefined || trueRow !== undefined) {
43!
1481
                        targetRect = grid.getBounds((trueCol ?? 0) + rowMarkerOffset, trueRow ?? 0) ?? targetRect;
43!
1482
                        if (targetRect.width === 0 || targetRect.height === 0) return;
43!
1483
                    }
43✔
1484

43✔
1485
                    const scrollBounds = canvas.getBoundingClientRect();
43✔
1486
                    const scale = scrollBounds.width / canvas.offsetWidth;
43✔
1487

43✔
1488
                    if (desiredX !== undefined) {
43!
1489
                        targetRect = {
×
UNCOV
1490
                            ...targetRect,
×
UNCOV
1491
                            x: desiredX - scrollBounds.left - scrollRef.current.scrollLeft,
×
UNCOV
1492
                            width: 1,
×
UNCOV
1493
                        };
×
UNCOV
1494
                    }
×
1495
                    if (desiredY !== undefined) {
43✔
1496
                        targetRect = {
4✔
1497
                            ...targetRect,
4✔
1498
                            y: desiredY + scrollBounds.top - scrollRef.current.scrollTop,
4✔
1499
                            height: 1,
4✔
1500
                        };
4✔
1501
                    }
4✔
1502

43✔
1503
                    if (targetRect !== undefined) {
43✔
1504
                        const bounds = {
43✔
1505
                            x: targetRect.x - paddingX,
43✔
1506
                            y: targetRect.y - paddingY,
43✔
1507
                            width: targetRect.width + 2 * paddingX,
43✔
1508
                            height: targetRect.height + 2 * paddingY,
43✔
1509
                        };
43✔
1510

43✔
1511
                        let frozenWidth = 0;
43✔
1512
                        for (let i = 0; i < freezeColumns; i++) {
43!
1513
                            frozenWidth += columns[i].width;
×
UNCOV
1514
                        }
×
1515
                        let trailingRowHeight = 0;
43✔
1516
                        if (lastRowSticky) {
43✔
1517
                            trailingRowHeight = typeof rowHeight === "number" ? rowHeight : rowHeight(rows);
43!
1518
                        }
43✔
1519

43✔
1520
                        // scrollBounds is already scaled
43✔
1521
                        let sLeft = frozenWidth * scale + scrollBounds.left + rowMarkerOffset * rowMarkerWidth * scale;
43✔
1522
                        let sRight = scrollBounds.right;
43✔
1523
                        let sTop = scrollBounds.top + totalHeaderHeight * scale;
43✔
1524
                        let sBottom = scrollBounds.bottom - trailingRowHeight * scale;
43✔
1525

43✔
1526
                        const minx = targetRect.width + paddingX * 2;
43✔
1527
                        switch (options?.hAlign) {
43✔
1528
                            case "start":
43!
1529
                                sRight = sLeft + minx;
×
1530
                                break;
×
1531
                            case "end":
43!
1532
                                sLeft = sRight - minx;
×
1533
                                break;
×
1534
                            case "center":
43!
1535
                                sLeft = Math.floor((sLeft + sRight) / 2) - minx / 2;
×
1536
                                sRight = sLeft + minx;
×
1537
                                break;
×
1538
                        }
43✔
1539

43✔
1540
                        const miny = targetRect.height + paddingY * 2;
43✔
1541
                        switch (options?.vAlign) {
43✔
1542
                            case "start":
43✔
1543
                                sBottom = sTop + miny;
1✔
1544
                                break;
1✔
1545
                            case "end":
43✔
1546
                                sTop = sBottom - miny;
1✔
1547
                                break;
1✔
1548
                            case "center":
43✔
1549
                                sTop = Math.floor((sTop + sBottom) / 2) - miny / 2;
1✔
1550
                                sBottom = sTop + miny;
1✔
1551
                                break;
1✔
1552
                        }
43✔
1553

43✔
1554
                        if (sLeft > bounds.x) {
43!
1555
                            scrollX = bounds.x - sLeft;
×
1556
                        } else if (sRight < bounds.x + bounds.width) {
43✔
1557
                            scrollX = bounds.x + bounds.width - sRight;
4✔
1558
                        }
4✔
1559

43✔
1560
                        if (sTop > bounds.y) {
43!
1561
                            scrollY = bounds.y - sTop;
×
1562
                        } else if (sBottom < bounds.y + bounds.height) {
43✔
1563
                            scrollY = bounds.y + bounds.height - sBottom;
12✔
1564
                        }
12✔
1565

43✔
1566
                        if (dir === "vertical" || (typeof col === "number" && col < freezeColumns)) {
43✔
1567
                            scrollX = 0;
2✔
1568
                        } else if (dir === "horizontal") {
43✔
1569
                            scrollY = 0;
6✔
1570
                        }
6✔
1571

43✔
1572
                        if (scrollX !== 0 || scrollY !== 0) {
43✔
1573
                            // Remove scaling as scrollTo method is unaffected by transform scale.
15✔
1574
                            if (scale !== 1) {
15!
1575
                                scrollX /= scale;
×
1576
                                scrollY /= scale;
×
UNCOV
1577
                            }
×
1578
                            scrollRef.current.scrollTo(
15✔
1579
                                scrollX + scrollRef.current.scrollLeft,
15✔
1580
                                scrollY + scrollRef.current.scrollTop
15✔
1581
                            );
15✔
1582
                        }
15✔
1583
                    }
43✔
1584
                }
43✔
1585
            }
43✔
1586
        },
43✔
1587
        [rowMarkerOffset, rowMarkerWidth, totalHeaderHeight, lastRowSticky, freezeColumns, columns, rowHeight, rows]
678✔
1588
    );
678✔
1589

678✔
1590
    const focusCallback = React.useRef(focusOnRowFromTrailingBlankRow);
678✔
1591
    const getCellContentRef = React.useRef(getCellContent);
678✔
1592
    const rowsRef = React.useRef(rows);
678✔
1593
    focusCallback.current = focusOnRowFromTrailingBlankRow;
678✔
1594
    getCellContentRef.current = getCellContent;
678✔
1595
    rowsRef.current = rows;
678✔
1596
    const appendRow = React.useCallback(
678✔
1597
        async (col: number, openOverlay: boolean = true): Promise<void> => {
678✔
1598
            const c = mangledCols[col];
1✔
1599
            if (c?.trailingRowOptions?.disabled === true) {
1!
1600
                return;
×
UNCOV
1601
            }
×
1602
            const appendResult = onRowAppended?.();
1✔
1603

1✔
1604
            let r: "top" | "bottom" | number | undefined = undefined;
1✔
1605
            let bottom = true;
1✔
1606
            if (appendResult !== undefined) {
1!
1607
                r = await appendResult;
×
1608
                if (r === "top") bottom = false;
×
1609
                if (typeof r === "number") bottom = false;
×
UNCOV
1610
            }
×
1611

1✔
1612
            let backoff = 0;
1✔
1613
            const doFocus = () => {
1✔
1614
                if (rowsRef.current <= rows) {
2✔
1615
                    if (backoff < 500) {
1✔
1616
                        window.setTimeout(doFocus, backoff);
1✔
1617
                    }
1✔
1618
                    backoff = 50 + backoff * 2;
1✔
1619
                    return;
1✔
1620
                }
1✔
1621

1✔
1622
                const row = typeof r === "number" ? r : bottom ? rows : 0;
2!
1623
                scrollTo(col - rowMarkerOffset, row);
2✔
1624
                setCurrent(
2✔
1625
                    {
2✔
1626
                        cell: [col, row],
2✔
1627
                        range: {
2✔
1628
                            x: col,
2✔
1629
                            y: row,
2✔
1630
                            width: 1,
2✔
1631
                            height: 1,
2✔
1632
                        },
2✔
1633
                    },
2✔
1634
                    false,
2✔
1635
                    false,
2✔
1636
                    "edit"
2✔
1637
                );
2✔
1638

2✔
1639
                const cell = getCellContentRef.current([col - rowMarkerOffset, row]);
2✔
1640
                if (cell.allowOverlay && isReadWriteCell(cell) && cell.readonly !== true && openOverlay) {
2✔
1641
                    // wait for scroll to have a chance to process
1✔
1642
                    window.setTimeout(() => {
1✔
1643
                        focusCallback.current(col, row);
1✔
1644
                    }, 0);
1✔
1645
                }
1✔
1646
            };
2✔
1647
            // Queue up to allow the consumer to react to the event and let us check if they did
1✔
1648
            doFocus();
1✔
1649
        },
1✔
1650
        [mangledCols, onRowAppended, rowMarkerOffset, rows, scrollTo, setCurrent]
678✔
1651
    );
678✔
1652

678✔
1653
    const getCustomNewRowTargetColumn = React.useCallback(
678✔
1654
        (col: number): number | undefined => {
678✔
1655
            const customTargetColumn =
1✔
1656
                columns[col]?.trailingRowOptions?.targetColumn ?? trailingRowOptions?.targetColumn;
1!
1657

1✔
1658
            if (typeof customTargetColumn === "number") {
1!
1659
                const customTargetOffset = hasRowMarkers ? 1 : 0;
×
1660
                return customTargetColumn + customTargetOffset;
×
UNCOV
1661
            }
×
1662

1✔
1663
            if (typeof customTargetColumn === "object") {
1!
1664
                const maybeIndex = columnsIn.indexOf(customTargetColumn);
×
UNCOV
1665
                if (maybeIndex >= 0) {
×
1666
                    const customTargetOffset = hasRowMarkers ? 1 : 0;
×
1667
                    return maybeIndex + customTargetOffset;
×
UNCOV
1668
                }
×
UNCOV
1669
            }
×
1670

1✔
1671
            return undefined;
1✔
1672
        },
1✔
1673
        [columns, columnsIn, hasRowMarkers, trailingRowOptions?.targetColumn]
678✔
1674
    );
678✔
1675

678✔
1676
    const lastSelectedRowRef = React.useRef<number>();
678✔
1677
    const lastSelectedColRef = React.useRef<number>();
678✔
1678

678✔
1679
    const themeForCell = React.useCallback(
678✔
1680
        (cell: InnerGridCell, pos: Item): FullTheme => {
678✔
1681
            const [col, row] = pos;
20✔
1682
            return mergeAndRealizeTheme(
20✔
1683
                mergedTheme,
20✔
1684
                mangledCols[col]?.themeOverride,
20✔
1685
                getRowThemeOverride?.(row),
20!
1686
                cell.themeOverride
20✔
1687
            );
20✔
1688
        },
20✔
1689
        [getRowThemeOverride, mangledCols, mergedTheme]
678✔
1690
    );
678✔
1691

678✔
1692
    const handleSelect = React.useCallback(
678✔
1693
        (args: GridMouseEventArgs) => {
678✔
1694
            const isMultiKey = browserIsOSX.value ? args.metaKey : args.ctrlKey;
118!
1695
            const isMultiRow = isMultiKey && rowSelect === "multi";
118✔
1696
            const isMultiCol = isMultiKey && columnSelect === "multi";
118✔
1697
            const [col, row] = args.location;
118✔
1698
            const selectedColumns = gridSelection.columns;
118✔
1699
            const selectedRows = gridSelection.rows;
118✔
1700
            const [cellCol, cellRow] = gridSelection.current?.cell ?? [];
118✔
1701
            // eslint-disable-next-line unicorn/prefer-switch
118✔
1702
            if (args.kind === "cell") {
118✔
1703
                lastSelectedColRef.current = undefined;
102✔
1704

102✔
1705
                lastMouseSelectLocation.current = [col, row];
102✔
1706

102✔
1707
                if (col === 0 && hasRowMarkers) {
102✔
1708
                    if (
15✔
1709
                        (showTrailingBlankRow === true && row === rows) ||
15✔
1710
                        rowMarkers === "number" ||
15✔
1711
                        rowSelect === "none"
14✔
1712
                    )
15✔
1713
                        return;
15✔
1714

14✔
1715
                    const markerCell = getMangledCellContent(args.location);
14✔
1716
                    if (markerCell.kind !== InnerGridCellKind.Marker) {
15!
1717
                        return;
×
UNCOV
1718
                    }
✔
1719

14✔
1720
                    if (onRowMoved !== undefined) {
15!
1721
                        const renderer = getCellRenderer(markerCell);
×
1722
                        assert(renderer?.kind === InnerGridCellKind.Marker);
×
1723
                        const postClick = renderer?.onClick?.({
×
UNCOV
1724
                            ...args,
×
UNCOV
1725
                            cell: markerCell,
×
UNCOV
1726
                            posX: args.localEventX,
×
UNCOV
1727
                            posY: args.localEventY,
×
UNCOV
1728
                            bounds: args.bounds,
×
UNCOV
1729
                            theme: themeForCell(markerCell, args.location),
×
1730
                            preventDefault: () => undefined,
×
UNCOV
1731
                        }) as MarkerCell | undefined;
×
1732
                        if (postClick === undefined || postClick.checked === markerCell.checked) return;
×
UNCOV
1733
                    }
✔
1734

14✔
1735
                    setOverlay(undefined);
14✔
1736
                    focus();
14✔
1737
                    const isSelected = selectedRows.hasIndex(row);
14✔
1738

14✔
1739
                    const lastHighlighted = lastSelectedRowRef.current;
14✔
1740
                    if (
14✔
1741
                        rowSelect === "multi" &&
14✔
1742
                        (args.shiftKey || args.isLongTouch === true) &&
8✔
1743
                        lastHighlighted !== undefined &&
1✔
1744
                        selectedRows.hasIndex(lastHighlighted)
1✔
1745
                    ) {
15✔
1746
                        const newSlice: Slice = [Math.min(lastHighlighted, row), Math.max(lastHighlighted, row) + 1];
1✔
1747

1✔
1748
                        if (isMultiRow || rowSelectionMode === "multi") {
1!
1749
                            setSelectedRows(undefined, newSlice, true);
×
1750
                        } else {
1✔
1751
                            setSelectedRows(CompactSelection.fromSingleSelection(newSlice), undefined, isMultiRow);
1✔
1752
                        }
1✔
1753
                    } else if (isMultiRow || args.isTouch || rowSelectionMode === "multi") {
15✔
1754
                        if (isSelected) {
3✔
1755
                            setSelectedRows(selectedRows.remove(row), undefined, true);
1✔
1756
                        } else {
2✔
1757
                            setSelectedRows(undefined, row, true);
2✔
1758
                            lastSelectedRowRef.current = row;
2✔
1759
                        }
2✔
1760
                    } else if (isSelected && selectedRows.length === 1) {
13✔
1761
                        setSelectedRows(CompactSelection.empty(), undefined, isMultiKey);
1✔
1762
                    } else {
10✔
1763
                        setSelectedRows(CompactSelection.fromSingleSelection(row), undefined, isMultiKey);
9✔
1764
                        lastSelectedRowRef.current = row;
9✔
1765
                    }
9✔
1766
                } else if (col >= rowMarkerOffset && showTrailingBlankRow && row === rows) {
102✔
1767
                    const customTargetColumn = getCustomNewRowTargetColumn(col);
1✔
1768
                    void appendRow(customTargetColumn ?? col);
1✔
1769
                } else {
87✔
1770
                    if (cellCol !== col || cellRow !== row) {
86✔
1771
                        const cell = getMangledCellContent(args.location);
80✔
1772
                        const renderer = getCellRenderer(cell);
80✔
1773

80✔
1774
                        if (renderer?.onSelect !== undefined) {
80!
1775
                            let prevented = false;
×
1776
                            renderer.onSelect({
×
UNCOV
1777
                                ...args,
×
UNCOV
1778
                                cell,
×
UNCOV
1779
                                posX: args.localEventX,
×
UNCOV
1780
                                posY: args.localEventY,
×
UNCOV
1781
                                bounds: args.bounds,
×
1782
                                preventDefault: () => (prevented = true),
×
UNCOV
1783
                                theme: themeForCell(cell, args.location),
×
UNCOV
1784
                            });
×
UNCOV
1785
                            if (prevented) {
×
1786
                                return;
×
UNCOV
1787
                            }
×
UNCOV
1788
                        }
×
1789
                        const isLastStickyRow = lastRowSticky && row === rows;
80✔
1790

80✔
1791
                        const startedFromLastSticky =
80✔
1792
                            lastRowSticky && gridSelection !== undefined && gridSelection.current?.cell[1] === rows;
80✔
1793

80✔
1794
                        if (
80✔
1795
                            (args.shiftKey || args.isLongTouch === true) &&
80✔
1796
                            cellCol !== undefined &&
6✔
1797
                            cellRow !== undefined &&
6✔
1798
                            gridSelection.current !== undefined &&
6✔
1799
                            !startedFromLastSticky
6✔
1800
                        ) {
80✔
1801
                            if (isLastStickyRow) {
6!
UNCOV
1802
                                // If we're making a selection and shift click in to the last sticky row,
×
UNCOV
1803
                                // just drop the event. Don't kill the selection.
×
1804
                                return;
×
UNCOV
1805
                            }
×
1806

6✔
1807
                            const left = Math.min(col, cellCol);
6✔
1808
                            const right = Math.max(col, cellCol);
6✔
1809
                            const top = Math.min(row, cellRow);
6✔
1810
                            const bottom = Math.max(row, cellRow);
6✔
1811
                            setCurrent(
6✔
1812
                                {
6✔
1813
                                    ...gridSelection.current,
6✔
1814
                                    range: {
6✔
1815
                                        x: left,
6✔
1816
                                        y: top,
6✔
1817
                                        width: right - left + 1,
6✔
1818
                                        height: bottom - top + 1,
6✔
1819
                                    },
6✔
1820
                                },
6✔
1821
                                true,
6✔
1822
                                isMultiKey,
6✔
1823
                                "click"
6✔
1824
                            );
6✔
1825
                            lastSelectedRowRef.current = undefined;
6✔
1826
                            focus();
6✔
1827
                        } else {
80✔
1828
                            setCurrent(
74✔
1829
                                {
74✔
1830
                                    cell: [col, row],
74✔
1831
                                    range: { x: col, y: row, width: 1, height: 1 },
74✔
1832
                                },
74✔
1833
                                true,
74✔
1834
                                isMultiKey,
74✔
1835
                                "click"
74✔
1836
                            );
74✔
1837
                            lastSelectedRowRef.current = undefined;
74✔
1838
                            setOverlay(undefined);
74✔
1839
                            focus();
74✔
1840
                        }
74✔
1841
                    }
80✔
1842
                }
86✔
1843
            } else if (args.kind === "header") {
118✔
1844
                lastMouseSelectLocation.current = [col, row];
12✔
1845
                setOverlay(undefined);
12✔
1846
                if (hasRowMarkers && col === 0) {
12✔
1847
                    lastSelectedRowRef.current = undefined;
4✔
1848
                    lastSelectedColRef.current = undefined;
4✔
1849
                    if (rowSelect === "multi") {
4✔
1850
                        if (selectedRows.length !== rows) {
4✔
1851
                            setSelectedRows(CompactSelection.fromSingleSelection([0, rows]), undefined, isMultiKey);
3✔
1852
                        } else {
4✔
1853
                            setSelectedRows(CompactSelection.empty(), undefined, isMultiKey);
1✔
1854
                        }
1✔
1855
                        focus();
4✔
1856
                    }
4✔
1857
                } else {
12✔
1858
                    const lastCol = lastSelectedColRef.current;
8✔
1859
                    if (
8✔
1860
                        columnSelect === "multi" &&
8✔
1861
                        (args.shiftKey || args.isLongTouch === true) &&
7!
UNCOV
1862
                        lastCol !== undefined &&
×
UNCOV
1863
                        selectedColumns.hasIndex(lastCol)
×
1864
                    ) {
8!
1865
                        const newSlice: Slice = [Math.min(lastCol, col), Math.max(lastCol, col) + 1];
×
UNCOV
1866

×
UNCOV
1867
                        if (isMultiCol) {
×
1868
                            setSelectedColumns(undefined, newSlice, isMultiKey);
×
UNCOV
1869
                        } else {
×
1870
                            setSelectedColumns(CompactSelection.fromSingleSelection(newSlice), undefined, isMultiKey);
×
UNCOV
1871
                        }
×
1872
                    } else if (isMultiCol) {
8✔
1873
                        if (selectedColumns.hasIndex(col)) {
1!
1874
                            setSelectedColumns(selectedColumns.remove(col), undefined, isMultiKey);
×
1875
                        } else {
1✔
1876
                            setSelectedColumns(undefined, col, isMultiKey);
1✔
1877
                        }
1✔
1878
                        lastSelectedColRef.current = col;
1✔
1879
                    } else if (columnSelect !== "none") {
8✔
1880
                        setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, isMultiKey);
7✔
1881
                        lastSelectedColRef.current = col;
7✔
1882
                    }
7✔
1883
                    lastSelectedRowRef.current = undefined;
8✔
1884
                    focus();
8✔
1885
                }
8✔
1886
            } else if (args.kind === groupHeaderKind) {
16✔
1887
                lastMouseSelectLocation.current = [col, row];
3✔
1888
            } else if (args.kind === outOfBoundsKind && !args.isMaybeScrollbar) {
4✔
1889
                setGridSelection(emptyGridSelection, false);
1✔
1890
                setOverlay(undefined);
1✔
1891
                focus();
1✔
1892
                onSelectionCleared?.();
1!
1893
                lastSelectedRowRef.current = undefined;
1✔
1894
                lastSelectedColRef.current = undefined;
1✔
1895
            }
1✔
1896
        },
118✔
1897
        [
678✔
1898
            appendRow,
678✔
1899
            columnSelect,
678✔
1900
            focus,
678✔
1901
            getCellRenderer,
678✔
1902
            getCustomNewRowTargetColumn,
678✔
1903
            getMangledCellContent,
678✔
1904
            gridSelection,
678✔
1905
            hasRowMarkers,
678✔
1906
            lastRowSticky,
678✔
1907
            onSelectionCleared,
678✔
1908
            onRowMoved,
678✔
1909
            rowMarkerOffset,
678✔
1910
            rowMarkers,
678✔
1911
            rowSelect,
678✔
1912
            rowSelectionMode,
678✔
1913
            rows,
678✔
1914
            setCurrent,
678✔
1915
            setGridSelection,
678✔
1916
            setSelectedColumns,
678✔
1917
            setSelectedRows,
678✔
1918
            showTrailingBlankRow,
678✔
1919
            themeForCell,
678✔
1920
        ]
678✔
1921
    );
678✔
1922
    const isActivelyDraggingHeader = React.useRef(false);
678✔
1923
    const lastMouseSelectLocation = React.useRef<readonly [number, number]>();
678✔
1924
    const touchDownArgs = React.useRef(visibleRegion);
678✔
1925
    const mouseDownData = React.useRef<{
678✔
1926
        time: number;
678✔
1927
        button: number;
678✔
1928
        location: Item;
678✔
1929
    }>();
678✔
1930
    const onMouseDown = React.useCallback(
678✔
1931
        (args: GridMouseEventArgs) => {
678✔
1932
            isPrevented.current = false;
135✔
1933
            touchDownArgs.current = visibleRegionRef.current;
135✔
1934
            if (args.button !== 0 && args.button !== 1) {
135✔
1935
                mouseDownData.current = undefined;
1✔
1936
                return;
1✔
1937
            }
1✔
1938

134✔
1939
            const time = performance.now();
134✔
1940
            mouseDownData.current = {
134✔
1941
                button: args.button,
134✔
1942
                time,
134✔
1943
                location: args.location,
134✔
1944
            };
134✔
1945

134✔
1946
            if (args?.kind === "header") {
135✔
1947
                isActivelyDraggingHeader.current = true;
18✔
1948
            }
18✔
1949

134✔
1950
            const fh = args.kind === "cell" && args.isFillHandle;
135✔
1951

135✔
1952
            if (!fh && args.kind !== "cell" && args.isEdge) return;
135✔
1953

127✔
1954
            setMouseState({
127✔
1955
                previousSelection: gridSelection,
127✔
1956
                fillHandle: fh,
127✔
1957
            });
127✔
1958
            lastMouseSelectLocation.current = undefined;
127✔
1959

127✔
1960
            if (!args.isTouch && args.button === 0 && !fh) {
135✔
1961
                handleSelect(args);
115✔
1962
            } else if (!args.isTouch && args.button === 1) {
135✔
1963
                lastMouseSelectLocation.current = args.location;
4✔
1964
            }
4✔
1965
        },
135✔
1966
        [gridSelection, handleSelect]
678✔
1967
    );
678✔
1968

678✔
1969
    const [renameGroup, setRenameGroup] = React.useState<{
678✔
1970
        group: string;
678✔
1971
        bounds: Rectangle;
678✔
1972
    }>();
678✔
1973

678✔
1974
    const handleGroupHeaderSelection = React.useCallback(
678✔
1975
        (args: GridMouseEventArgs) => {
678✔
1976
            if (args.kind !== groupHeaderKind || columnSelect !== "multi") {
3!
1977
                return;
×
UNCOV
1978
            }
×
1979
            const isMultiKey = browserIsOSX.value ? args.metaKey : args.ctrlKey;
3!
1980
            const [col] = args.location;
3✔
1981
            const selectedColumns = gridSelection.columns;
3✔
1982

3✔
1983
            if (col < rowMarkerOffset) return;
3!
1984

3✔
1985
            const needle = mangledCols[col];
3✔
1986
            let start = col;
3✔
1987
            let end = col;
3✔
1988
            for (let i = col - 1; i >= rowMarkerOffset; i--) {
3✔
1989
                if (!isGroupEqual(needle.group, mangledCols[i].group)) break;
3!
1990
                start--;
3✔
1991
            }
3✔
1992

3✔
1993
            for (let i = col + 1; i < mangledCols.length; i++) {
3✔
1994
                if (!isGroupEqual(needle.group, mangledCols[i].group)) break;
27!
1995
                end++;
27✔
1996
            }
27✔
1997

3✔
1998
            focus();
3✔
1999

3✔
2000
            if (isMultiKey) {
3✔
2001
                if (selectedColumns.hasAll([start, end + 1])) {
2✔
2002
                    let newVal = selectedColumns;
1✔
2003
                    for (let index = start; index <= end; index++) {
1✔
2004
                        newVal = newVal.remove(index);
11✔
2005
                    }
11✔
2006
                    setSelectedColumns(newVal, undefined, isMultiKey);
1✔
2007
                } else {
1✔
2008
                    setSelectedColumns(undefined, [start, end + 1], isMultiKey);
1✔
2009
                }
1✔
2010
            } else {
3✔
2011
                setSelectedColumns(CompactSelection.fromSingleSelection([start, end + 1]), undefined, isMultiKey);
1✔
2012
            }
1✔
2013
        },
3✔
2014
        [columnSelect, focus, gridSelection.columns, mangledCols, rowMarkerOffset, setSelectedColumns]
678✔
2015
    );
678✔
2016

678✔
2017
    const fillDown = React.useCallback(
678✔
2018
        (reverse: boolean) => {
678✔
2019
            if (gridSelection.current === undefined) return;
1!
2020
            const v: EditListItem[] = [];
1✔
2021
            const r = gridSelection.current.range;
1✔
2022
            for (let x = 0; x < r.width; x++) {
1✔
2023
                const fillCol = x + r.x;
2✔
2024
                const fillVal = getMangledCellContent([fillCol, reverse ? r.y + r.height - 1 : r.y]);
2!
2025
                if (isInnerOnlyCell(fillVal) || !isReadWriteCell(fillVal)) continue;
2!
2026
                for (let y = 1; y < r.height; y++) {
2✔
2027
                    const fillRow = reverse ? r.y + r.height - (y + 1) : y + r.y;
8!
2028
                    const target = [fillCol, fillRow] as const;
8✔
2029
                    v.push({
8✔
2030
                        location: target,
8✔
2031
                        value: { ...fillVal },
8✔
2032
                    });
8✔
2033
                }
8✔
2034
            }
2✔
2035

1✔
2036
            mangledOnCellsEdited(v);
1✔
2037

1✔
2038
            gridRef.current?.damage(
1✔
2039
                v.map(c => ({
1✔
2040
                    cell: c.location,
8✔
2041
                }))
1✔
2042
            );
1✔
2043
        },
1✔
2044
        [getMangledCellContent, gridSelection, mangledOnCellsEdited]
678✔
2045
    );
678✔
2046

678✔
2047
    const isPrevented = React.useRef(false);
678✔
2048

678✔
2049
    const normalSizeColumn = React.useCallback(
678✔
2050
        async (col: number): Promise<void> => {
678✔
2051
            if (getCellsForSelection !== undefined && onColumnResize !== undefined) {
2✔
2052
                const start = visibleRegionRef.current.y;
2✔
2053
                const end = visibleRegionRef.current.height;
2✔
2054
                let cells = getCellsForSelection(
2✔
2055
                    {
2✔
2056
                        x: col,
2✔
2057
                        y: start,
2✔
2058
                        width: 1,
2✔
2059
                        height: Math.min(end, rows - start),
2✔
2060
                    },
2✔
2061
                    abortControllerRef.current.signal
2✔
2062
                );
2✔
2063
                if (typeof cells !== "object") {
2!
2064
                    cells = await cells();
×
UNCOV
2065
                }
×
2066
                const inputCol = columns[col - rowMarkerOffset];
2✔
2067
                const offscreen = document.createElement("canvas");
2✔
2068
                const ctx = offscreen.getContext("2d", { alpha: false });
2✔
2069
                if (ctx !== null) {
2✔
2070
                    ctx.font = mergedTheme.baseFontFull;
2✔
2071
                    const newCol = measureColumn(
2✔
2072
                        ctx,
2✔
2073
                        mergedTheme,
2✔
2074
                        inputCol,
2✔
2075
                        0,
2✔
2076
                        cells,
2✔
2077
                        minColumnWidth,
2✔
2078
                        maxColumnWidth,
2✔
2079
                        false,
2✔
2080
                        getCellRenderer
2✔
2081
                    );
2✔
2082
                    onColumnResize?.(inputCol, newCol.width, col, newCol.width);
2✔
2083
                }
2✔
2084
            }
2✔
2085
        },
2✔
2086
        [
678✔
2087
            columns,
678✔
2088
            getCellsForSelection,
678✔
2089
            maxColumnWidth,
678✔
2090
            mergedTheme,
678✔
2091
            minColumnWidth,
678✔
2092
            onColumnResize,
678✔
2093
            rowMarkerOffset,
678✔
2094
            rows,
678✔
2095
            getCellRenderer,
678✔
2096
        ]
678✔
2097
    );
678✔
2098

678✔
2099
    const [scrollDir, setScrollDir] = React.useState<GridMouseEventArgs["scrollEdge"]>();
678✔
2100

678✔
2101
    const fillPattern = React.useCallback(
678✔
2102
        async (previousSelection: GridSelection, currentSelection: GridSelection) => {
678✔
2103
            const patternRange = previousSelection.current?.range;
4✔
2104

4✔
2105
            if (
4✔
2106
                patternRange === undefined ||
4✔
2107
                getCellsForSelection === undefined ||
4✔
2108
                currentSelection.current === undefined
4✔
2109
            ) {
4!
NEW
2110
                return;
×
NEW
2111
            }
×
2112
            const currentRange = currentSelection.current.range;
4✔
2113

4✔
2114
            if (onFillPattern !== undefined) {
4!
NEW
2115
                let canceled = false;
×
NEW
2116
                onFillPattern({
×
NEW
2117
                    fillDestination: { ...currentRange, x: currentRange.x - rowMarkerOffset },
×
NEW
2118
                    patternSource: { ...patternRange, x: patternRange.x - rowMarkerOffset },
×
NEW
2119
                    preventDefault: () => (canceled = true),
×
NEW
2120
                });
×
NEW
2121
                if (canceled) return;
×
NEW
2122
            }
×
2123

4✔
2124
            let cells = getCellsForSelection(patternRange, abortControllerRef.current.signal);
4✔
2125
            if (typeof cells !== "object") cells = await cells();
4!
2126

4✔
2127
            const pattern = cells;
4✔
2128

4✔
2129
            // loop through all cells in currentSelection.current.range
4✔
2130
            const editItemList: EditListItem[] = [];
4✔
2131
            for (let x = 0; x < currentRange.width; x++) {
4✔
2132
                for (let y = 0; y < currentRange.height; y++) {
4✔
2133
                    const cell: Item = [currentRange.x + x, currentRange.y + y];
15✔
2134
                    if (itemIsInRect(cell, patternRange)) continue;
15✔
2135
                    const patternCell = pattern[y % patternRange.height][x % patternRange.width];
11✔
2136
                    if (isInnerOnlyCell(patternCell) || !isReadWriteCell(patternCell)) continue;
15!
2137
                    editItemList.push({
11✔
2138
                        location: cell,
11✔
2139
                        value: { ...patternCell },
11✔
2140
                    });
11✔
2141
                }
11✔
2142
            }
4✔
2143
            mangledOnCellsEdited(editItemList);
4✔
2144

4✔
2145
            gridRef.current?.damage(
4✔
2146
                editItemList.map(c => ({
4✔
2147
                    cell: c.location,
11✔
2148
                }))
4✔
2149
            );
4✔
2150
        },
4✔
2151
        [getCellsForSelection, mangledOnCellsEdited, onFillPattern, rowMarkerOffset]
678✔
2152
    );
678✔
2153

678✔
2154
    const onMouseUp = React.useCallback(
678✔
2155
        (args: GridMouseEventArgs, isOutside: boolean) => {
678✔
2156
            const mouse = mouseState;
134✔
2157
            setMouseState(undefined);
134✔
2158
            setFillHighlightRegion(undefined);
134✔
2159
            setScrollDir(undefined);
134✔
2160
            isActivelyDraggingHeader.current = false;
134✔
2161

134✔
2162
            if (isOutside) return;
134✔
2163

133✔
2164
            if (
133✔
2165
                mouse?.fillHandle === true &&
134✔
2166
                gridSelection.current !== undefined &&
4✔
2167
                mouse.previousSelection?.current !== undefined
4✔
2168
            ) {
134✔
2169
                if (fillHighlightRegion === undefined) return;
4!
2170
                const newRange = {
4✔
2171
                    ...gridSelection,
4✔
2172
                    current: {
4✔
2173
                        ...gridSelection.current,
4✔
2174
                        range: combineRects(mouse.previousSelection.current.range, fillHighlightRegion),
4✔
2175
                    },
4✔
2176
                };
4✔
2177
                void fillPattern(mouse.previousSelection, newRange);
4✔
2178
                setGridSelection(newRange, true);
4✔
2179
                return;
4✔
2180
            }
4✔
2181

129✔
2182
            const [col, row] = args.location;
129✔
2183
            const [lastMouseDownCol, lastMouseDownRow] = lastMouseSelectLocation.current ?? [];
134✔
2184

134✔
2185
            const preventDefault = () => {
134✔
2186
                isPrevented.current = true;
×
UNCOV
2187
            };
×
2188

134✔
2189
            const handleMaybeClick = (a: GridMouseCellEventArgs): boolean => {
134✔
2190
                const isValidClick = a.isTouch || (lastMouseDownCol === col && lastMouseDownRow === row);
104✔
2191
                if (isValidClick) {
104✔
2192
                    onCellClicked?.([col - rowMarkerOffset, row], {
94✔
2193
                        ...a,
4✔
2194
                        preventDefault,
4✔
2195
                    });
4✔
2196
                }
94✔
2197
                if (a.button === 1) return !isPrevented.current;
104✔
2198
                if (!isPrevented.current) {
101✔
2199
                    const c = getMangledCellContent(args.location);
101✔
2200
                    const r = getCellRenderer(c);
101✔
2201
                    if (r !== undefined && r.onClick !== undefined && isValidClick) {
101✔
2202
                        const newVal = r.onClick({
20✔
2203
                            ...a,
20✔
2204
                            cell: c,
20✔
2205
                            posX: a.localEventX,
20✔
2206
                            posY: a.localEventY,
20✔
2207
                            bounds: a.bounds,
20✔
2208
                            theme: themeForCell(c, args.location),
20✔
2209
                            preventDefault,
20✔
2210
                        });
20✔
2211
                        if (newVal !== undefined && !isInnerOnlyCell(newVal) && isEditableGridCell(newVal)) {
20✔
2212
                            mangledOnCellsEdited([{ location: a.location, value: newVal }]);
4✔
2213
                            gridRef.current?.damage([
4✔
2214
                                {
4✔
2215
                                    cell: a.location,
4✔
2216
                                },
4✔
2217
                            ]);
4✔
2218
                        }
4✔
2219
                    }
20✔
2220
                    if (isPrevented.current || gridSelection.current === undefined) return false;
101✔
2221

85✔
2222
                    let shouldActivate = false;
85✔
2223
                    switch (cellActivationBehavior) {
85✔
2224
                        case "double-click":
101✔
2225
                        case "second-click": {
101✔
2226
                            if (mouse?.previousSelection?.current?.cell === undefined) break;
84✔
2227
                            const [selectedCol, selectedRow] = gridSelection.current.cell;
22✔
2228
                            const [prevCol, prevRow] = mouse.previousSelection.current.cell;
22✔
2229
                            const isClickOnSelected =
22✔
2230
                                col === selectedCol && col === prevCol && row === selectedRow && row === prevRow;
84✔
2231
                            shouldActivate =
84✔
2232
                                isClickOnSelected &&
84✔
2233
                                (a.isDoubleClick === true || cellActivationBehavior === "second-click");
6✔
2234
                            break;
84✔
2235
                        }
84✔
2236
                        case "single-click": {
101✔
2237
                            shouldActivate = true;
1✔
2238
                            break;
1✔
2239
                        }
1✔
2240
                    }
101✔
2241
                    if (shouldActivate) {
101✔
2242
                        onCellActivated?.([col - rowMarkerOffset, row]);
6✔
2243
                        reselect(a.bounds, false);
6✔
2244
                        return true;
6✔
2245
                    }
6✔
2246
                }
101✔
2247
                return false;
79✔
2248
            };
104✔
2249

134✔
2250
            const clickLocation = args.location[0] - rowMarkerOffset;
134✔
2251
            if (args.isTouch) {
134✔
2252
                const vr = visibleRegionRef.current;
4✔
2253
                const touchVr = touchDownArgs.current;
4✔
2254
                if (vr.x !== touchVr.x || vr.y !== touchVr.y) {
4!
UNCOV
2255
                    // we scrolled, abort
×
2256
                    return;
×
UNCOV
2257
                }
×
2258
                // take care of context menus first if long pressed item is already selected
4✔
2259
                if (args.isLongTouch === true) {
4!
NEW
2260
                    if (args.kind === "cell" && itemsAreEqual(gridSelection.current?.cell, args.location)) {
×
UNCOV
2261
                        onCellContextMenu?.([clickLocation, args.location[1]], {
×
UNCOV
2262
                            ...args,
×
UNCOV
2263
                            preventDefault,
×
UNCOV
2264
                        });
×
2265
                        return;
×
UNCOV
2266
                    } else if (args.kind === "header" && gridSelection.columns.hasIndex(col)) {
×
2267
                        onHeaderContextMenu?.(clickLocation, { ...args, preventDefault });
×
2268
                        return;
×
UNCOV
2269
                    } else if (args.kind === groupHeaderKind) {
×
UNCOV
2270
                        if (clickLocation < 0) {
×
2271
                            return;
×
UNCOV
2272
                        }
×
UNCOV
2273

×
2274
                        onGroupHeaderContextMenu?.(clickLocation, { ...args, preventDefault });
×
2275
                        return;
×
UNCOV
2276
                    }
×
UNCOV
2277
                }
×
2278
                if (args.kind === "cell") {
4✔
2279
                    // click that cell
2✔
2280
                    if (!handleMaybeClick(args)) {
2✔
2281
                        handleSelect(args);
2✔
2282
                    }
2✔
2283
                } else if (args.kind === groupHeaderKind) {
2✔
2284
                    onGroupHeaderClicked?.(clickLocation, { ...args, preventDefault });
1✔
2285
                } else {
1✔
2286
                    if (args.kind === headerKind) {
1✔
2287
                        onHeaderClicked?.(clickLocation, {
1✔
2288
                            ...args,
1✔
2289
                            preventDefault,
1✔
2290
                        });
1✔
2291
                    }
1✔
2292
                    handleSelect(args);
1✔
2293
                }
1✔
2294
                return;
4✔
2295
            }
4✔
2296

125✔
2297
            if (args.kind === "header") {
134✔
2298
                if (clickLocation < 0) {
16✔
2299
                    return;
3✔
2300
                }
3✔
2301

13✔
2302
                if (args.isEdge) {
16✔
2303
                    if (args.isDoubleClick === true) {
2✔
2304
                        void normalSizeColumn(col);
1✔
2305
                    }
1✔
2306
                } else if (args.button === 0 && col === lastMouseDownCol && row === lastMouseDownRow) {
16✔
2307
                    onHeaderClicked?.(clickLocation, { ...args, preventDefault });
5✔
2308
                }
5✔
2309
            }
16✔
2310

122✔
2311
            if (args.kind === groupHeaderKind) {
134✔
2312
                if (clickLocation < 0) {
3!
2313
                    return;
×
UNCOV
2314
                }
×
2315

3✔
2316
                if (args.button === 0 && col === lastMouseDownCol && row === lastMouseDownRow) {
3✔
2317
                    onGroupHeaderClicked?.(clickLocation, { ...args, preventDefault });
3!
2318
                    if (!isPrevented.current) {
3✔
2319
                        handleGroupHeaderSelection(args);
3✔
2320
                    }
3✔
2321
                }
3✔
2322
            }
3✔
2323

122✔
2324
            if (args.kind === "cell" && (args.button === 0 || args.button === 1)) {
134✔
2325
                handleMaybeClick(args);
102✔
2326
            }
102✔
2327

122✔
2328
            lastMouseSelectLocation.current = undefined;
122✔
2329
        },
134✔
2330
        [
678✔
2331
            mouseState,
678✔
2332
            gridSelection,
678✔
2333
            rowMarkerOffset,
678✔
2334
            fillHighlightRegion,
678✔
2335
            fillPattern,
678✔
2336
            setGridSelection,
678✔
2337
            onCellClicked,
678✔
2338
            getMangledCellContent,
678✔
2339
            getCellRenderer,
678✔
2340
            cellActivationBehavior,
678✔
2341
            themeForCell,
678✔
2342
            mangledOnCellsEdited,
678✔
2343
            onCellActivated,
678✔
2344
            reselect,
678✔
2345
            onCellContextMenu,
678✔
2346
            onHeaderContextMenu,
678✔
2347
            onGroupHeaderContextMenu,
678✔
2348
            handleSelect,
678✔
2349
            onGroupHeaderClicked,
678✔
2350
            onHeaderClicked,
678✔
2351
            normalSizeColumn,
678✔
2352
            handleGroupHeaderSelection,
678✔
2353
        ]
678✔
2354
    );
678✔
2355

678✔
2356
    const onMouseMoveImpl = React.useCallback(
678✔
2357
        (args: GridMouseEventArgs) => {
678✔
2358
            const a: GridMouseEventArgs = {
38✔
2359
                ...args,
38✔
2360
                location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
38✔
2361
            };
38✔
2362
            onMouseMove?.(a);
38✔
2363
            setScrollDir(cv => {
38✔
2364
                if (isActivelyDraggingHeader.current) return [args.scrollEdge[0], 0];
38✔
2365
                if (args.scrollEdge[0] === cv?.[0] && args.scrollEdge[1] === cv[1]) return cv;
38✔
2366
                return mouseState === undefined || (mouseDownData.current?.location[0] ?? 0) < rowMarkerOffset
36!
2367
                    ? undefined
13✔
2368
                    : args.scrollEdge;
13✔
2369
            });
38✔
2370
        },
38✔
2371
        [mouseState, onMouseMove, rowMarkerOffset]
678✔
2372
    );
678✔
2373

678✔
2374
    const onHeaderMenuClickInner = React.useCallback(
678✔
2375
        (col: number, screenPosition: Rectangle) => {
678✔
2376
            onHeaderMenuClick?.(col - rowMarkerOffset, screenPosition);
1✔
2377
        },
1✔
2378
        [onHeaderMenuClick, rowMarkerOffset]
678✔
2379
    );
678✔
2380

678✔
2381
    const currentCell = gridSelection?.current?.cell;
678✔
2382
    const onVisibleRegionChangedImpl = React.useCallback(
678✔
2383
        (
678✔
2384
            region: Rectangle,
141✔
2385
            clientWidth: number,
141✔
2386
            clientHeight: number,
141✔
2387
            rightElWidth: number,
141✔
2388
            tx: number,
141✔
2389
            ty: number
141✔
2390
        ) => {
141✔
2391
            hasJustScrolled.current = false;
141✔
2392
            let selected = currentCell;
141✔
2393
            if (selected !== undefined) {
141✔
2394
                selected = [selected[0] - rowMarkerOffset, selected[1]];
10✔
2395
            }
10✔
2396
            const newRegion = {
141✔
2397
                x: region.x - rowMarkerOffset,
141✔
2398
                y: region.y,
141✔
2399
                width: region.width,
141✔
2400
                height: showTrailingBlankRow && region.y + region.height >= rows ? region.height - 1 : region.height,
141✔
2401
                tx,
141✔
2402
                ty,
141✔
2403
                extras: {
141✔
2404
                    selected,
141✔
2405
                    freezeRegion:
141✔
2406
                        freezeColumns === 0
141✔
2407
                            ? undefined
141!
UNCOV
2408
                            : {
×
UNCOV
2409
                                  x: 0,
×
UNCOV
2410
                                  y: region.y,
×
UNCOV
2411
                                  width: freezeColumns,
×
UNCOV
2412
                                  height: region.height,
×
UNCOV
2413
                              },
×
2414
                },
141✔
2415
            };
141✔
2416
            visibleRegionRef.current = newRegion;
141✔
2417
            setVisibleRegion(newRegion);
141✔
2418
            setClientSize([clientWidth, clientHeight, rightElWidth]);
141✔
2419
            onVisibleRegionChanged?.(newRegion, newRegion.tx, newRegion.ty, newRegion.extras);
141!
2420
        },
141✔
2421
        [
678✔
2422
            currentCell,
678✔
2423
            rowMarkerOffset,
678✔
2424
            showTrailingBlankRow,
678✔
2425
            rows,
678✔
2426
            freezeColumns,
678✔
2427
            setVisibleRegion,
678✔
2428
            onVisibleRegionChanged,
678✔
2429
        ]
678✔
2430
    );
678✔
2431

678✔
2432
    const onColumnMovedImpl = whenDefined(
678✔
2433
        onColumnMoved,
678✔
2434
        React.useCallback(
678✔
2435
            (startIndex: number, endIndex: number) => {
678✔
2436
                onColumnMoved?.(startIndex - rowMarkerOffset, endIndex - rowMarkerOffset);
1✔
2437
                if (columnSelect !== "none") {
1✔
2438
                    setSelectedColumns(CompactSelection.fromSingleSelection(endIndex), undefined, true);
1✔
2439
                }
1✔
2440
            },
1✔
2441
            [columnSelect, onColumnMoved, rowMarkerOffset, setSelectedColumns]
678✔
2442
        )
678✔
2443
    );
678✔
2444

678✔
2445
    const isActivelyDragging = React.useRef(false);
678✔
2446
    const onDragStartImpl = React.useCallback(
678✔
2447
        (args: GridDragEventArgs) => {
678✔
2448
            if (args.location[0] === 0 && rowMarkerOffset > 0) {
1!
2449
                args.preventDefault();
×
2450
                return;
×
UNCOV
2451
            }
×
2452
            onDragStart?.({
1✔
2453
                ...args,
1✔
2454
                location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
1✔
2455
            });
1✔
2456

1✔
2457
            if (!args.defaultPrevented()) {
1✔
2458
                isActivelyDragging.current = true;
1✔
2459
            }
1✔
2460
            setMouseState(undefined);
1✔
2461
        },
1✔
2462
        [onDragStart, rowMarkerOffset]
678✔
2463
    );
678✔
2464

678✔
2465
    const onDragEnd = React.useCallback(() => {
678✔
2466
        isActivelyDragging.current = false;
×
2467
    }, []);
678✔
2468

678✔
2469
    const hoveredRef = React.useRef<GridMouseEventArgs>();
678✔
2470
    const onItemHoveredImpl = React.useCallback(
678✔
2471
        (args: GridMouseEventArgs) => {
678✔
2472
            if (mouseEventArgsAreEqual(args, hoveredRef.current)) return;
33!
2473
            hoveredRef.current = args;
33✔
2474
            if (mouseDownData?.current?.button !== undefined && mouseDownData.current.button >= 1) return;
33✔
2475
            if (
32✔
2476
                mouseState !== undefined &&
32✔
2477
                mouseDownData.current?.location[0] === 0 &&
19✔
2478
                args.location[0] === 0 &&
3✔
2479
                rowMarkerOffset === 1 &&
3✔
2480
                rowSelect === "multi" &&
3✔
2481
                mouseState.previousSelection &&
2✔
2482
                !mouseState.previousSelection.rows.hasIndex(mouseDownData.current.location[1]) &&
2✔
2483
                gridSelection.rows.hasIndex(mouseDownData.current.location[1])
2✔
2484
            ) {
33✔
2485
                const start = Math.min(mouseDownData.current.location[1], args.location[1]);
2✔
2486
                const end = Math.max(mouseDownData.current.location[1], args.location[1]) + 1;
2✔
2487
                setSelectedRows(CompactSelection.fromSingleSelection([start, end]), undefined, false);
2✔
2488
            }
2✔
2489
            if (
32✔
2490
                mouseState !== undefined &&
32✔
2491
                gridSelection.current !== undefined &&
19✔
2492
                !isActivelyDragging.current &&
17✔
2493
                !isActivelyDraggingHeader.current &&
17✔
2494
                (rangeSelect === "rect" || rangeSelect === "multi-rect")
15✔
2495
            ) {
33✔
2496
                const [selectedCol, selectedRow] = gridSelection.current.cell;
11✔
2497
                // eslint-disable-next-line prefer-const
11✔
2498
                let [col, row] = args.location;
11✔
2499

11✔
2500
                if (row < 0) {
11✔
2501
                    row = visibleRegionRef.current.y;
1✔
2502
                }
1✔
2503

11✔
2504
                if (mouseState.fillHandle === true && mouseState.previousSelection?.current !== undefined) {
11✔
2505
                    const prevRange = mouseState.previousSelection.current.range;
5✔
2506
                    row = Math.min(row, lastRowSticky ? rows - 1 : rows);
5!
2507
                    const rect = getClosestRect(prevRange, col, row, allowedFillDirections);
5✔
2508
                    setFillHighlightRegion(rect);
5✔
2509
                } else {
11✔
2510
                    const startedFromLastStickyRow = lastRowSticky && selectedRow === rows;
6✔
2511
                    if (startedFromLastStickyRow) return;
6!
2512

6✔
2513
                    const landedOnLastStickyRow = lastRowSticky && row === rows;
6✔
2514
                    if (landedOnLastStickyRow) {
6!
NEW
2515
                        if (args.kind === outOfBoundsKind) row--;
×
NEW
2516
                        else return;
×
NEW
2517
                    }
×
2518

6✔
2519
                    col = Math.max(col, rowMarkerOffset);
6✔
2520

6✔
2521
                    const deltaX = col - selectedCol;
6✔
2522
                    const deltaY = row - selectedRow;
6✔
2523

6✔
2524
                    const newRange: Rectangle = {
6✔
2525
                        x: deltaX >= 0 ? selectedCol : col,
6!
2526
                        y: deltaY >= 0 ? selectedRow : row,
6✔
2527
                        width: Math.abs(deltaX) + 1,
6✔
2528
                        height: Math.abs(deltaY) + 1,
6✔
2529
                    };
6✔
2530

6✔
2531
                    setCurrent(
6✔
2532
                        {
6✔
2533
                            ...gridSelection.current,
6✔
2534
                            range: newRange,
6✔
2535
                        },
6✔
2536
                        true,
6✔
2537
                        false,
6✔
2538
                        "drag"
6✔
2539
                    );
6✔
2540
                }
6✔
2541
            }
11✔
2542

32✔
2543
            onItemHovered?.({ ...args, location: [args.location[0] - rowMarkerOffset, args.location[1]] as any });
33✔
2544
        },
33✔
2545
        [
678✔
2546
            allowedFillDirections,
678✔
2547
            mouseState,
678✔
2548
            rowMarkerOffset,
678✔
2549
            rowSelect,
678✔
2550
            gridSelection,
678✔
2551
            rangeSelect,
678✔
2552
            onItemHovered,
678✔
2553
            setSelectedRows,
678✔
2554
            lastRowSticky,
678✔
2555
            rows,
678✔
2556
            setCurrent,
678✔
2557
        ]
678✔
2558
    );
678✔
2559

678✔
2560
    const adjustSelectionOnScroll = React.useCallback(() => {
678✔
NEW
2561
        const args = hoveredRef.current;
×
NEW
2562
        if (args === undefined) return;
×
NEW
2563
        const [xDir, yDir] = args.scrollEdge;
×
NEW
2564
        let [col, row] = args.location;
×
NEW
2565
        const visible = visibleRegionRef.current;
×
NEW
2566
        if (xDir === -1) {
×
NEW
2567
            col = visible.x;
×
NEW
2568
        } else if (xDir === 1) {
×
NEW
2569
            col = visible.x + visible.width;
×
NEW
2570
        }
×
NEW
2571
        if (yDir === -1) {
×
NEW
2572
            row = Math.max(0, visible.y);
×
NEW
2573
        } else if (yDir === 1) {
×
NEW
2574
            row = Math.min(rows, visible.y + visible.height);
×
NEW
2575
        }
×
NEW
2576
        onItemHoveredImpl({
×
NEW
2577
            ...args,
×
NEW
2578
            location: [col, row] as any,
×
NEW
2579
        });
×
2580
    }, [onItemHoveredImpl, rows]);
678✔
2581

678✔
2582
    useAutoscroll(scrollDir, scrollRef, adjustSelectionOnScroll);
678✔
2583

678✔
2584
    // 1 === move one
678✔
2585
    // 2 === move to end
678✔
2586
    const adjustSelection = React.useCallback(
678✔
2587
        (direction: [0 | 1 | -1 | 2 | -2, 0 | 1 | -1 | 2 | -2]) => {
678✔
2588
            if (gridSelection.current === undefined) return;
8!
2589

8✔
2590
            const [x, y] = direction;
8✔
2591
            const [col, row] = gridSelection.current.cell;
8✔
2592
            const old = gridSelection.current.range;
8✔
2593
            let left = old.x;
8✔
2594
            let right = old.x + old.width;
8✔
2595
            let top = old.y;
8✔
2596
            let bottom = old.y + old.height;
8✔
2597

8✔
2598
            // take care of vertical first in case new spans come in
8✔
2599
            if (y !== 0) {
8✔
2600
                switch (y) {
2✔
2601
                    case 2: {
2✔
2602
                        // go to end
1✔
2603
                        bottom = rows;
1✔
2604
                        top = row;
1✔
2605
                        scrollTo(0, bottom, "vertical");
1✔
2606

1✔
2607
                        break;
1✔
2608
                    }
1✔
2609
                    case -2: {
2!
UNCOV
2610
                        // go to start
×
2611
                        top = 0;
×
2612
                        bottom = row + 1;
×
2613
                        scrollTo(0, top, "vertical");
×
UNCOV
2614

×
2615
                        break;
×
UNCOV
2616
                    }
×
2617
                    case 1: {
2✔
2618
                        // motion down
1✔
2619
                        if (top < row) {
1!
2620
                            top++;
×
2621
                            scrollTo(0, top, "vertical");
×
2622
                        } else {
1✔
2623
                            bottom = Math.min(rows, bottom + 1);
1✔
2624
                            scrollTo(0, bottom, "vertical");
1✔
2625
                        }
1✔
2626

1✔
2627
                        break;
1✔
2628
                    }
1✔
2629
                    case -1: {
2!
UNCOV
2630
                        // motion up
×
UNCOV
2631
                        if (bottom > row + 1) {
×
2632
                            bottom--;
×
2633
                            scrollTo(0, bottom, "vertical");
×
UNCOV
2634
                        } else {
×
2635
                            top = Math.max(0, top - 1);
×
2636
                            scrollTo(0, top, "vertical");
×
UNCOV
2637
                        }
×
UNCOV
2638

×
2639
                        break;
×
UNCOV
2640
                    }
×
2641
                    default: {
2!
2642
                        assertNever(y);
×
UNCOV
2643
                    }
×
2644
                }
2✔
2645
            }
2✔
2646

8✔
2647
            if (x !== 0) {
8✔
2648
                if (x === 2) {
6✔
2649
                    right = mangledCols.length;
1✔
2650
                    left = col;
1✔
2651
                    scrollTo(right - 1 - rowMarkerOffset, 0, "horizontal");
1✔
2652
                } else if (x === -2) {
6!
2653
                    left = rowMarkerOffset;
×
2654
                    right = col + 1;
×
2655
                    scrollTo(left - rowMarkerOffset, 0, "horizontal");
×
2656
                } else {
5✔
2657
                    let disallowed: number[] = [];
5✔
2658
                    if (getCellsForSelection !== undefined) {
5✔
2659
                        const cells = getCellsForSelection(
5✔
2660
                            {
5✔
2661
                                x: left,
5✔
2662
                                y: top,
5✔
2663
                                width: right - left - rowMarkerOffset,
5✔
2664
                                height: bottom - top,
5✔
2665
                            },
5✔
2666
                            abortControllerRef.current.signal
5✔
2667
                        );
5✔
2668

5✔
2669
                        if (typeof cells === "object") {
5✔
2670
                            disallowed = getSpanStops(cells);
5✔
2671
                        }
5✔
2672
                    }
5✔
2673
                    if (x === 1) {
5✔
2674
                        // motion right
4✔
2675
                        let done = false;
4✔
2676
                        if (left < col) {
4!
UNCOV
2677
                            if (disallowed.length > 0) {
×
2678
                                const target = range(left + 1, col + 1).find(
×
2679
                                    n => !disallowed.includes(n - rowMarkerOffset)
×
UNCOV
2680
                                );
×
UNCOV
2681
                                if (target !== undefined) {
×
2682
                                    left = target;
×
2683
                                    done = true;
×
UNCOV
2684
                                }
×
UNCOV
2685
                            } else {
×
2686
                                left++;
×
2687
                                done = true;
×
UNCOV
2688
                            }
×
2689
                            if (done) scrollTo(left, 0, "horizontal");
×
UNCOV
2690
                        }
×
2691
                        if (!done) {
4✔
2692
                            right = Math.min(mangledCols.length, right + 1);
4✔
2693
                            scrollTo(right - 1 - rowMarkerOffset, 0, "horizontal");
4✔
2694
                        }
4✔
2695
                    } else if (x === -1) {
5✔
2696
                        // motion left
1✔
2697
                        let done = false;
1✔
2698
                        if (right > col + 1) {
1!
UNCOV
2699
                            if (disallowed.length > 0) {
×
2700
                                const target = range(right - 1, col, -1).find(
×
2701
                                    n => !disallowed.includes(n - rowMarkerOffset)
×
UNCOV
2702
                                );
×
UNCOV
2703
                                if (target !== undefined) {
×
2704
                                    right = target;
×
2705
                                    done = true;
×
UNCOV
2706
                                }
×
UNCOV
2707
                            } else {
×
2708
                                right--;
×
2709
                                done = true;
×
UNCOV
2710
                            }
×
2711
                            if (done) scrollTo(right - rowMarkerOffset, 0, "horizontal");
×
UNCOV
2712
                        }
×
2713
                        if (!done) {
1✔
2714
                            left = Math.max(rowMarkerOffset, left - 1);
1✔
2715
                            scrollTo(left - rowMarkerOffset, 0, "horizontal");
1✔
2716
                        }
1✔
2717
                    } else {
1!
2718
                        assertNever(x);
×
UNCOV
2719
                    }
×
2720
                }
5✔
2721
            }
6✔
2722

8✔
2723
            setCurrent(
8✔
2724
                {
8✔
2725
                    cell: gridSelection.current.cell,
8✔
2726
                    range: {
8✔
2727
                        x: left,
8✔
2728
                        y: top,
8✔
2729
                        width: right - left,
8✔
2730
                        height: bottom - top,
8✔
2731
                    },
8✔
2732
                },
8✔
2733
                true,
8✔
2734
                false,
8✔
2735
                "keyboard-select"
8✔
2736
            );
8✔
2737
        },
8✔
2738
        [getCellsForSelection, gridSelection, mangledCols.length, rowMarkerOffset, rows, scrollTo, setCurrent]
678✔
2739
    );
678✔
2740

678✔
2741
    const updateSelectedCell = React.useCallback(
678✔
2742
        (col: number, row: number, fromEditingTrailingRow: boolean, freeMove: boolean): boolean => {
678✔
2743
            const rowMax = mangledRows - (fromEditingTrailingRow ? 0 : 1);
53!
2744
            col = clamp(col, rowMarkerOffset, columns.length - 1 + rowMarkerOffset);
53✔
2745
            row = clamp(row, 0, rowMax);
53✔
2746

53✔
2747
            if (col === currentCell?.[0] && row === currentCell?.[1]) return false;
53✔
2748
            if (freeMove && gridSelection.current !== undefined) {
53✔
2749
                const newStack = [...gridSelection.current.rangeStack];
1✔
2750
                if (gridSelection.current.range.width > 1 || gridSelection.current.range.height > 1) {
1!
2751
                    newStack.push(gridSelection.current.range);
1✔
2752
                }
1✔
2753
                setGridSelection(
1✔
2754
                    {
1✔
2755
                        ...gridSelection,
1✔
2756
                        current: {
1✔
2757
                            cell: [col, row],
1✔
2758
                            range: { x: col, y: row, width: 1, height: 1 },
1✔
2759
                            rangeStack: newStack,
1✔
2760
                        },
1✔
2761
                    },
1✔
2762
                    true
1✔
2763
                );
1✔
2764
            } else {
53✔
2765
                setCurrent(
25✔
2766
                    {
25✔
2767
                        cell: [col, row],
25✔
2768
                        range: { x: col, y: row, width: 1, height: 1 },
25✔
2769
                    },
25✔
2770
                    true,
25✔
2771
                    false,
25✔
2772
                    "keyboard-nav"
25✔
2773
                );
25✔
2774
            }
25✔
2775

26✔
2776
            if (lastSent.current !== undefined && lastSent.current[0] === col && lastSent.current[1] === row) {
53✔
2777
                lastSent.current = undefined;
2✔
2778
            }
2✔
2779

26✔
2780
            scrollTo(col - rowMarkerOffset, row);
26✔
2781

26✔
2782
            return true;
26✔
2783
        },
53✔
2784
        [
678✔
2785
            mangledRows,
678✔
2786
            rowMarkerOffset,
678✔
2787
            columns.length,
678✔
2788
            currentCell,
678✔
2789
            gridSelection,
678✔
2790
            scrollTo,
678✔
2791
            setGridSelection,
678✔
2792
            setCurrent,
678✔
2793
        ]
678✔
2794
    );
678✔
2795

678✔
2796
    const onFinishEditing = React.useCallback(
678✔
2797
        (newValue: GridCell | undefined, movement: readonly [-1 | 0 | 1, -1 | 0 | 1]) => {
678✔
2798
            if (overlay?.cell !== undefined && newValue !== undefined && isEditableGridCell(newValue)) {
7✔
2799
                mangledOnCellsEdited([{ location: overlay.cell, value: newValue }]);
4✔
2800
                window.requestAnimationFrame(() => {
4✔
2801
                    gridRef.current?.damage([
4✔
2802
                        {
4✔
2803
                            cell: overlay.cell,
4✔
2804
                        },
4✔
2805
                    ]);
4✔
2806
                });
4✔
2807
            }
4✔
2808
            focus(true);
7✔
2809
            setOverlay(undefined);
7✔
2810

7✔
2811
            const [movX, movY] = movement;
7✔
2812
            if (gridSelection.current !== undefined && (movX !== 0 || movY !== 0)) {
7✔
2813
                const isEditingTrailingRow =
3✔
2814
                    gridSelection.current.cell[1] === mangledRows - 1 && newValue !== undefined;
3!
2815
                updateSelectedCell(
3✔
2816
                    clamp(gridSelection.current.cell[0] + movX, 0, mangledCols.length - 1),
3✔
2817
                    clamp(gridSelection.current.cell[1] + movY, 0, mangledRows - 1),
3✔
2818
                    isEditingTrailingRow,
3✔
2819
                    false
3✔
2820
                );
3✔
2821
            }
3✔
2822
            onFinishedEditing?.(newValue, movement);
7✔
2823
        },
7✔
2824
        [
678✔
2825
            overlay?.cell,
678✔
2826
            focus,
678✔
2827
            gridSelection,
678✔
2828
            onFinishedEditing,
678✔
2829
            mangledOnCellsEdited,
678✔
2830
            mangledRows,
678✔
2831
            updateSelectedCell,
678✔
2832
            mangledCols.length,
678✔
2833
        ]
678✔
2834
    );
678✔
2835

678✔
2836
    const overlayID = React.useMemo(() => {
678✔
2837
        return `gdg-overlay-${idCounter++}`;
137✔
2838
    }, []);
678✔
2839

678✔
2840
    const deleteRange = React.useCallback(
678✔
2841
        (r: Rectangle) => {
678✔
2842
            focus();
8✔
2843
            const editList: EditListItem[] = [];
8✔
2844
            for (let x = r.x; x < r.x + r.width; x++) {
8✔
2845
                for (let y = r.y; y < r.y + r.height; y++) {
23✔
2846
                    const cellValue = getCellContent([x - rowMarkerOffset, y]);
1,066✔
2847
                    if (!cellValue.allowOverlay && cellValue.kind !== GridCellKind.Boolean) continue;
1,066✔
2848
                    let newVal: InnerGridCell | undefined = undefined;
1,042✔
2849
                    if (cellValue.kind === GridCellKind.Custom) {
1,066✔
2850
                        const toDelete = getCellRenderer(cellValue);
1✔
2851
                        const editor = toDelete?.provideEditor?.(cellValue);
1!
2852
                        if (toDelete?.onDelete !== undefined) {
1✔
2853
                            newVal = toDelete.onDelete(cellValue);
1✔
2854
                        } else if (isObjectEditorCallbackResult(editor)) {
1!
2855
                            newVal = editor?.deletedValue?.(cellValue);
×
UNCOV
2856
                        }
×
2857
                    } else if (
1✔
2858
                        (isEditableGridCell(cellValue) && cellValue.allowOverlay) ||
1,041✔
2859
                        cellValue.kind === GridCellKind.Boolean
1✔
2860
                    ) {
1,041✔
2861
                        const toDelete = getCellRenderer(cellValue);
1,041✔
2862
                        newVal = toDelete?.onDelete?.(cellValue);
1,041✔
2863
                    }
1,041✔
2864
                    if (newVal !== undefined && !isInnerOnlyCell(newVal) && isEditableGridCell(newVal)) {
1,066✔
2865
                        editList.push({ location: [x, y], value: newVal });
1,041✔
2866
                    }
1,041✔
2867
                }
1,066✔
2868
            }
23✔
2869
            mangledOnCellsEdited(editList);
8✔
2870
            gridRef.current?.damage(editList.map(x => ({ cell: x.location })));
8✔
2871
        },
8✔
2872
        [focus, getCellContent, getCellRenderer, mangledOnCellsEdited, rowMarkerOffset]
678✔
2873
    );
678✔
2874

678✔
2875
    const onKeyDown = React.useCallback(
678✔
2876
        (event: GridKeyEventArgs) => {
678✔
2877
            const fn = async () => {
61✔
2878
                let cancelled = false;
61✔
2879
                if (onKeyDownIn !== undefined) {
61✔
2880
                    onKeyDownIn({
1✔
2881
                        ...event,
1✔
2882
                        cancel: () => {
1✔
2883
                            cancelled = true;
×
UNCOV
2884
                        },
×
2885
                    });
1✔
2886
                }
1✔
2887

61✔
2888
                if (cancelled) return;
61!
2889

61✔
2890
                const cancel = () => {
61✔
2891
                    event.stopPropagation();
42✔
2892
                    event.preventDefault();
42✔
2893
                };
42✔
2894

61✔
2895
                const overlayOpen = overlay !== undefined;
61✔
2896
                const { altKey, shiftKey, metaKey, ctrlKey, key, bounds } = event;
61✔
2897
                const isOSX = browserIsOSX.value;
61✔
2898
                const isPrimaryKey = isOSX ? metaKey : ctrlKey;
61!
2899
                const isDeleteKey = key === "Delete" || (isOSX && key === "Backspace");
61!
2900
                const vr = visibleRegionRef.current;
61✔
2901
                const selectedColumns = gridSelection.columns;
61✔
2902
                const selectedRows = gridSelection.rows;
61✔
2903

61✔
2904
                if (key === "Escape") {
61✔
2905
                    if (overlayOpen) {
4✔
2906
                        setOverlay(undefined);
2✔
2907
                    } else if (keybindings.clear) {
2✔
2908
                        setGridSelection(emptyGridSelection, false);
2✔
2909
                        onSelectionCleared?.();
2!
2910
                    }
2✔
2911
                    return;
4✔
2912
                } else if (isHotkey("primary+a", event) && keybindings.selectAll) {
61✔
2913
                    if (!overlayOpen) {
1✔
2914
                        setGridSelection(
1✔
2915
                            {
1✔
2916
                                columns: CompactSelection.empty(),
1✔
2917
                                rows: CompactSelection.empty(),
1✔
2918
                                current: {
1✔
2919
                                    cell: gridSelection.current?.cell ?? [rowMarkerOffset, 0],
1!
2920
                                    range: {
1✔
2921
                                        x: rowMarkerOffset,
1✔
2922
                                        y: 0,
1✔
2923
                                        width: columnsIn.length,
1✔
2924
                                        height: rows,
1✔
2925
                                    },
1✔
2926
                                    rangeStack: [],
1✔
2927
                                },
1✔
2928
                            },
1✔
2929
                            false
1✔
2930
                        );
1✔
2931
                    } else {
1!
2932
                        const el = document.getElementById(overlayID);
×
UNCOV
2933
                        if (el !== null) {
×
2934
                            const s = window.getSelection();
×
2935
                            const r = document.createRange();
×
2936
                            r.selectNodeContents(el);
×
2937
                            s?.removeAllRanges();
×
2938
                            s?.addRange(r);
×
UNCOV
2939
                        }
×
UNCOV
2940
                    }
×
2941
                    cancel();
1✔
2942
                    return;
1✔
2943
                } else if (isHotkey("primary+f", event) && keybindings.search) {
57!
2944
                    cancel();
×
2945
                    searchInputRef?.current?.focus({ preventScroll: true });
×
2946
                    setShowSearchInner(true);
×
UNCOV
2947
                }
✔
2948

56✔
2949
                if (isDeleteKey) {
61✔
2950
                    const callbackResult = onDelete?.(gridSelection) ?? true;
8✔
2951
                    cancel();
8✔
2952
                    if (callbackResult !== false) {
8✔
2953
                        const toDelete = callbackResult === true ? gridSelection : callbackResult;
8✔
2954

8✔
2955
                        // delete order:
8✔
2956
                        // 1) primary range
8✔
2957
                        // 2) secondary ranges
8✔
2958
                        // 3) columns
8✔
2959
                        // 4) rows
8✔
2960

8✔
2961
                        if (toDelete.current !== undefined) {
8✔
2962
                            deleteRange(toDelete.current.range);
5✔
2963
                            for (const r of toDelete.current.rangeStack) {
5!
2964
                                deleteRange(r);
×
UNCOV
2965
                            }
×
2966
                        }
5✔
2967

8✔
2968
                        for (const r of toDelete.rows) {
8✔
2969
                            deleteRange({
1✔
2970
                                x: rowMarkerOffset,
1✔
2971
                                y: r,
1✔
2972
                                width: mangledCols.length - rowMarkerOffset,
1✔
2973
                                height: 1,
1✔
2974
                            });
1✔
2975
                        }
1✔
2976

8✔
2977
                        for (const col of toDelete.columns) {
8✔
2978
                            deleteRange({
1✔
2979
                                x: col,
1✔
2980
                                y: 0,
1✔
2981
                                width: 1,
1✔
2982
                                height: rows,
1✔
2983
                            });
1✔
2984
                        }
1✔
2985
                    }
8✔
2986
                    return;
8✔
2987
                }
8✔
2988

48✔
2989
                if (gridSelection.current === undefined) return;
48✔
2990
                let [col, row] = gridSelection.current.cell;
45✔
2991
                let freeMove = false;
45✔
2992

45✔
2993
                if (keybindings.selectColumn && isHotkey("ctrl+ ", event) && columnSelect !== "none") {
61✔
2994
                    if (selectedColumns.hasIndex(col)) {
2!
2995
                        setSelectedColumns(selectedColumns.remove(col), undefined, true);
×
2996
                    } else {
2✔
2997
                        if (columnSelect === "single") {
2!
2998
                            setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, true);
×
2999
                        } else {
2✔
3000
                            setSelectedColumns(undefined, col, true);
2✔
3001
                        }
2✔
3002
                    }
2✔
3003
                } else if (keybindings.selectRow && isHotkey("shift+ ", event) && rowSelect !== "none") {
61✔
3004
                    if (selectedRows.hasIndex(row)) {
2!
3005
                        setSelectedRows(selectedRows.remove(row), undefined, true);
×
3006
                    } else {
2✔
3007
                        if (rowSelect === "single") {
2!
3008
                            setSelectedRows(CompactSelection.fromSingleSelection(row), undefined, true);
×
3009
                        } else {
2✔
3010
                            setSelectedRows(undefined, row, true);
2✔
3011
                        }
2✔
3012
                    }
2✔
3013
                } else if (
2✔
3014
                    (isHotkey("Enter", event) || isHotkey(" ", event) || isHotkey("shift+Enter", event)) &&
41✔
3015
                    bounds !== undefined
8✔
3016
                ) {
41✔
3017
                    if (overlayOpen) {
8✔
3018
                        setOverlay(undefined);
2✔
3019
                        if (isHotkey("Enter", event)) {
2✔
3020
                            row++;
2✔
3021
                        } else if (isHotkey("shift+Enter", event)) {
2!
3022
                            row--;
×
UNCOV
3023
                        }
×
3024
                    } else if (row === rows && showTrailingBlankRow) {
8!
3025
                        window.setTimeout(() => {
×
3026
                            const customTargetColumn = getCustomNewRowTargetColumn(col);
×
3027
                            void appendRow(customTargetColumn ?? col);
×
UNCOV
3028
                        }, 0);
×
3029
                    } else {
6✔
3030
                        onCellActivated?.([col - rowMarkerOffset, row]);
6✔
3031
                        reselect(bounds, true);
6✔
3032
                        cancel();
6✔
3033
                    }
6✔
3034
                } else if (
8✔
3035
                    keybindings.downFill &&
33✔
3036
                    isHotkey("primary+_68", event) &&
1✔
3037
                    gridSelection.current.range.height > 1
1✔
3038
                ) {
33✔
3039
                    // ctrl/cmd + d
1✔
3040
                    fillDown(false);
1✔
3041
                    cancel();
1✔
3042
                } else if (
1✔
3043
                    keybindings.rightFill &&
32✔
3044
                    isHotkey("primary+_82", event) &&
1✔
3045
                    gridSelection.current.range.width > 1
1✔
3046
                ) {
32✔
3047
                    // ctrl/cmd + r
1✔
3048
                    const editList: EditListItem[] = [];
1✔
3049
                    const r = gridSelection.current.range;
1✔
3050
                    for (let y = 0; y < r.height; y++) {
1✔
3051
                        const fillRow = y + r.y;
5✔
3052
                        const fillVal = getMangledCellContent([r.x, fillRow]);
5✔
3053
                        if (isInnerOnlyCell(fillVal) || !isReadWriteCell(fillVal)) continue;
5!
3054
                        for (let x = 1; x < r.width; x++) {
5✔
3055
                            const fillCol = x + r.x;
5✔
3056
                            const target = [fillCol, fillRow] as const;
5✔
3057
                            editList.push({
5✔
3058
                                location: target,
5✔
3059
                                value: { ...fillVal },
5✔
3060
                            });
5✔
3061
                        }
5✔
3062
                    }
5✔
3063
                    mangledOnCellsEdited(editList);
1✔
3064
                    gridRef.current?.damage(
1✔
3065
                        editList.map(c => ({
1✔
3066
                            cell: c.location,
5✔
3067
                        }))
1✔
3068
                    );
1✔
3069
                    cancel();
1✔
3070
                } else if (keybindings.pageDown && isHotkey("PageDown", event)) {
32!
3071
                    row += Math.max(1, visibleRegionRef.current.height - 4); // partial cell accounting
×
3072
                    cancel();
×
3073
                } else if (keybindings.pageUp && isHotkey("PageUp", event)) {
31!
3074
                    row -= Math.max(1, visibleRegionRef.current.height - 4); // partial cell accounting
×
3075
                    cancel();
×
3076
                } else if (keybindings.first && isHotkey("primary+Home", event)) {
31!
3077
                    setOverlay(undefined);
×
3078
                    row = 0;
×
3079
                    col = 0;
×
3080
                } else if (keybindings.last && isHotkey("primary+End", event)) {
31!
3081
                    setOverlay(undefined);
×
3082
                    row = Number.MAX_SAFE_INTEGER;
×
3083
                    col = Number.MAX_SAFE_INTEGER;
×
3084
                } else if (keybindings.first && isHotkey("primary+shift+Home", event)) {
31!
3085
                    setOverlay(undefined);
×
3086
                    adjustSelection([-2, -2]);
×
3087
                } else if (keybindings.last && isHotkey("primary+shift+End", event)) {
31!
3088
                    setOverlay(undefined);
×
3089
                    adjustSelection([2, 2]);
×
UNCOV
3090
                    // eslint-disable-next-line unicorn/prefer-switch
×
3091
                } else if (key === "ArrowDown") {
31✔
3092
                    if (ctrlKey && altKey) {
8!
3093
                        return;
×
UNCOV
3094
                    }
×
3095
                    setOverlay(undefined);
8✔
3096
                    if (shiftKey && (rangeSelect === "rect" || rangeSelect === "multi-rect")) {
8!
3097
                        // ctrl + alt is used as a screen reader command, let's not nuke it.
2✔
3098
                        adjustSelection([0, isPrimaryKey && !altKey ? 2 : 1]);
2✔
3099
                    } else {
8✔
3100
                        if (altKey && !isPrimaryKey) {
6!
3101
                            freeMove = true;
×
UNCOV
3102
                        }
×
3103
                        if (isPrimaryKey && !altKey) {
6✔
3104
                            row = rows - 1;
1✔
3105
                        } else {
6✔
3106
                            row += 1;
5✔
3107
                        }
5✔
3108
                    }
6✔
3109
                } else if (key === "ArrowUp" || key === "Home") {
31✔
3110
                    const asPrimary = key === "Home" || isPrimaryKey;
2✔
3111
                    setOverlay(undefined);
2✔
3112
                    if (shiftKey && (rangeSelect === "rect" || rangeSelect === "multi-rect")) {
2!
UNCOV
3113
                        // ctrl + alt is used as a screen reader command, let's not nuke it.
×
3114
                        adjustSelection([0, asPrimary && !altKey ? -2 : -1]);
×
3115
                    } else {
2✔
3116
                        if (altKey && !asPrimary) {
2!
3117
                            freeMove = true;
×
UNCOV
3118
                        }
×
3119
                        row += asPrimary && !altKey ? Number.MIN_SAFE_INTEGER : -1;
2✔
3120
                    }
2✔
3121
                } else if (key === "ArrowRight" || key === "End") {
23✔
3122
                    const asPrimary = key === "End" || isPrimaryKey;
8✔
3123
                    setOverlay(undefined);
8✔
3124
                    if (shiftKey && (rangeSelect === "rect" || rangeSelect === "multi-rect")) {
8!
3125
                        // ctrl + alt is used as a screen reader command, let's not nuke it.
5✔
3126
                        adjustSelection([asPrimary && !altKey ? 2 : 1, 0]);
5✔
3127
                    } else {
8✔
3128
                        if (altKey && !asPrimary) {
3!
3129
                            freeMove = true;
×
UNCOV
3130
                        }
×
3131
                        col += asPrimary && !altKey ? Number.MAX_SAFE_INTEGER : 1;
3✔
3132
                    }
3✔
3133
                } else if (key === "ArrowLeft") {
21✔
3134
                    setOverlay(undefined);
4✔
3135
                    if (shiftKey && (rangeSelect === "rect" || rangeSelect === "multi-rect")) {
4!
3136
                        // ctrl + alt is used as a screen reader command, let's not nuke it.
1✔
3137
                        adjustSelection([isPrimaryKey && !altKey ? -2 : -1, 0]);
1!
3138
                    } else {
4✔
3139
                        if (altKey && !isPrimaryKey) {
3✔
3140
                            freeMove = true;
1✔
3141
                        }
1✔
3142
                        col += isPrimaryKey && !altKey ? Number.MIN_SAFE_INTEGER : -1;
3✔
3143
                    }
3✔
3144
                } else if (key === "Tab") {
13✔
3145
                    setOverlay(undefined);
2✔
3146
                    if (shiftKey) {
2✔
3147
                        col--;
1✔
3148
                    } else {
1✔
3149
                        col++;
1✔
3150
                    }
1✔
3151
                } else if (
2✔
3152
                    !metaKey &&
7✔
3153
                    !ctrlKey &&
7✔
3154
                    gridSelection.current !== undefined &&
7✔
3155
                    key.length === 1 &&
7✔
3156
                    /[ -~]/g.test(key) &&
7✔
3157
                    bounds !== undefined &&
7✔
3158
                    isReadWriteCell(getCellContent([col - rowMarkerOffset, Math.max(0, Math.min(row, rows - 1))]))
7✔
3159
                ) {
7✔
3160
                    if (
7✔
3161
                        (!lastRowSticky || row !== rows) &&
7✔
3162
                        (vr.y > row || row > vr.y + vr.height || vr.x > col || col > vr.x + vr.width)
7✔
3163
                    ) {
7!
3164
                        return;
×
UNCOV
3165
                    }
×
3166
                    reselect(bounds, true, key);
7✔
3167
                    cancel();
7✔
3168
                }
7✔
3169

45✔
3170
                const moved = updateSelectedCell(col, row, false, freeMove);
45✔
3171
                if (moved) {
61✔
3172
                    cancel();
18✔
3173
                }
18✔
3174
            };
61✔
3175
            void fn();
61✔
3176
        },
61✔
3177
        [
678✔
3178
            onKeyDownIn,
678✔
3179
            deleteRange,
678✔
3180
            overlay,
678✔
3181
            gridSelection,
678✔
3182
            keybindings.selectAll,
678✔
3183
            keybindings.search,
678✔
3184
            keybindings.selectColumn,
678✔
3185
            keybindings.selectRow,
678✔
3186
            keybindings.downFill,
678✔
3187
            keybindings.rightFill,
678✔
3188
            keybindings.pageDown,
678✔
3189
            keybindings.pageUp,
678✔
3190
            keybindings.first,
678✔
3191
            keybindings.last,
678✔
3192
            keybindings.clear,
678✔
3193
            columnSelect,
678✔
3194
            rowSelect,
678✔
3195
            getCellContent,
678✔
3196
            rowMarkerOffset,
678✔
3197
            updateSelectedCell,
678✔
3198
            setGridSelection,
678✔
3199
            onSelectionCleared,
678✔
3200
            columnsIn.length,
678✔
3201
            rows,
678✔
3202
            overlayID,
678✔
3203
            mangledOnCellsEdited,
678✔
3204
            onDelete,
678✔
3205
            mangledCols.length,
678✔
3206
            setSelectedColumns,
678✔
3207
            setSelectedRows,
678✔
3208
            showTrailingBlankRow,
678✔
3209
            getCustomNewRowTargetColumn,
678✔
3210
            appendRow,
678✔
3211
            onCellActivated,
678✔
3212
            reselect,
678✔
3213
            fillDown,
678✔
3214
            getMangledCellContent,
678✔
3215
            adjustSelection,
678✔
3216
            rangeSelect,
678✔
3217
            lastRowSticky,
678✔
3218
        ]
678✔
3219
    );
678✔
3220

678✔
3221
    const onContextMenu = React.useCallback(
678✔
3222
        (args: GridMouseEventArgs, preventDefault: () => void) => {
678✔
3223
            const adjustedCol = args.location[0] - rowMarkerOffset;
7✔
3224
            if (args.kind === "header") {
7!
3225
                onHeaderContextMenu?.(adjustedCol, { ...args, preventDefault });
×
UNCOV
3226
            }
×
3227

7✔
3228
            if (args.kind === groupHeaderKind) {
7!
UNCOV
3229
                if (adjustedCol < 0) {
×
3230
                    return;
×
UNCOV
3231
                }
×
3232
                onGroupHeaderContextMenu?.(adjustedCol, { ...args, preventDefault });
×
UNCOV
3233
            }
×
3234

7✔
3235
            if (args.kind === "cell") {
7✔
3236
                const [col, row] = args.location;
7✔
3237
                onCellContextMenu?.([adjustedCol, row], {
7✔
3238
                    ...args,
7✔
3239
                    preventDefault,
7✔
3240
                });
7✔
3241

7✔
3242
                if (!gridSelectionHasItem(gridSelection, args.location)) {
7✔
3243
                    updateSelectedCell(col, row, false, false);
3✔
3244
                }
3✔
3245
            }
7✔
3246
        },
7✔
3247
        [
678✔
3248
            gridSelection,
678✔
3249
            onCellContextMenu,
678✔
3250
            onGroupHeaderContextMenu,
678✔
3251
            onHeaderContextMenu,
678✔
3252
            rowMarkerOffset,
678✔
3253
            updateSelectedCell,
678✔
3254
        ]
678✔
3255
    );
678✔
3256

678✔
3257
    const onPasteInternal = React.useCallback(
678✔
3258
        async (e?: ClipboardEvent) => {
678✔
3259
            if (!keybindings.paste) return;
6!
3260
            function pasteToCell(
6✔
3261
                inner: InnerGridCell,
51✔
3262
                target: Item,
51✔
3263
                rawValue: string | boolean | string[] | number | boolean | BooleanEmpty | BooleanIndeterminate,
51✔
3264
                formatted?: string | string[]
51✔
3265
            ): EditListItem | undefined {
51✔
3266
                const stringifiedRawValue =
51✔
3267
                    typeof rawValue === "object" ? rawValue?.join("\n") ?? "" : rawValue?.toString() ?? "";
51!
3268

51✔
3269
                if (!isInnerOnlyCell(inner) && isReadWriteCell(inner) && inner.readonly !== true) {
51✔
3270
                    const coerced = coercePasteValue?.(stringifiedRawValue, inner);
51!
3271
                    if (coerced !== undefined && isEditableGridCell(coerced)) {
51!
UNCOV
3272
                        if (process.env.NODE_ENV !== "production" && coerced.kind !== inner.kind) {
×
UNCOV
3273
                            // eslint-disable-next-line no-console
×
3274
                            console.warn("Coercion should not change cell kind.");
×
UNCOV
3275
                        }
×
3276
                        return {
×
UNCOV
3277
                            location: target,
×
UNCOV
3278
                            value: coerced,
×
UNCOV
3279
                        };
×
UNCOV
3280
                    }
×
3281
                    const r = getCellRenderer(inner);
51✔
3282
                    if (r === undefined) return undefined;
51!
3283
                    if (r.kind === GridCellKind.Custom) {
51✔
3284
                        assert(inner.kind === GridCellKind.Custom);
1✔
3285
                        const newVal = (r as unknown as CustomRenderer<CustomCell<any>>).onPaste?.(
1✔
3286
                            stringifiedRawValue,
1✔
3287
                            inner.data
1✔
3288
                        );
1✔
3289
                        if (newVal === undefined) return undefined;
1!
3290
                        return {
×
UNCOV
3291
                            location: target,
×
UNCOV
3292
                            value: {
×
UNCOV
3293
                                ...inner,
×
UNCOV
3294
                                data: newVal,
×
UNCOV
3295
                            },
×
UNCOV
3296
                        };
×
3297
                    } else {
51✔
3298
                        const newVal = r.onPaste?.(stringifiedRawValue, inner, {
50✔
3299
                            formatted,
50✔
3300
                            formattedString: typeof formatted === "string" ? formatted : formatted?.join("\n"),
50!
3301
                            rawValue,
50✔
3302
                        });
50✔
3303
                        if (newVal === undefined) return undefined;
50✔
3304
                        assert(newVal.kind === inner.kind);
36✔
3305
                        return {
36✔
3306
                            location: target,
36✔
3307
                            value: newVal,
36✔
3308
                        };
36✔
3309
                    }
36✔
3310
                }
51!
3311
                return undefined;
×
3312
            }
51✔
3313

6✔
3314
            const selectedColumns = gridSelection.columns;
6✔
3315
            const selectedRows = gridSelection.rows;
6✔
3316
            const focused =
6✔
3317
                scrollRef.current?.contains(document.activeElement) === true ||
6✔
3318
                canvasRef.current?.contains(document.activeElement) === true;
6✔
3319

6✔
3320
            let target: Item | undefined;
6✔
3321

6✔
3322
            if (gridSelection.current !== undefined) {
6✔
3323
                target = [gridSelection.current.range.x, gridSelection.current.range.y];
5✔
3324
            } else if (selectedColumns.length === 1) {
6!
UNCOV
3325
                target = [selectedColumns.first() ?? 0, 0];
×
3326
            } else if (selectedRows.length === 1) {
1!
UNCOV
3327
                target = [rowMarkerOffset, selectedRows.first() ?? 0];
×
UNCOV
3328
            }
×
3329

6✔
3330
            if (focused && target !== undefined) {
6✔
3331
                let data: CopyBuffer | undefined;
5✔
3332
                let text: string | undefined;
5✔
3333

5✔
3334
                const textPlain = "text/plain";
5✔
3335
                const textHtml = "text/html";
5✔
3336

5✔
3337
                if (navigator.clipboard.read !== undefined) {
5!
3338
                    const clipboardContent = await navigator.clipboard.read();
×
UNCOV
3339

×
UNCOV
3340
                    for (const item of clipboardContent) {
×
UNCOV
3341
                        if (item.types.includes(textHtml)) {
×
3342
                            const htmlBlob = await item.getType(textHtml);
×
3343
                            const html = await htmlBlob.text();
×
3344
                            const decoded = decodeHTML(html);
×
UNCOV
3345
                            if (decoded !== undefined) {
×
3346
                                data = decoded;
×
3347
                                break;
×
UNCOV
3348
                            }
×
UNCOV
3349
                        }
×
UNCOV
3350
                        if (item.types.includes(textPlain)) {
×
UNCOV
3351
                            // eslint-disable-next-line unicorn/no-await-expression-member
×
3352
                            text = await (await item.getType(textPlain)).text();
×
UNCOV
3353
                        }
×
UNCOV
3354
                    }
×
3355
                } else if (navigator.clipboard.readText !== undefined) {
5✔
3356
                    text = await navigator.clipboard.readText();
5✔
3357
                } else if (e !== undefined && e?.clipboardData !== null) {
5!
UNCOV
3358
                    if (e.clipboardData.types.includes(textHtml)) {
×
3359
                        const html = e.clipboardData.getData(textHtml);
×
3360
                        data = decodeHTML(html);
×
UNCOV
3361
                    }
×
UNCOV
3362
                    if (data === undefined && e.clipboardData.types.includes(textPlain)) {
×
3363
                        text = e.clipboardData.getData(textPlain);
×
UNCOV
3364
                    }
×
UNCOV
3365
                } else {
×
3366
                    return; // I didn't want to read that paste value anyway
×
UNCOV
3367
                }
×
3368

5✔
3369
                const [targetCol, targetRow] = target;
5✔
3370

5✔
3371
                const editList: EditListItem[] = [];
5✔
3372
                do {
5✔
3373
                    if (onPaste === undefined) {
5✔
3374
                        const cellData = getMangledCellContent(target);
2✔
3375
                        const rawValue = text ?? data?.map(r => r.map(cb => cb.rawValue).join("\t")).join("\t") ?? "";
2!
3376
                        const newVal = pasteToCell(cellData, target, rawValue, undefined);
2✔
3377
                        if (newVal !== undefined) {
2✔
3378
                            editList.push(newVal);
1✔
3379
                        }
1✔
3380
                        break;
2✔
3381
                    }
2✔
3382

3✔
3383
                    if (data === undefined) {
3✔
3384
                        if (text === undefined) return;
3!
3385
                        data = unquote(text);
3✔
3386
                    }
3✔
3387

3✔
3388
                    if (
3✔
3389
                        onPaste === false ||
3✔
3390
                        (typeof onPaste === "function" &&
3✔
3391
                            onPaste?.(
2✔
3392
                                [target[0] - rowMarkerOffset, target[1]],
2✔
3393
                                data.map(r => r.map(cb => cb.rawValue?.toString() ?? ""))
2!
3394
                            ) !== true)
2✔
3395
                    ) {
5!
3396
                        return;
×
UNCOV
3397
                    }
✔
3398

3✔
3399
                    for (const [row, dataRow] of data.entries()) {
5✔
3400
                        if (row + targetRow >= rows) break;
21!
3401
                        for (const [col, dataItem] of dataRow.entries()) {
21✔
3402
                            const index = [col + targetCol, row + targetRow] as const;
63✔
3403
                            const [writeCol, writeRow] = index;
63✔
3404
                            if (writeCol >= mangledCols.length) continue;
63✔
3405
                            if (writeRow >= mangledRows) continue;
49!
3406
                            const cellData = getMangledCellContent(index);
49✔
3407
                            const newVal = pasteToCell(cellData, index, dataItem.rawValue, dataItem.formatted);
49✔
3408
                            if (newVal !== undefined) {
63✔
3409
                                editList.push(newVal);
35✔
3410
                            }
35✔
3411
                        }
63✔
3412
                    }
21✔
3413
                    // eslint-disable-next-line no-constant-condition
3✔
3414
                } while (false);
5✔
3415

5✔
3416
                mangledOnCellsEdited(editList);
5✔
3417

5✔
3418
                gridRef.current?.damage(
5✔
3419
                    editList.map(c => ({
5✔
3420
                        cell: c.location,
36✔
3421
                    }))
5✔
3422
                );
5✔
3423
            }
5✔
3424
        },
6✔
3425
        [
678✔
3426
            coercePasteValue,
678✔
3427
            getCellRenderer,
678✔
3428
            getMangledCellContent,
678✔
3429
            gridSelection,
678✔
3430
            keybindings.paste,
678✔
3431
            mangledCols.length,
678✔
3432
            mangledOnCellsEdited,
678✔
3433
            mangledRows,
678✔
3434
            onPaste,
678✔
3435
            rowMarkerOffset,
678✔
3436
            rows,
678✔
3437
        ]
678✔
3438
    );
678✔
3439

678✔
3440
    useEventListener("paste", onPasteInternal, safeWindow, false, true);
678✔
3441

678✔
3442
    // While this function is async, we deeply prefer not to await if we don't have to. This will lead to unpacking
678✔
3443
    // promises in rather awkward ways when possible to avoid awaiting. We have to use fallback copy mechanisms when
678✔
3444
    // an await has happened.
678✔
3445
    const onCopy = React.useCallback(
678✔
3446
        async (e?: ClipboardEvent, ignoreFocus?: boolean) => {
678✔
3447
            if (!keybindings.copy) return;
6!
3448
            const focused =
6✔
3449
                ignoreFocus === true ||
6✔
3450
                scrollRef.current?.contains(document.activeElement) === true ||
5✔
3451
                canvasRef.current?.contains(document.activeElement) === true;
5✔
3452

6✔
3453
            const selectedColumns = gridSelection.columns;
6✔
3454
            const selectedRows = gridSelection.rows;
6✔
3455

6✔
3456
            const copyToClipboardWithHeaders = (
6✔
3457
                cells: readonly (readonly GridCell[])[],
5✔
3458
                columnIndexes: readonly number[]
5✔
3459
            ) => {
5✔
3460
                if (!copyHeaders) {
5✔
3461
                    copyToClipboard(cells, columnIndexes, e);
5✔
3462
                } else {
5!
3463
                    const headers = columnIndexes.map(index => ({
×
UNCOV
3464
                        kind: GridCellKind.Text,
×
UNCOV
3465
                        data: columnsIn[index].title,
×
UNCOV
3466
                        displayData: columnsIn[index].title,
×
UNCOV
3467
                        allowOverlay: false,
×
UNCOV
3468
                    })) as GridCell[];
×
3469
                    copyToClipboard([headers, ...cells], columnIndexes, e);
×
UNCOV
3470
                }
×
3471
            };
5✔
3472

6✔
3473
            if (focused && getCellsForSelection !== undefined) {
6✔
3474
                if (gridSelection.current !== undefined) {
6✔
3475
                    let thunk = getCellsForSelection(gridSelection.current.range, abortControllerRef.current.signal);
3✔
3476
                    if (typeof thunk !== "object") {
3!
3477
                        thunk = await thunk();
×
UNCOV
3478
                    }
×
3479
                    copyToClipboardWithHeaders(
3✔
3480
                        thunk,
3✔
3481
                        range(
3✔
3482
                            gridSelection.current.range.x - rowMarkerOffset,
3✔
3483
                            gridSelection.current.range.x + gridSelection.current.range.width - rowMarkerOffset
3✔
3484
                        )
3✔
3485
                    );
3✔
3486
                } else if (selectedRows !== undefined && selectedRows.length > 0) {
3✔
3487
                    const toCopy = [...selectedRows];
1✔
3488
                    const cells = toCopy.map(rowIndex => {
1✔
3489
                        const thunk = getCellsForSelection(
1✔
3490
                            {
1✔
3491
                                x: rowMarkerOffset,
1✔
3492
                                y: rowIndex,
1✔
3493
                                width: columnsIn.length,
1✔
3494
                                height: 1,
1✔
3495
                            },
1✔
3496
                            abortControllerRef.current.signal
1✔
3497
                        );
1✔
3498
                        if (typeof thunk === "object") {
1✔
3499
                            return thunk[0];
1✔
3500
                        }
1!
3501
                        return thunk().then(v => v[0]);
×
3502
                    });
1✔
3503
                    if (cells.some(x => x instanceof Promise)) {
1!
3504
                        const settled = await Promise.all(cells);
×
3505
                        copyToClipboardWithHeaders(settled, range(columnsIn.length));
×
3506
                    } else {
1✔
3507
                        copyToClipboardWithHeaders(cells as (readonly GridCell[])[], range(columnsIn.length));
1✔
3508
                    }
1✔
3509
                } else if (selectedColumns.length > 0) {
3✔
3510
                    const results: (readonly (readonly GridCell[])[])[] = [];
1✔
3511
                    const cols: number[] = [];
1✔
3512
                    for (const col of selectedColumns) {
1✔
3513
                        let thunk = getCellsForSelection(
3✔
3514
                            {
3✔
3515
                                x: col,
3✔
3516
                                y: 0,
3✔
3517
                                width: 1,
3✔
3518
                                height: rows,
3✔
3519
                            },
3✔
3520
                            abortControllerRef.current.signal
3✔
3521
                        );
3✔
3522
                        if (typeof thunk !== "object") {
3!
3523
                            thunk = await thunk();
×
UNCOV
3524
                        }
×
3525
                        results.push(thunk);
3✔
3526
                        cols.push(col - rowMarkerOffset);
3✔
3527
                    }
3✔
3528
                    if (results.length === 1) {
1!
3529
                        copyToClipboardWithHeaders(results[0], cols);
×
3530
                    } else {
1✔
3531
                        // FIXME: this is dumb
1✔
3532
                        const toCopy = results.reduce((pv, cv) => pv.map((row, index) => [...row, ...cv[index]]));
1✔
3533
                        copyToClipboardWithHeaders(toCopy, cols);
1✔
3534
                    }
1✔
3535
                }
1✔
3536
            }
6✔
3537
        },
6✔
3538
        [columnsIn, getCellsForSelection, gridSelection, keybindings.copy, rowMarkerOffset, rows, copyHeaders]
678✔
3539
    );
678✔
3540

678✔
3541
    useEventListener("copy", onCopy, safeWindow, false, false);
678✔
3542

678✔
3543
    const onCut = React.useCallback(
678✔
3544
        async (e?: ClipboardEvent) => {
678✔
3545
            if (!keybindings.cut) return;
1!
3546
            const focused =
1✔
3547
                scrollRef.current?.contains(document.activeElement) === true ||
1✔
3548
                canvasRef.current?.contains(document.activeElement) === true;
1✔
3549

1✔
3550
            if (!focused) return;
1!
3551
            await onCopy(e);
1✔
3552
            if (gridSelection.current !== undefined) {
1✔
3553
                let effectiveSelection: GridSelection = {
1✔
3554
                    current: {
1✔
3555
                        cell: gridSelection.current.cell,
1✔
3556
                        range: gridSelection.current.range,
1✔
3557
                        rangeStack: [],
1✔
3558
                    },
1✔
3559
                    rows: CompactSelection.empty(),
1✔
3560
                    columns: CompactSelection.empty(),
1✔
3561
                };
1✔
3562
                const onDeleteResult = onDelete?.(effectiveSelection);
1✔
3563
                if (onDeleteResult === false) return;
1!
3564
                effectiveSelection = onDeleteResult === true ? effectiveSelection : onDeleteResult;
1!
3565
                if (effectiveSelection.current === undefined) return;
1!
3566
                deleteRange(effectiveSelection.current.range);
1✔
3567
            }
1✔
3568
        },
1✔
3569
        [deleteRange, gridSelection, keybindings.cut, onCopy, onDelete]
678✔
3570
    );
678✔
3571

678✔
3572
    useEventListener("cut", onCut, safeWindow, false, false);
678✔
3573

678✔
3574
    const onSearchResultsChanged = React.useCallback(
678✔
3575
        (results: readonly Item[], navIndex: number) => {
678✔
3576
            if (onSearchResultsChangedIn !== undefined) {
7!
UNCOV
3577
                if (rowMarkerOffset !== 0) {
×
3578
                    results = results.map(item => [item[0] - rowMarkerOffset, item[1]]);
×
UNCOV
3579
                }
×
3580
                onSearchResultsChangedIn(results, navIndex);
×
3581
                return;
×
UNCOV
3582
            }
×
3583
            if (results.length === 0 || navIndex === -1) return;
7✔
3584

2✔
3585
            const [col, row] = results[navIndex];
2✔
3586
            if (lastSent.current !== undefined && lastSent.current[0] === col && lastSent.current[1] === row) {
7!
3587
                return;
×
UNCOV
3588
            }
✔
3589
            lastSent.current = [col, row];
2✔
3590
            updateSelectedCell(col, row, false, false);
2✔
3591
        },
7✔
3592
        [onSearchResultsChangedIn, rowMarkerOffset, updateSelectedCell]
678✔
3593
    );
678✔
3594

678✔
3595
    // this effects purpose in life is to scroll the newly selected cell into view when and ONLY when that cell
678✔
3596
    // is from an external gridSelection change. Also note we want the unmangled out selection because scrollTo
678✔
3597
    // expects unmangled indexes
678✔
3598
    const [outCol, outRow] = gridSelectionOuter?.current?.cell ?? [];
678✔
3599
    const scrollToRef = React.useRef(scrollTo);
678✔
3600
    scrollToRef.current = scrollTo;
678✔
3601
    React.useLayoutEffect(() => {
678✔
3602
        if (
211✔
3603
            !hasJustScrolled.current &&
211✔
3604
            outCol !== undefined &&
211✔
3605
            outRow !== undefined &&
77✔
3606
            (outCol !== expectedExternalGridSelection.current?.current?.cell[0] ||
77✔
3607
                outRow !== expectedExternalGridSelection.current?.current?.cell[1])
77✔
3608
        ) {
211!
3609
            scrollToRef.current(outCol, outRow);
×
UNCOV
3610
        }
×
3611
        hasJustScrolled.current = false; //only allow skipping a single scroll
211✔
3612
    }, [outCol, outRow]);
678✔
3613

678✔
3614
    const selectionOutOfBounds =
678✔
3615
        gridSelection.current !== undefined &&
678✔
3616
        (gridSelection.current.cell[0] >= mangledCols.length || gridSelection.current.cell[1] >= mangledRows);
321✔
3617
    React.useLayoutEffect(() => {
678✔
3618
        if (selectionOutOfBounds) {
140✔
3619
            setGridSelection(emptyGridSelection, false);
1✔
3620
        }
1✔
3621
    }, [selectionOutOfBounds, setGridSelection]);
678✔
3622

678✔
3623
    const disabledRows = React.useMemo(() => {
678✔
3624
        if (showTrailingBlankRow === true && trailingRowOptions?.tint === true) {
139✔
3625
            return CompactSelection.fromSingleSelection(mangledRows - 1);
136✔
3626
        }
136✔
3627
        return CompactSelection.empty();
3✔
3628
    }, [mangledRows, showTrailingBlankRow, trailingRowOptions?.tint]);
678✔
3629

678✔
3630
    const mangledVerticalBorder = React.useCallback(
678✔
3631
        (col: number) => {
678✔
3632
            return typeof verticalBorder === "boolean"
7,568!
UNCOV
3633
                ? verticalBorder
×
3634
                : verticalBorder?.(col - rowMarkerOffset) ?? true;
7,568!
3635
        },
7,568✔
3636
        [rowMarkerOffset, verticalBorder]
678✔
3637
    );
678✔
3638

678✔
3639
    const renameGroupNode = React.useMemo(() => {
678✔
3640
        if (renameGroup === undefined || canvasRef.current === null) return null;
140✔
3641
        const { bounds, group } = renameGroup;
2✔
3642
        const canvasBounds = canvasRef.current.getBoundingClientRect();
2✔
3643
        return (
2✔
3644
            <GroupRename
2✔
3645
                bounds={bounds}
2✔
3646
                group={group}
2✔
3647
                canvasBounds={canvasBounds}
2✔
3648
                onClose={() => setRenameGroup(undefined)}
2✔
3649
                onFinish={newVal => {
2✔
3650
                    setRenameGroup(undefined);
1✔
3651
                    onGroupHeaderRenamed?.(group, newVal);
1✔
3652
                }}
1✔
3653
            />
2✔
3654
        );
140✔
3655
    }, [onGroupHeaderRenamed, renameGroup]);
678✔
3656

678✔
3657
    const mangledFreezeColumns = Math.min(mangledCols.length, freezeColumns + (hasRowMarkers ? 1 : 0));
678✔
3658

678✔
3659
    React.useImperativeHandle(
678✔
3660
        forwardedRef,
678✔
3661
        () => ({
678✔
3662
            appendRow: (col: number, openOverlay?: boolean) => appendRow(col + rowMarkerOffset, openOverlay),
27✔
3663
            updateCells: damageList => {
27✔
3664
                if (rowMarkerOffset !== 0) {
2✔
3665
                    damageList = damageList.map(x => ({ cell: [x.cell[0] + rowMarkerOffset, x.cell[1]] }));
1✔
3666
                }
1✔
3667
                return gridRef.current?.damage(damageList);
2✔
3668
            },
2✔
3669
            getBounds: (col, row) => {
27✔
3670
                if (canvasRef?.current === null || scrollRef?.current === null) {
1!
NEW
3671
                    return undefined;
×
UNCOV
3672
                }
×
3673

1✔
3674
                if (col === undefined && row === undefined) {
1!
UNCOV
3675
                    // Return the bounds of the entire scroll area:
×
NEW
3676
                    const rect = canvasRef.current.getBoundingClientRect();
×
NEW
3677
                    const scale = rect.width / scrollRef.current.clientWidth;
×
NEW
3678
                    return {
×
NEW
3679
                        x: rect.x - scrollRef.current.scrollLeft * scale,
×
NEW
3680
                        y: rect.y - scrollRef.current.scrollTop * scale,
×
NEW
3681
                        width: scrollRef.current.scrollWidth * scale,
×
NEW
3682
                        height: scrollRef.current.scrollHeight * scale,
×
NEW
3683
                    };
×
3684
                }
×
3685
                return gridRef.current?.getBounds((col ?? 0) + rowMarkerOffset, row);
1!
3686
            },
1✔
3687
            focus: () => gridRef.current?.focus(),
27✔
3688
            emit: async e => {
27✔
3689
                switch (e) {
5✔
3690
                    case "delete":
5✔
3691
                        onKeyDown({
1✔
3692
                            bounds: undefined,
1✔
3693
                            cancel: () => undefined,
1✔
3694
                            stopPropagation: () => undefined,
1✔
3695
                            preventDefault: () => undefined,
1✔
3696
                            ctrlKey: false,
1✔
3697
                            key: "Delete",
1✔
3698
                            keyCode: 46,
1✔
3699
                            metaKey: false,
1✔
3700
                            shiftKey: false,
1✔
3701
                            altKey: false,
1✔
3702
                            rawEvent: undefined,
1✔
3703
                            location: undefined,
1✔
3704
                        });
1✔
3705
                        break;
1✔
3706
                    case "fill-right":
5✔
3707
                        onKeyDown({
1✔
3708
                            bounds: undefined,
1✔
3709
                            cancel: () => undefined,
1✔
3710
                            stopPropagation: () => undefined,
1✔
3711
                            preventDefault: () => undefined,
1✔
3712
                            ctrlKey: true,
1✔
3713
                            key: "r",
1✔
3714
                            keyCode: 82,
1✔
3715
                            metaKey: false,
1✔
3716
                            shiftKey: false,
1✔
3717
                            altKey: false,
1✔
3718
                            rawEvent: undefined,
1✔
3719
                            location: undefined,
1✔
3720
                        });
1✔
3721
                        break;
1✔
3722
                    case "fill-down":
5✔
3723
                        onKeyDown({
1✔
3724
                            bounds: undefined,
1✔
3725
                            cancel: () => undefined,
1✔
3726
                            stopPropagation: () => undefined,
1✔
3727
                            preventDefault: () => undefined,
1✔
3728
                            ctrlKey: true,
1✔
3729
                            key: "d",
1✔
3730
                            keyCode: 68,
1✔
3731
                            metaKey: false,
1✔
3732
                            shiftKey: false,
1✔
3733
                            altKey: false,
1✔
3734
                            rawEvent: undefined,
1✔
3735
                            location: undefined,
1✔
3736
                        });
1✔
3737
                        break;
1✔
3738
                    case "copy":
5✔
3739
                        await onCopy(undefined, true);
1✔
3740
                        break;
1✔
3741
                    case "paste":
5✔
3742
                        await onPasteInternal();
1✔
3743
                        break;
1✔
3744
                }
5✔
3745
            },
5✔
3746
            scrollTo,
27✔
3747
            remeasureColumns: cols => {
27✔
3748
                for (const col of cols) {
1✔
3749
                    void normalSizeColumn(col + rowMarkerOffset);
1✔
3750
                }
1✔
3751
            },
1✔
3752
        }),
27✔
3753
        [appendRow, normalSizeColumn, onCopy, onKeyDown, onPasteInternal, rowMarkerOffset, scrollTo]
678✔
3754
    );
678✔
3755

678✔
3756
    const [selCol, selRow] = currentCell ?? [];
678✔
3757
    const onCellFocused = React.useCallback(
678✔
3758
        (cell: Item) => {
678✔
3759
            const [col, row] = cell;
28✔
3760

28✔
3761
            if (row === -1) {
28!
UNCOV
3762
                if (columnSelect !== "none") {
×
3763
                    setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, false);
×
3764
                    focus();
×
UNCOV
3765
                }
×
3766
                return;
×
UNCOV
3767
            }
×
3768

28✔
3769
            if (selCol === col && selRow === row) return;
28✔
3770
            setCurrent(
1✔
3771
                {
1✔
3772
                    cell,
1✔
3773
                    range: { x: col, y: row, width: 1, height: 1 },
1✔
3774
                },
1✔
3775
                true,
1✔
3776
                false,
1✔
3777
                "keyboard-nav"
1✔
3778
            );
1✔
3779
            scrollTo(col, row);
1✔
3780
        },
28✔
3781
        [columnSelect, focus, scrollTo, selCol, selRow, setCurrent, setSelectedColumns]
678✔
3782
    );
678✔
3783

678✔
3784
    const [isFocused, setIsFocused] = React.useState(false);
678✔
3785
    const setIsFocusedDebounced = React.useRef(
678✔
3786
        debounce((val: boolean) => {
678✔
3787
            setIsFocused(val);
56✔
3788
        }, 5)
678✔
3789
    );
678✔
3790

678✔
3791
    const onCanvasFocused = React.useCallback(() => {
678✔
3792
        setIsFocusedDebounced.current(true);
66✔
3793

66✔
3794
        // check for mouse state, don't do anything if the user is clicked to focus.
66✔
3795
        if (
66✔
3796
            gridSelection.current === undefined &&
66✔
3797
            gridSelection.columns.length === 0 &&
6✔
3798
            gridSelection.rows.length === 0 &&
5✔
3799
            mouseState === undefined
5✔
3800
        ) {
66✔
3801
            setCurrent(
5✔
3802
                {
5✔
3803
                    cell: [rowMarkerOffset, cellYOffset],
5✔
3804
                    range: {
5✔
3805
                        x: rowMarkerOffset,
5✔
3806
                        y: cellYOffset,
5✔
3807
                        width: 1,
5✔
3808
                        height: 1,
5✔
3809
                    },
5✔
3810
                },
5✔
3811
                true,
5✔
3812
                false,
5✔
3813
                "keyboard-select"
5✔
3814
            );
5✔
3815
        }
5✔
3816
    }, [cellYOffset, gridSelection, mouseState, rowMarkerOffset, setCurrent]);
678✔
3817

678✔
3818
    const onFocusOut = React.useCallback(() => {
678✔
3819
        setIsFocusedDebounced.current(false);
32✔
3820
    }, []);
678✔
3821

678✔
3822
    const [idealWidth, idealHeight] = React.useMemo(() => {
678✔
3823
        let h: number;
152✔
3824
        const scrollbarWidth = experimental?.scrollbarWidthOverride ?? getScrollBarWidth();
152✔
3825
        const rowsCountWithTrailingRow = rows + (showTrailingBlankRow ? 1 : 0);
152!
3826
        if (typeof rowHeight === "number") {
152✔
3827
            h = totalHeaderHeight + rowsCountWithTrailingRow * rowHeight;
151✔
3828
        } else {
152✔
3829
            let avg = 0;
1✔
3830
            const toAverage = Math.min(rowsCountWithTrailingRow, 10);
1✔
3831
            for (let i = 0; i < toAverage; i++) {
1✔
3832
                avg += rowHeight(i);
10✔
3833
            }
10✔
3834
            avg = Math.floor(avg / toAverage);
1✔
3835

1✔
3836
            h = totalHeaderHeight + rowsCountWithTrailingRow * avg;
1✔
3837
        }
1✔
3838
        h += scrollbarWidth;
152✔
3839

152✔
3840
        const w = mangledCols.reduce((acc, x) => x.width + acc, 0) + scrollbarWidth;
152✔
3841

152✔
3842
        // We need to set a reasonable cap here as some browsers will just ignore huge values
152✔
3843
        // rather than treat them as huge values.
152✔
3844
        return [`${Math.min(100_000, w)}px`, `${Math.min(100_000, h)}px`];
152✔
3845
    }, [mangledCols, experimental?.scrollbarWidthOverride, rowHeight, rows, showTrailingBlankRow, totalHeaderHeight]);
678✔
3846

678✔
3847
    return (
678✔
3848
        <ThemeContext.Provider value={mergedTheme}>
678✔
3849
            <DataEditorContainer
678✔
3850
                style={makeCSSStyle(mergedTheme)}
678✔
3851
                className={className}
678✔
3852
                inWidth={width ?? idealWidth}
678✔
3853
                inHeight={height ?? idealHeight}>
678✔
3854
                <DataGridSearch
678✔
3855
                    fillHandle={fillHandle}
678✔
3856
                    drawFocusRing={drawFocusRing}
678✔
3857
                    experimental={experimental}
678✔
3858
                    fixedShadowX={fixedShadowX}
678✔
3859
                    fixedShadowY={fixedShadowY}
678✔
3860
                    getRowThemeOverride={getRowThemeOverride}
678✔
3861
                    headerIcons={headerIcons}
678✔
3862
                    imageWindowLoader={imageWindowLoader}
678✔
3863
                    initialSize={initialSize}
678✔
3864
                    isDraggable={isDraggable}
678✔
3865
                    onDragLeave={onDragLeave}
678✔
3866
                    onRowMoved={onRowMoved}
678✔
3867
                    overscrollX={overscrollX}
678✔
3868
                    overscrollY={overscrollY}
678✔
3869
                    preventDiagonalScrolling={preventDiagonalScrolling}
678✔
3870
                    rightElement={rightElement}
678✔
3871
                    rightElementProps={rightElementProps}
678✔
3872
                    smoothScrollX={smoothScrollX}
678✔
3873
                    smoothScrollY={smoothScrollY}
678✔
3874
                    className={className}
678✔
3875
                    enableGroups={enableGroups}
678✔
3876
                    onCanvasFocused={onCanvasFocused}
678✔
3877
                    onCanvasBlur={onFocusOut}
678✔
3878
                    canvasRef={canvasRef}
678✔
3879
                    onContextMenu={onContextMenu}
678✔
3880
                    theme={mergedTheme}
678✔
3881
                    cellXOffset={cellXOffset}
678✔
3882
                    cellYOffset={cellYOffset}
678✔
3883
                    accessibilityHeight={visibleRegion.height}
678✔
3884
                    onDragEnd={onDragEnd}
678✔
3885
                    columns={mangledCols}
678✔
3886
                    nonGrowWidth={nonGrowWidth}
678✔
3887
                    drawHeader={drawHeader}
678✔
3888
                    onColumnProposeMove={onColumnProposeMove}
678✔
3889
                    drawCell={drawCell}
678✔
3890
                    disabledRows={disabledRows}
678✔
3891
                    freezeColumns={mangledFreezeColumns}
678✔
3892
                    lockColumns={rowMarkerOffset}
678✔
3893
                    firstColAccessible={rowMarkerOffset === 0}
678✔
3894
                    getCellContent={getMangledCellContent}
678✔
3895
                    minColumnWidth={minColumnWidth}
678✔
3896
                    maxColumnWidth={maxColumnWidth}
678✔
3897
                    searchInputRef={searchInputRef}
678✔
3898
                    showSearch={showSearch}
678✔
3899
                    onSearchClose={onSearchClose}
678✔
3900
                    highlightRegions={highlightRegions}
678✔
3901
                    getCellsForSelection={getCellsForSelection}
678✔
3902
                    getGroupDetails={mangledGetGroupDetails}
678✔
3903
                    headerHeight={headerHeight}
678✔
3904
                    isFocused={isFocused}
678✔
3905
                    groupHeaderHeight={enableGroups ? groupHeaderHeight : 0}
678✔
3906
                    trailingRowType={
678✔
3907
                        !showTrailingBlankRow ? "none" : trailingRowOptions?.sticky === true ? "sticky" : "appended"
678!
3908
                    }
678✔
3909
                    onColumnResize={onColumnResize}
678✔
3910
                    onColumnResizeEnd={onColumnResizeEnd}
678✔
3911
                    onColumnResizeStart={onColumnResizeStart}
678✔
3912
                    onCellFocused={onCellFocused}
678✔
3913
                    onColumnMoved={onColumnMovedImpl}
678✔
3914
                    onDragStart={onDragStartImpl}
678✔
3915
                    onHeaderMenuClick={onHeaderMenuClickInner}
678✔
3916
                    onItemHovered={onItemHoveredImpl}
678✔
3917
                    isFilling={mouseState?.fillHandle === true}
678✔
3918
                    onMouseMove={onMouseMoveImpl}
678✔
3919
                    onKeyDown={onKeyDown}
678✔
3920
                    onKeyUp={onKeyUpIn}
678✔
3921
                    onMouseDown={onMouseDown}
678✔
3922
                    onMouseUp={onMouseUp}
678✔
3923
                    onDragOverCell={onDragOverCell}
678✔
3924
                    onDrop={onDrop}
678✔
3925
                    onSearchResultsChanged={onSearchResultsChanged}
678✔
3926
                    onVisibleRegionChanged={onVisibleRegionChangedImpl}
678✔
3927
                    clientSize={clientSize}
678✔
3928
                    rowHeight={rowHeight}
678✔
3929
                    searchResults={searchResults}
678✔
3930
                    searchValue={searchValue}
678✔
3931
                    onSearchValueChange={onSearchValueChange}
678✔
3932
                    rows={mangledRows}
678✔
3933
                    scrollRef={scrollRef}
678✔
3934
                    selection={gridSelection}
678✔
3935
                    translateX={visibleRegion.tx}
678✔
3936
                    translateY={visibleRegion.ty}
678✔
3937
                    verticalBorder={mangledVerticalBorder}
678✔
3938
                    gridRef={gridRef}
678✔
3939
                    getCellRenderer={getCellRenderer}
678✔
3940
                />
678✔
3941
                {renameGroupNode}
678✔
3942
                {overlay !== undefined && (
678✔
3943
                    <React.Suspense fallback={null}>
30✔
3944
                        <DataGridOverlayEditor
30✔
3945
                            {...overlay}
30✔
3946
                            validateCell={validateCell}
30✔
3947
                            id={overlayID}
30✔
3948
                            getCellRenderer={getCellRenderer}
30✔
3949
                            className={experimental?.isSubGrid === true ? "click-outside-ignore" : undefined}
30!
3950
                            provideEditor={provideEditor}
30✔
3951
                            imageEditorOverride={imageEditorOverride}
30✔
3952
                            onFinishEditing={onFinishEditing}
30✔
3953
                            markdownDivCreateNode={markdownDivCreateNode}
30✔
3954
                            isOutsideClick={isOutsideClick}
30✔
3955
                        />
30✔
3956
                    </React.Suspense>
30✔
3957
                )}
678✔
3958
            </DataEditorContainer>
678✔
3959
        </ThemeContext.Provider>
678✔
3960
    );
678✔
3961
};
678✔
3962

1✔
3963
/**
1✔
3964
 * The primary component of Glide Data Grid.
1✔
3965
 * @category DataEditor
1✔
3966
 * @param {DataEditorProps} props
1✔
3967
 */
1✔
3968
export const DataEditor = React.forwardRef(DataEditorImpl);
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc