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

glideapps / glide-data-grid / 6389513233

03 Oct 2023 06:31AM UTC coverage: 86.237%. First build
6389513233

Pull #781

github

web-flow
Merge 90bdfc424 into 0ea52f371
Pull Request #781: 5.3.1

3378 of 4436 branches covered (0.0%)

225 of 225 new or added lines in 29 files covered. (100.0%)

4010 of 4650 relevant lines covered (86.24%)

3214.2 hits per line

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

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

69
let idCounter = 0;
9✔
70

71
interface MouseState {
72
    readonly previousSelection?: GridSelection;
73
    readonly fillHandle?: boolean;
74
}
75

76
type Props = Partial<
77
    Omit<
78
        DataGridSearchProps,
79
        | "accessibilityHeight"
80
        | "canvasRef"
81
        | "cellXOffset"
82
        | "cellYOffset"
83
        | "className"
84
        | "clientSize"
85
        | "columns"
86
        | "disabledRows"
87
        | "drawCustomCell"
88
        | "enableGroups"
89
        | "firstColAccessible"
90
        | "firstColSticky"
91
        | "freezeColumns"
92
        | "getCellContent"
93
        | "getCellRenderer"
94
        | "getCellsForSelection"
95
        | "gridRef"
96
        | "groupHeaderHeight"
97
        | "headerHeight"
98
        | "isFilling"
99
        | "isFocused"
100
        | "lockColumns"
101
        | "maxColumnWidth"
102
        | "minColumnWidth"
103
        | "onCanvasBlur"
104
        | "onCanvasFocused"
105
        | "onCellFocused"
106
        | "onContextMenu"
107
        | "onDragEnd"
108
        | "onMouseDown"
109
        | "onMouseMove"
110
        | "onMouseUp"
111
        | "onVisibleRegionChanged"
112
        | "rowHeight"
113
        | "rows"
114
        | "scrollRef"
115
        | "searchInputRef"
116
        | "selectedColumns"
117
        | "selection"
118
        | "theme"
119
        | "trailingRowType"
120
        | "translateX"
121
        | "translateY"
122
        | "verticalBorder"
123
    >
124
>;
125

126
type EditListItem = { location: Item; value: EditableGridCell };
127

128
type EmitEvents = "copy" | "paste" | "delete" | "fill-right" | "fill-down";
129

130
function getSpanStops(cells: readonly (readonly GridCell[])[]): number[] {
131
    return uniq(
5✔
132
        flatten(
133
            flatten(cells)
134
                .filter(c => c.span !== undefined)
5✔
135
                .map(c => range((c.span?.[0] ?? 0) + 1, (c.span?.[1] ?? 0) + 1))
×
136
        )
137
    );
138
}
139

140
function shiftSelection(input: GridSelection, offset: number): GridSelection {
141
    if (input === undefined || offset === 0 || (input.columns.length === 0 && input.current === undefined))
279✔
142
        return input;
244✔
143

144
    return {
35✔
145
        current:
146
            input.current === undefined
35✔
147
                ? undefined
148
                : {
149
                      cell: [input.current.cell[0] + offset, input.current.cell[1]],
150
                      range: {
151
                          ...input.current.range,
152
                          x: input.current.range.x + offset,
153
                      },
154
                      rangeStack: input.current.rangeStack.map(r => ({
×
155
                          ...r,
156
                          x: r.x + offset,
157
                      })),
158
                  },
159
        rows: input.rows,
160
        columns: input.columns.offset(offset),
161
    };
162
}
163

164
interface Keybinds {
165
    readonly selectAll: boolean;
166
    readonly selectRow: boolean;
167
    readonly selectColumn: boolean;
168
    readonly downFill: boolean;
169
    readonly rightFill: boolean;
170
    readonly pageUp: boolean;
171
    readonly pageDown: boolean;
172
    readonly clear: boolean;
173
    readonly copy: boolean;
174
    readonly paste: boolean;
175
    readonly cut: boolean;
176
    readonly search: boolean;
177
    readonly first: boolean;
178
    readonly last: boolean;
179
}
180

181
const keybindingDefaults: Keybinds = {
9✔
182
    selectAll: true,
183
    selectRow: true,
184
    selectColumn: true,
185
    downFill: false,
186
    rightFill: false,
187
    pageUp: false,
188
    pageDown: false,
189
    clear: true,
190
    copy: true,
191
    paste: true,
192
    cut: true,
193
    search: false,
194
    first: true,
195
    last: true,
196
};
197

198
/**
199
 * @category DataEditor
200
 */
201
export interface DataEditorProps extends Props {
202
    /** Emitted whenever the user has requested the deletion of the selection.
203
     * @group Editing
204
     */
205
    readonly onDelete?: (selection: GridSelection) => boolean | GridSelection;
206
    /** Emitted whenever a cell edit is completed.
207
     * @group Editing
208
     */
209
    readonly onCellEdited?: (cell: Item, newValue: EditableGridCell) => void;
210
    /** Emitted whenever a cell mutation is completed and provides all edits inbound as a single batch.
211
     * @group Editing
212
     */
213
    readonly onCellsEdited?: (newValues: readonly EditListItem[]) => boolean | void;
214
    /** Emitted whenever a row append operation is requested. Append location can be set in callback.
215
     * @group Editing
216
     */
217
    readonly onRowAppended?: () => Promise<"top" | "bottom" | number | undefined> | void;
218
    /** Emitted when a column header should show a context menu. Usually right click.
219
     * @group Events
220
     */
221
    readonly onHeaderClicked?: (colIndex: number, event: HeaderClickedEventArgs) => void;
222
    /** Emitted when a group header is clicked.
223
     * @group Events
224
     */
225
    readonly onGroupHeaderClicked?: (colIndex: number, event: GroupHeaderClickedEventArgs) => void;
226
    /** Emitted whe the user wishes to rename a group.
227
     * @group Events
228
     */
229
    readonly onGroupHeaderRenamed?: (groupName: string, newVal: string) => void;
230
    /** Emitted when a cell is clicked.
231
     * @group Events
232
     */
233
    readonly onCellClicked?: (cell: Item, event: CellClickedEventArgs) => void;
234
    /** Emitted when a cell is activated, by pressing Enter, Space or double clicking it.
235
     * @group Events
236
     */
237
    readonly onCellActivated?: (cell: Item) => void;
238
    /** Emitted when editing has finished, regardless of data changing or not.
239
     * @group Editing
240
     */
241
    readonly onFinishedEditing?: (newValue: GridCell | undefined, movement: Item) => void;
242
    /** Emitted when a column header should show a context menu. Usually right click.
243
     * @group Events
244
     */
245
    readonly onHeaderContextMenu?: (colIndex: number, event: HeaderClickedEventArgs) => void;
246
    /** Emitted when a group header should show a context menu. Usually right click.
247
     * @group Events
248
     */
249
    readonly onGroupHeaderContextMenu?: (colIndex: number, event: GroupHeaderClickedEventArgs) => void;
250
    /** Emitted when a cell should show a context menu. Usually right click.
251
     * @group Events
252
     */
253
    readonly onCellContextMenu?: (cell: Item, event: CellClickedEventArgs) => void;
254
    /** Used for validating cell values during editing.
255
     * @group Editing
256
     * @param cell The cell which is being validated.
257
     * @param newValue The new value being proposed.
258
     * @param prevValue The previous value before the edit.
259
     * @returns A return of false indicates the value will not be accepted. A value of
260
     * true indicates the value will be accepted. Returning a new GridCell will immediately coerce the value to match.
261
     */
262
    readonly validateCell?: (
263
        cell: Item,
264
        newValue: EditableGridCell,
265
        prevValue: GridCell
266
    ) => boolean | ValidatedGridCell;
267

268
    /** The columns to display in the data grid.
269
     * @group Data
270
     */
271
    readonly columns: readonly GridColumn[];
272

273
    /** Controls the trailing row used to insert new data into the grid.
274
     * @group Editing
275
     */
276
    readonly trailingRowOptions?: {
277
        /** If the trailing row should be tinted */
278
        readonly tint?: boolean;
279
        /** A hint string displayed on hover. Usually something like "New row" */
280
        readonly hint?: string;
281
        /** When set to true, the trailing row is always visible. */
282
        readonly sticky?: boolean;
283
        /** The icon to use for the cell. Either a GridColumnIcon or a member of the passed headerIcons */
284
        readonly addIcon?: string;
285
        /** Overrides the column to focus when a new row is created. */
286
        readonly targetColumn?: number | GridColumn;
287
    };
288
    /** Controls the height of the header row
289
     * @defaultValue 36
290
     * @group Style
291
     */
292
    readonly headerHeight?: number;
293
    /** Controls the header of the group header row
294
     * @defaultValue `headerHeight`
295
     * @group Style
296
     */
297
    readonly groupHeaderHeight?: number;
298

299
    /**
300
     * The number of rows in the grid.
301
     * @group Data
302
     */
303
    readonly rows: number;
304

305
    /** Determines if row markers should be automatically added to the grid.
306
     * Interactive row markers allow the user to select a row.
307
     *
308
     * - "clickable-number" renders a number that can be clicked to
309
     *   select the row
310
     * - "both" causes the row marker to show up as a number but
311
     *   reveal a checkbox when the marker is hovered.
312
     *
313
     * @defaultValue `none`
314
     * @group Style
315
     */
316
    readonly rowMarkers?: "checkbox" | "number" | "clickable-number" | "checkbox-visible" | "both" | "none";
317
    /**
318
     * Sets the width of row markers in pixels, if unset row markers will automatically size.
319
     * @group Style
320
     */
321
    readonly rowMarkerWidth?: number;
322
    /** Changes the starting index for row markers.
323
     * @defaultValue 1
324
     * @group Style
325
     */
326
    readonly rowMarkerStartIndex?: number;
327

328
    /** Changes the theme of the row marker column
329
     * @group Style
330
     */
331
    readonly rowMarkerTheme?: Partial<Theme>;
332

333
    /** Sets the width of the data grid.
334
     * @group Style
335
     */
336
    readonly width?: number | string;
337
    /** Sets the height of the data grid.
338
     * @group Style
339
     */
340
    readonly height?: number | string;
341
    /** Custom classname for data grid wrapper.
342
     * @group Style
343
     */
344
    readonly className?: string;
345

346
    /** If set to `default`, `gridSelection` will be coerced to always include full spans.
347
     * @group Selection
348
     * @defaultValue `default`
349
     */
350
    readonly spanRangeBehavior?: "default" | "allowPartial";
351

352
    /** Controls which types of selections can exist at the same time in the grid. If selection blending is set to
353
     * exclusive, the grid will clear other types of selections when the exclusive selection is made. By default row,
354
     * column, and range selections are exclusive.
355
     * @group Selection
356
     * @defaultValue `exclusive`
357
     * */
358
    readonly rangeSelectionBlending?: SelectionBlending;
359
    /** {@inheritDoc rangeSelectionBlending}
360
     * @group Selection
361
     */
362
    readonly columnSelectionBlending?: SelectionBlending;
363
    /** {@inheritDoc rangeSelectionBlending}
364
     * @group Selection
365
     */
366
    readonly rowSelectionBlending?: SelectionBlending;
367
    /** Controls if multi-selection is allowed. If disabled, shift/ctrl/command clicking will work as if no modifiers
368
     * are pressed.
369
     *
370
     * 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
371
     * time. The multi variants allow for multiples of the rect or cell to be selected.
372
     * @group Selection
373
     * @defaultValue `rect`
374
     */
375
    readonly rangeSelect?: "none" | "cell" | "rect" | "multi-cell" | "multi-rect";
376
    /** {@inheritDoc rangeSelect}
377
     * @group Selection
378
     * @defaultValue `multi`
379
     */
380
    readonly columnSelect?: "none" | "single" | "multi";
381
    /** {@inheritDoc rangeSelect}
382
     * @group Selection
383
     * @defaultValue `multi`
384
     */
385
    readonly rowSelect?: "none" | "single" | "multi";
386

387
    /** Sets the initial scroll Y offset.
388
     * @see {@link scrollOffsetX}
389
     * @group Advanced
390
     */
391
    readonly scrollOffsetY?: number;
392
    /** Sets the initial scroll X offset
393
     * @see {@link scrollOffsetY}
394
     * @group Advanced
395
     */
396
    readonly scrollOffsetX?: number;
397

398
    /** Determins the height of each row.
399
     * @group Style
400
     * @defaultValue 34
401
     */
402
    readonly rowHeight?: DataGridSearchProps["rowHeight"];
403
    /** Fires whenever the mouse moves
404
     * @group Events
405
     * @param args
406
     */
407
    readonly onMouseMove?: DataGridSearchProps["onMouseMove"];
408

409
    /**
410
     * The minimum width a column can be resized to.
411
     * @defaultValue 50
412
     * @group Style
413
     */
414
    readonly minColumnWidth?: DataGridSearchProps["minColumnWidth"];
415
    /**
416
     * The maximum width a column can be resized to.
417
     * @defaultValue 500
418
     * @group Style
419
     */
420
    readonly maxColumnWidth?: DataGridSearchProps["maxColumnWidth"];
421
    /**
422
     * The maximum width a column can be automatically sized to.
423
     * @defaultValue `maxColumnWidth`
424
     * @group Style
425
     */
426
    readonly maxColumnAutoWidth?: number;
427

428
    /**
429
     * Used to provide an override to the default image editor for the data grid. `provideEditor` may be a better
430
     * choice for most people.
431
     * @group Advanced
432
     * */
433
    readonly imageEditorOverride?: ImageEditorType;
434
    /**
435
     * If specified, it will be used to render Markdown, instead of the default Markdown renderer used by the Grid.
436
     * You'll want to use this if you need to process your Markdown for security purposes, or if you want to use a
437
     * renderer with different Markdown features.
438
     * @group Advanced
439
     */
440
    readonly markdownDivCreateNode?: (content: string) => DocumentFragment;
441

442
    /** Callback for providing a custom editor for a cell.
443
     * @group Editing
444
     */
445
    readonly provideEditor?: ProvideEditorCallback<GridCell>;
446
    /**
447
     * Allows coercion of pasted values.
448
     * @group Editing
449
     * @param val The pasted value
450
     * @param cell The cell being pasted into
451
     * @returns `undefined` to accept default behavior or a `GridCell` which should be used to represent the pasted value.
452
     */
453
    readonly coercePasteValue?: (val: string, cell: GridCell) => GridCell | undefined;
454

455
    /**
456
     * Emitted when the grid selection is cleared.
457
     * @group Selection
458
     */
459
    readonly onSelectionCleared?: () => void;
460

461
    /**
462
     * Callback used to override the rendering of any cell.
463
     * @group Drawing
464
     */
465
    readonly drawCell?: DrawCustomCellCallback;
466

467
    /**
468
     * The current selection of the data grid. Contains all selected cells, ranges, rows, and columns.
469
     * Used in conjunction with {@link onGridSelectionChange}
470
     * method to implement a controlled selection.
471
     * @group Selection
472
     */
473
    readonly gridSelection?: GridSelection;
474
    /**
475
     * Emitted whenever the grid selection changes. Specifying
476
     * this function will make the grid’s selection controlled, so
477
     * so you will need to specify {@link gridSelection} as well. See
478
     * the "Controlled Selection" example for details.
479
     *
480
     * @param newSelection The new gridSelection as created by user input.
481
     * @group Selection
482
     */
483
    readonly onGridSelectionChange?: (newSelection: GridSelection) => void;
484
    /**
485
     * Emitted whenever the visible cells change, usually due to scrolling.
486
     * @group Events
487
     * @param range An inclusive range of all visible cells. May include cells obscured by UI elements such
488
     * as headers.
489
     * @param tx The x transform of the cell region.
490
     * @param ty The y transform of the cell region.
491
     * @param extras Contains information about the selected cell and
492
     * any visible freeze columns.
493
     */
494
    readonly onVisibleRegionChanged?: (
495
        range: Rectangle,
496
        tx?: number,
497
        ty?: number,
498
        extras?: {
499
            /** The selected item if visible */
500
            selected?: Item;
501
            /** A selection of visible freeze columns */
502
            freezeRegion?: Rectangle;
503
        }
504
    ) => void;
505

506
    /**
507
     * The primary callback for getting cell data into the data grid.
508
     * @group Data
509
     * @param cell The location of the cell being requested.
510
     * @returns A valid GridCell to be rendered by the Grid.
511
     */
512
    readonly getCellContent: (cell: Item) => GridCell;
513
    /**
514
     * Determines if row selection requires a modifier key to enable multi-selection or not. In auto mode it adapts to
515
     * touch or mouse environments automatically, in multi-mode it always acts as if the multi key (Ctrl) is pressed.
516
     * @group Editing
517
     * @defaultValue `auto`
518
     */
519
    readonly rowSelectionMode?: "auto" | "multi";
520

521
    /**
522
     * Add table headers to copied data.
523
     * @group Editing
524
     * @defaultValue `false`
525
     */
526
    readonly copyHeaders?: boolean;
527

528
    /**
529
     * Determins which keybindings are enabled.
530
     * @group Editing
531
     * @defaultValue is
532

533
            {
534
                selectAll: true,
535
                selectRow: true,
536
                selectColumn: true,
537
                downFill: false,
538
                rightFill: false,
539
                pageUp: false,
540
                pageDown: false,
541
                clear: true,
542
                copy: true,
543
                paste: true,
544
                search: false,
545
                first: true,
546
                last: true,
547
            }
548
     */
549
    readonly keybindings?: Partial<Keybinds>;
550

551
    /**
552
     * Used to fetch large amounts of cells at once. Used for copy/paste, if unset copy will not work.
553
     *
554
     * `getCellsForSelection` is called when the user copies a selection to the clipboard or the data editor needs to
555
     * inspect data which may be outside the curently visible range. It must return a two-dimensional array (an array of
556
     * rows, where each row is an array of cells) of the cells in the selection's rectangle. Note that the rectangle can
557
     * include cells that are not currently visible.
558
     *
559
     * If `true` is passed instead of a callback, the data grid will internally use the `getCellContent` callback to
560
     * provide a basic implementation of `getCellsForSelection`. This can make it easier to light up more data grid
561
     * functionality, but may have negative side effects if your data source is not able to handle being queried for
562
     * data outside the normal window.
563
     *
564
     * If `getCellsForSelection` returns a thunk, the data may be loaded asynchronously, however the data grid may be
565
     * unable to properly react to column spans when performing range selections. Copying large amounts of data out of
566
     * the grid will depend on the performance of the thunk as well.
567
     * @group Data
568
     * @param {Rectangle} selection The range of requested cells
569
     * @param {AbortSignal} abortSignal A signal indicating the requested cells are no longer needed
570
     * @returns A row-major collection of cells or an async thunk which returns a row-major collection.
571
     */
572
    readonly getCellsForSelection?: DataGridSearchProps["getCellsForSelection"] | true;
573

574
    /** The number of columns which should remain in place when scrolling horizontally. The row marker column, if
575
     * enabled is always frozen and is not included in this count.
576
     * @defaultValue 0
577
     * @group Style
578
     */
579
    readonly freezeColumns?: DataGridSearchProps["freezeColumns"];
580

581
    /**
582
     * Controls the drawing of the left hand vertical border of a column. If set to a boolean value it controls all
583
     * borders.
584
     * @defaultValue `true`
585
     * @group Style
586
     */
587
    readonly verticalBorder?: DataGridSearchProps["verticalBorder"] | boolean;
588

589
    /**
590
     * Called when data is pasted into the grid. If left undefined, the `DataEditor` will operate in a
591
     * fallback mode and attempt to paste the text buffer into the current cell assuming the current cell is not
592
     * readonly and can accept the data type. If `onPaste` is set to false or the function returns false, the grid will
593
     * simply ignore paste. If `onPaste` evaluates to true the grid will attempt to split the data by tabs and newlines
594
     * and paste into available cells.
595
     *
596
     * The grid will not attempt to add additional rows if more data is pasted then can fit. In that case it is
597
     * advisable to simply return false from onPaste and handle the paste manually.
598
     * @group Editing
599
     */
600
    readonly onPaste?: ((target: Item, values: readonly (readonly string[])[]) => boolean) | boolean;
601

602
    /**
603
     * The theme used by the data grid to get all color and font information
604
     * @group Style
605
     */
606
    readonly theme?: Partial<Theme>;
607

608
    /**
609
     * An array of custom renderers which can be used to extend the data grid.
610
     * @group Advanced
611
     */
612
    readonly customRenderers?: readonly CustomRenderer<CustomCell<any>>[];
613

614
    readonly scaleToRem?: boolean;
615

616
    /**
617
     * Custom predicate function to decide whether the click event occurred outside the grid
618
     * Especially used when custom editor is opened with the portal and is outside the grid, but there is no possibility
619
     * to add a class "click-outside-ignore"
620
     * If this function is supplied and returns false, the click event is ignored
621
     */
622
    readonly isOutsideClick?: (e: MouseEvent | TouchEvent) => boolean;
623
}
624

625
type ScrollToFn = (
626
    col: number | { amount: number; unit: "cell" | "px" },
627
    row: number | { amount: number; unit: "cell" | "px" },
628
    dir?: "horizontal" | "vertical" | "both",
629
    paddingX?: number,
630
    paddingY?: number,
631
    options?: {
632
        hAlign?: "start" | "center" | "end";
633
        vAlign?: "start" | "center" | "end";
634
    }
635
) => void;
636

637
/** @category DataEditor */
638
export interface DataEditorRef {
639
    /**
640
     * Programatically appends a row.
641
     * @param col The column index to focus in the new row.
642
     * @returns A promise which waits for the append to complete.
643
     */
644
    appendRow: (col: number, openOverlay?: boolean) => Promise<void>;
645
    /**
646
     * Triggers cells to redraw.
647
     */
648
    updateCells: DataGridRef["damage"];
649
    /**
650
     * Gets the screen space bounds of the requested item.
651
     */
652
    getBounds: DataGridRef["getBounds"];
653
    /**
654
     * Triggers the data grid to focus itself or the correct accessibility element.
655
     */
656
    focus: DataGridRef["focus"];
657
    /**
658
     * Generic API for emitting events as if they had been triggered via user interaction.
659
     */
660
    emit: (eventName: EmitEvents) => Promise<void>;
661
    /**
662
     * Scrolls to the desired cell or location in the grid.
663
     */
664
    scrollTo: ScrollToFn;
665
    /**
666
     * Causes the columns in the selection to have their natural size recomputed and re-emitted as a resize event.
667
     */
668
    remeasureColumns: (cols: CompactSelection) => void;
669
}
670

671
const loadingCell: GridCell = {
9✔
672
    kind: GridCellKind.Loading,
673
    allowOverlay: false,
674
};
675

676
const emptyGridSelection: GridSelection = {
9✔
677
    columns: CompactSelection.empty(),
678
    rows: CompactSelection.empty(),
679
    current: undefined,
680
};
681

682
const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorProps> = (p, forwardedRef) => {
9✔
683
    const [gridSelectionInner, setGridSelectionInner] = React.useState<GridSelection>(emptyGridSelection);
649✔
684
    const [overlay, setOverlay] = React.useState<{
648✔
685
        target: Rectangle;
686
        content: GridCell;
687
        theme: Theme;
688
        initialValue: string | undefined;
689
        cell: Item;
690
        highlight: boolean;
691
        forceEditMode: boolean;
692
    }>();
693
    const searchInputRef = React.useRef<HTMLInputElement | null>(null);
648✔
694
    const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
648✔
695
    const [mouseState, setMouseState] = React.useState<MouseState>();
648✔
696
    const scrollRef = React.useRef<HTMLDivElement | null>(null);
648✔
697
    const lastSent = React.useRef<[number, number]>();
648✔
698

699
    const {
700
        rowMarkers = "none",
517✔
701
        rowMarkerWidth: rowMarkerWidthRaw,
702
        imageEditorOverride,
703
        getRowThemeOverride,
704
        markdownDivCreateNode,
705
        width,
706
        height,
707
        columns: columnsIn,
708
        rows,
709
        getCellContent,
710
        onCellClicked,
711
        onCellActivated,
712
        onFinishedEditing,
713
        coercePasteValue,
714
        drawHeader: drawHeaderIn,
715
        onHeaderClicked,
716
        spanRangeBehavior = "default",
648✔
717
        onGroupHeaderClicked,
718
        onCellContextMenu,
719
        className,
720
        onHeaderContextMenu,
721
        getCellsForSelection: getCellsForSelectionIn,
722
        onGroupHeaderContextMenu,
723
        onGroupHeaderRenamed,
724
        onCellEdited,
725
        onCellsEdited,
726
        onSearchResultsChanged: onSearchResultsChangedIn,
727
        searchResults,
728
        onSearchValueChange,
729
        searchValue,
730
        onKeyDown: onKeyDownIn,
731
        onKeyUp: onKeyUpIn,
732
        keybindings: keybindingsIn,
733
        onRowAppended,
734
        onColumnMoved,
735
        validateCell: validateCellIn,
736
        highlightRegions: highlightRegionsIn,
737
        drawCell,
738
        rangeSelect = "rect",
618✔
739
        columnSelect = "multi",
643✔
740
        rowSelect = "multi",
629✔
741
        rangeSelectionBlending = "exclusive",
613✔
742
        columnSelectionBlending = "exclusive",
623✔
743
        rowSelectionBlending = "exclusive",
623✔
744
        onDelete: onDeleteIn,
745
        onDragStart,
746
        onMouseMove,
747
        onPaste,
748
        copyHeaders = false,
648✔
749
        freezeColumns = 0,
648✔
750
        rowSelectionMode = "auto",
648✔
751
        rowMarkerStartIndex = 1,
648✔
752
        rowMarkerTheme,
753
        onHeaderMenuClick,
754
        getGroupDetails,
755
        onSearchClose: onSearchCloseIn,
756
        onItemHovered,
757
        onSelectionCleared,
758
        showSearch: showSearchIn,
759
        onVisibleRegionChanged,
760
        gridSelection: gridSelectionOuter,
761
        onGridSelectionChange,
762
        minColumnWidth: minColumnWidthIn = 50,
648✔
763
        maxColumnWidth: maxColumnWidthIn = 500,
648✔
764
        maxColumnAutoWidth: maxColumnAutoWidthIn,
765
        provideEditor,
766
        trailingRowOptions,
767
        scrollOffsetX,
768
        scrollOffsetY,
769
        verticalBorder,
770
        onDragOverCell,
771
        onDrop,
772
        onColumnResize: onColumnResizeIn,
773
        onColumnResizeEnd: onColumnResizeEndIn,
774
        onColumnResizeStart: onColumnResizeStartIn,
775
        customRenderers: additionalRenderers,
776
        fillHandle,
777
        drawFocusRing,
778
        experimental,
779
        fixedShadowX,
780
        fixedShadowY,
781
        headerIcons,
782
        imageWindowLoader,
783
        initialSize,
784
        isDraggable,
785
        onDragLeave,
786
        onRowMoved,
787
        overscrollX: overscrollXIn,
788
        overscrollY: overscrollYIn,
789
        preventDiagonalScrolling,
790
        rightElement,
791
        rightElementProps,
792
        showMinimap,
793
        smoothScrollX,
794
        smoothScrollY,
795
        scrollToEnd,
796
        scaleToRem = false,
648✔
797
        rowHeight: rowHeightIn = 34,
×
798
        headerHeight: headerHeightIn = 36,
×
799
        groupHeaderHeight: groupHeaderHeightIn = headerHeightIn,
×
800
        theme: themeIn,
801
        isOutsideClick,
802
    } = p;
648✔
803

804
    const minColumnWidth = Math.max(minColumnWidthIn, 20);
648✔
805
    const maxColumnWidth = Math.max(maxColumnWidthIn, minColumnWidth);
648✔
806
    const maxColumnAutoWidth = Math.max(maxColumnAutoWidthIn ?? maxColumnWidth, minColumnWidth);
648!
807

808
    const docStyle = React.useMemo(() => {
648✔
809
        if (typeof window === "undefined") return { fontSize: "16px" };
132!
810
        return window.getComputedStyle(document.documentElement);
132✔
811
    }, []);
812

813
    const fontSizeStr = docStyle.fontSize;
648✔
814

815
    const remSize = React.useMemo(() => Number.parseFloat(fontSizeStr), [fontSizeStr]);
648✔
816

817
    const { rowHeight, headerHeight, groupHeaderHeight, theme, overscrollX, overscrollY } = useRemAdjuster({
648✔
818
        groupHeaderHeight: groupHeaderHeightIn,
819
        headerHeight: headerHeightIn,
820
        overscrollX: overscrollXIn,
821
        overscrollY: overscrollYIn,
822
        remSize,
823
        rowHeight: rowHeightIn,
824
        scaleToRem,
825
        theme: themeIn,
826
    });
827

828
    const keybindings = React.useMemo(() => {
648✔
829
        return keybindingsIn === undefined
132✔
830
            ? keybindingDefaults
831
            : {
832
                  ...keybindingDefaults,
833
                  ...keybindingsIn,
834
              };
835
    }, [keybindingsIn]);
836

837
    const rowMarkerWidth = rowMarkerWidthRaw ?? (rows > 10_000 ? 48 : rows > 1000 ? 44 : rows > 100 ? 36 : 32);
648!
838
    const hasRowMarkers = rowMarkers !== "none";
648✔
839
    const rowMarkerOffset = hasRowMarkers ? 1 : 0;
648✔
840
    const showTrailingBlankRow = onRowAppended !== undefined;
648✔
841
    const lastRowSticky = trailingRowOptions?.sticky === true;
648!
842

843
    const [showSearchInner, setShowSearchInner] = React.useState(false);
648✔
844
    const showSearch = showSearchIn ?? showSearchInner;
648✔
845

846
    const onSearchClose = React.useCallback(() => {
648✔
847
        if (onSearchCloseIn !== undefined) {
848
            onSearchCloseIn();
2✔
849
        } else {
850
            setShowSearchInner(false);
×
851
        }
852
    }, [onSearchCloseIn]);
853

854
    const gridSelectionOuterMangled: GridSelection | undefined = React.useMemo((): GridSelection | undefined => {
648✔
855
        return gridSelectionOuter === undefined ? undefined : shiftSelection(gridSelectionOuter, rowMarkerOffset);
255✔
856
    }, [gridSelectionOuter, rowMarkerOffset]);
857
    const gridSelection = gridSelectionOuterMangled ?? gridSelectionInner;
648✔
858

859
    const abortControllerRef = React.useRef(new AbortController());
648✔
860
    React.useEffect(() => {
648✔
861
        return () => {
132✔
862
            // eslint-disable-next-line react-hooks/exhaustive-deps
863
            abortControllerRef?.current.abort();
132!
864
        };
865
    }, []);
866

867
    const [getCellsForSelection, getCellsForSeletionDirect] = useCellsForSelection(
648✔
868
        getCellsForSelectionIn,
869
        getCellContent,
870
        rowMarkerOffset,
871
        abortControllerRef.current,
872
        rows
873
    );
874

875
    const validateCell = React.useCallback<NonNullable<typeof validateCellIn>>(
648✔
876
        (cell, newValue, prevValue) => {
877
            if (validateCellIn === undefined) return true;
16✔
878
            const item: Item = [cell[0] - rowMarkerOffset, cell[1]];
1✔
879
            return validateCellIn?.(item, newValue, prevValue);
1!
880
        },
881
        [rowMarkerOffset, validateCellIn]
882
    );
883

884
    const expectedExternalGridSelection = React.useRef<GridSelection | undefined>(gridSelectionOuter);
648✔
885
    const setGridSelection = React.useCallback(
648✔
886
        (newVal: GridSelection, expand: boolean): void => {
887
            if (expand) {
888
                newVal = expandSelection(
124✔
889
                    newVal,
890
                    getCellsForSelection,
891
                    rowMarkerOffset,
892
                    spanRangeBehavior,
893
                    abortControllerRef.current
894
                );
895
            }
896
            if (onGridSelectionChange !== undefined) {
897
                expectedExternalGridSelection.current = shiftSelection(newVal, -rowMarkerOffset);
127✔
898
                onGridSelectionChange(expectedExternalGridSelection.current);
127✔
899
            } else {
900
                setGridSelectionInner(newVal);
39✔
901
            }
902
        },
903
        [onGridSelectionChange, getCellsForSelection, rowMarkerOffset, spanRangeBehavior]
904
    );
905

906
    const onColumnResize = whenDefined(
648✔
907
        onColumnResizeIn,
908
        React.useCallback<NonNullable<typeof onColumnResizeIn>>(
909
            (_, w, ind, wg) => {
910
                onColumnResizeIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
11!
911
            },
912
            [onColumnResizeIn, rowMarkerOffset, columnsIn]
913
        )
914
    );
915

916
    const onColumnResizeEnd = whenDefined(
648✔
917
        onColumnResizeEndIn,
918
        React.useCallback<NonNullable<typeof onColumnResizeEndIn>>(
919
            (_, w, ind, wg) => {
920
                onColumnResizeEndIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
2!
921
            },
922
            [onColumnResizeEndIn, rowMarkerOffset, columnsIn]
923
        )
924
    );
925

926
    const onColumnResizeStart = whenDefined(
648✔
927
        onColumnResizeStartIn,
928
        React.useCallback<NonNullable<typeof onColumnResizeStartIn>>(
929
            (_, w, ind, wg) => {
930
                onColumnResizeStartIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
×
931
            },
932
            [onColumnResizeStartIn, rowMarkerOffset, columnsIn]
933
        )
934
    );
935

936
    const drawHeader = whenDefined(
648✔
937
        drawHeaderIn,
938
        React.useCallback<NonNullable<typeof drawHeaderIn>>(
939
            args => {
940
                return drawHeaderIn?.({ ...args, columnIndex: args.columnIndex - rowMarkerOffset }) ?? false;
×
941
            },
942
            [drawHeaderIn, rowMarkerOffset]
943
        )
944
    );
945

946
    const onDelete = React.useCallback<NonNullable<DataEditorProps["onDelete"]>>(
648✔
947
        sel => {
948
            if (onDeleteIn !== undefined) {
949
                const result = onDeleteIn(shiftSelection(sel, -rowMarkerOffset));
5✔
950
                if (typeof result === "boolean") {
951
                    return result;
×
952
                }
953
                return shiftSelection(result, rowMarkerOffset);
5✔
954
            }
955
            return true;
3✔
956
        },
957
        [onDeleteIn, rowMarkerOffset]
958
    );
959

960
    const [setCurrent, setSelectedRows, setSelectedColumns] = useSelectionBehavior(
648✔
961
        gridSelection,
962
        setGridSelection,
963
        rangeSelectionBlending,
964
        columnSelectionBlending,
965
        rowSelectionBlending,
966
        rangeSelect
967
    );
968

969
    const mergedTheme = React.useMemo(() => {
648✔
970
        return { ...getDataEditorTheme(), ...theme };
132✔
971
    }, [theme]);
972

973
    const [clientSize, setClientSize] = React.useState<readonly [number, number, number]>([10, 10, 0]);
648✔
974

975
    const getCellRenderer: <T extends InnerGridCell>(cell: T) => CellRenderer<T> | undefined = React.useCallback(
648✔
976
        <T extends InnerGridCell>(cell: T) => {
977
            if (cell.kind !== GridCellKind.Custom) {
978
                return CellRenderers[cell.kind] as unknown as CellRenderer<T>;
129,865✔
979
            }
980
            return additionalRenderers?.find(x => x.isMatch(cell)) as CellRenderer<T>;
3,730!
981
        },
982
        [additionalRenderers]
983
    );
984

985
    const columns = useColumnSizer(
648✔
986
        columnsIn,
987
        rows,
988
        getCellsForSeletionDirect,
989
        clientSize[0] - (rowMarkerOffset === 0 ? 0 : rowMarkerWidth) - clientSize[2],
648✔
990
        minColumnWidth,
991
        maxColumnAutoWidth,
992
        mergedTheme,
993
        getCellRenderer,
994
        abortControllerRef.current
995
    );
996

997
    const enableGroups = React.useMemo(() => {
648✔
998
        return columns.some(c => c.group !== undefined);
1,389✔
999
    }, [columns]);
1000

1001
    const totalHeaderHeight = enableGroups ? headerHeight + groupHeaderHeight : headerHeight;
648✔
1002

1003
    const numSelectedRows = gridSelection.rows.length;
648✔
1004
    const rowMarkerHeader =
1005
        rowMarkers === "none"
648✔
1006
            ? ""
1007
            : numSelectedRows === 0
131✔
1008
            ? headerCellUnheckedMarker
1009
            : numSelectedRows === rows
44✔
1010
            ? headerCellCheckedMarker
1011
            : headerCellIndeterminateMarker;
1012

1013
    const mangledCols = React.useMemo(() => {
648✔
1014
        if (rowMarkers === "none") return columns;
146✔
1015
        return [
45✔
1016
            {
1017
                title: rowMarkerHeader,
1018
                width: rowMarkerWidth,
1019
                icon: undefined,
1020
                hasMenu: false,
1021
                style: "normal" as const,
1022
                themeOverride: rowMarkerTheme,
1023
            },
1024
            ...columns,
1025
        ];
1026
    }, [columns, rowMarkerWidth, rowMarkers, rowMarkerHeader, rowMarkerTheme]);
1027

1028
    const [visibleRegionY, visibleRegionTy] = React.useMemo(() => {
648✔
1029
        return [
132✔
1030
            scrollOffsetY !== undefined && typeof rowHeight === "number" ? Math.floor(scrollOffsetY / rowHeight) : 0,
264!
1031
            scrollOffsetY !== undefined && typeof rowHeight === "number" ? -(scrollOffsetY % rowHeight) : 0,
264!
1032
        ];
1033
    }, [scrollOffsetY, rowHeight]);
1034

1035
    type VisibleRegion = Rectangle & {
1036
        /** value in px */
1037
        tx?: number;
1038
        /** value in px */
1039
        ty?: number;
1040
        extras?: {
1041
            selected?: Item;
1042
            freezeRegion?: Rectangle;
1043
        };
1044
    };
1045

1046
    const visibleRegionRef = React.useRef<VisibleRegion>({
648✔
1047
        height: 1,
1048
        width: 1,
1049
        x: 0,
1050
        y: 0,
1051
    });
1052
    const visibleRegionInput = React.useMemo<VisibleRegion>(
648✔
1053
        () => ({
132✔
1054
            x: visibleRegionRef.current.x,
1055
            y: visibleRegionY,
1056
            width: visibleRegionRef.current.width ?? 1,
396!
1057
            height: visibleRegionRef.current.height ?? 1,
396!
1058
            // tx: 'TODO',
1059
            ty: visibleRegionTy,
1060
        }),
1061
        [visibleRegionTy, visibleRegionY]
1062
    );
1063

1064
    const hasJustScrolled = React.useRef(false);
648✔
1065

1066
    const [visibleRegion, setVisibleRegion, empty] = useStateWithReactiveInput<VisibleRegion>(visibleRegionInput);
648✔
1067

1068
    const vScrollReady = (visibleRegion.height ?? 1) > 1;
648!
1069
    React.useLayoutEffect(() => {
648✔
1070
        if (scrollOffsetY !== undefined && scrollRef.current !== null && vScrollReady) {
264!
1071
            if (scrollRef.current.scrollTop === scrollOffsetY) return;
×
1072
            scrollRef.current.scrollTop = scrollOffsetY;
×
1073
            if (scrollRef.current.scrollTop !== scrollOffsetY) {
1074
                empty();
×
1075
            }
1076
            hasJustScrolled.current = true;
×
1077
        }
1078
    }, [scrollOffsetY, vScrollReady, empty]);
1079

1080
    const hScrollReady = (visibleRegion.width ?? 1) > 1;
648!
1081
    React.useLayoutEffect(() => {
648✔
1082
        if (scrollOffsetX !== undefined && scrollRef.current !== null && hScrollReady) {
264!
1083
            if (scrollRef.current.scrollLeft === scrollOffsetX) return;
×
1084
            scrollRef.current.scrollLeft = scrollOffsetX;
×
1085
            if (scrollRef.current.scrollLeft !== scrollOffsetX) {
1086
                empty();
×
1087
            }
1088
            hasJustScrolled.current = true;
×
1089
        }
1090
    }, [scrollOffsetX, hScrollReady, empty]);
1091

1092
    const cellXOffset = visibleRegion.x + rowMarkerOffset;
648✔
1093
    const cellYOffset = visibleRegion.y;
648✔
1094

1095
    const gridRef = React.useRef<DataGridRef | null>(null);
648✔
1096

1097
    const focus = React.useCallback((immediate?: boolean) => {
648✔
1098
        if (immediate === true) {
1099
            gridRef.current?.focus();
7!
1100
        } else {
1101
            window.requestAnimationFrame(() => {
113✔
1102
                gridRef.current?.focus();
103✔
1103
            });
1104
        }
1105
    }, []);
1106

1107
    const mangledRows = showTrailingBlankRow ? rows + 1 : rows;
648!
1108

1109
    const mangledOnCellsEdited = React.useCallback<NonNullable<typeof onCellsEdited>>(
648✔
1110
        (items: readonly EditListItem[]) => {
1111
            const mangledItems =
1112
                rowMarkerOffset === 0
26✔
1113
                    ? items
1114
                    : items.map(x => ({
29✔
1115
                          ...x,
1116
                          location: [x.location[0] - rowMarkerOffset, x.location[1]] as const,
1117
                      }));
1118
            const r = onCellsEdited?.(mangledItems);
26✔
1119

1120
            if (r !== true) {
1121
                for (const i of mangledItems) onCellEdited?.(i.location, i.value);
1,103✔
1122
            }
1123

1124
            return r;
26✔
1125
        },
1126
        [onCellEdited, onCellsEdited, rowMarkerOffset]
1127
    );
1128

1129
    const highlightRegions = React.useMemo(() => {
648✔
1130
        if (highlightRegionsIn === undefined) return undefined;
132✔
1131
        if (rowMarkerOffset === 0) return highlightRegionsIn;
1✔
1132

1133
        return highlightRegionsIn
×
1134
            .map(r => {
1135
                const maxWidth = mangledCols.length - r.range.x - rowMarkerOffset;
×
1136
                if (maxWidth <= 0) return undefined;
×
1137
                return {
×
1138
                    color: r.color,
1139
                    range: {
1140
                        ...r.range,
1141
                        x: r.range.x + rowMarkerOffset,
1142
                        width: Math.min(maxWidth, r.range.width),
1143
                    },
1144
                    style: r.style,
1145
                };
1146
            })
1147
            .filter(x => x !== undefined) as typeof highlightRegionsIn;
×
1148
    }, [highlightRegionsIn, mangledCols.length, rowMarkerOffset]);
1149

1150
    const mangledColsRef = React.useRef(mangledCols);
648✔
1151
    mangledColsRef.current = mangledCols;
648✔
1152
    const getMangledCellContent = React.useCallback(
648✔
1153
        ([col, row]: Item, forceStrict: boolean = false): InnerGridCell => {
109,964✔
1154
            const isTrailing = showTrailingBlankRow && row === mangledRows - 1;
130,745✔
1155
            const isRowMarkerCol = col === 0 && hasRowMarkers;
130,745✔
1156
            if (isRowMarkerCol) {
1157
                if (isTrailing) {
1158
                    return loadingCell;
98✔
1159
                }
1160
                return {
2,085✔
1161
                    kind: InnerGridCellKind.Marker,
1162
                    allowOverlay: false,
1163
                    checked: gridSelection?.rows.hasIndex(row) === true,
6,255!
1164
                    markerKind: rowMarkers === "clickable-number" ? "number" : rowMarkers,
2,085!
1165
                    row: rowMarkerStartIndex + row,
1166
                    drawHandle: onRowMoved !== undefined,
1167
                    cursor: rowMarkers === "clickable-number" ? "pointer" : undefined,
2,085!
1168
                };
1169
            } else if (isTrailing) {
1170
                //If the grid is empty, we will return text
1171
                const isFirst = col === rowMarkerOffset;
3,647✔
1172

1173
                const maybeFirstColumnHint = isFirst ? trailingRowOptions?.hint ?? "" : "";
3,647!
1174
                const c = mangledColsRef.current[col];
3,647✔
1175

1176
                if (c?.trailingRowOptions?.disabled === true) {
21,882!
1177
                    return loadingCell;
×
1178
                } else {
1179
                    const hint = c?.trailingRowOptions?.hint ?? maybeFirstColumnHint;
3,647!
1180
                    const icon = c?.trailingRowOptions?.addIcon ?? trailingRowOptions?.addIcon;
3,647!
1181
                    return {
3,647✔
1182
                        kind: InnerGridCellKind.NewRow,
1183
                        hint,
1184
                        allowOverlay: false,
1185
                        icon,
1186
                    };
1187
                }
1188
            } else {
1189
                const outerCol = col - rowMarkerOffset;
124,915✔
1190
                if (forceStrict || experimental?.strict === true) {
541,691✔
1191
                    const vr = visibleRegionRef.current;
21,967✔
1192
                    const isOutsideMainArea =
1193
                        vr.x > outerCol || outerCol > vr.x + vr.width || vr.y > row || row > vr.y + vr.height;
21,967✔
1194
                    const isSelected = outerCol === vr.extras?.selected?.[0] && row === vr.extras?.selected[1];
21,967!
1195
                    const isOutsideFreezeArea =
1196
                        vr.extras?.freezeRegion === undefined ||
21,967!
1197
                        vr.extras.freezeRegion.x > outerCol ||
1198
                        outerCol > vr.extras.freezeRegion.x + vr.extras.freezeRegion.width ||
1199
                        vr.extras.freezeRegion.y > row ||
1200
                        row > vr.extras.freezeRegion.y + vr.extras.freezeRegion.height;
1201
                    if (isOutsideMainArea && !isSelected && isOutsideFreezeArea) {
21,967!
1202
                        return {
×
1203
                            kind: GridCellKind.Loading,
1204
                            allowOverlay: false,
1205
                        };
1206
                    }
1207
                }
1208
                let result = getCellContent([outerCol, row]);
124,915✔
1209
                if (rowMarkerOffset !== 0 && result.span !== undefined) {
146,897✔
1210
                    result = {
×
1211
                        ...result,
1212
                        span: [result.span[0] + rowMarkerOffset, result.span[1] + rowMarkerOffset],
1213
                    };
1214
                }
1215
                return result;
124,915✔
1216
            }
1217
        },
1218
        [
1219
            showTrailingBlankRow,
1220
            mangledRows,
1221
            hasRowMarkers,
1222
            gridSelection?.rows,
1,944!
1223
            onRowMoved,
1224
            rowMarkers,
1225
            rowMarkerOffset,
1226
            trailingRowOptions?.hint,
1,944!
1227
            trailingRowOptions?.addIcon,
1,944!
1228
            experimental?.strict,
1,944✔
1229
            getCellContent,
1230
            rowMarkerStartIndex,
1231
        ]
1232
    );
1233

1234
    const mangledGetGroupDetails = React.useCallback<NonNullable<DataEditorProps["getGroupDetails"]>>(
648✔
1235
        group => {
1236
            let result = getGroupDetails?.(group) ?? { name: group };
7,799✔
1237
            if (onGroupHeaderRenamed !== undefined && group !== "") {
7,879✔
1238
                result = {
80✔
1239
                    icon: result.icon,
1240
                    name: result.name,
1241
                    overrideTheme: result.overrideTheme,
1242
                    actions: [
1243
                        ...(result.actions ?? []),
240!
1244
                        {
1245
                            title: "Rename",
1246
                            icon: "renameIcon",
1247
                            onClick: e =>
1248
                                setRenameGroup({
2✔
1249
                                    group: result.name,
1250
                                    bounds: e.bounds,
1251
                                }),
1252
                        },
1253
                    ],
1254
                };
1255
            }
1256
            return result;
7,799✔
1257
        },
1258
        [getGroupDetails, onGroupHeaderRenamed]
1259
    );
1260

1261
    const setOverlaySimple = React.useCallback(
648✔
1262
        (val: Omit<NonNullable<typeof overlay>, "theme">) => {
1263
            const [col, row] = val.cell;
16✔
1264
            const column = mangledCols[col];
16✔
1265
            const groupTheme =
1266
                column?.group !== undefined ? mangledGetGroupDetails(column.group)?.overrideTheme : undefined;
16!
1267
            const colTheme = column?.themeOverride;
16!
1268
            const rowTheme = getRowThemeOverride?.(row);
16!
1269

1270
            setOverlay({
16✔
1271
                ...val,
1272
                theme: { ...mergedTheme, ...groupTheme, ...colTheme, ...rowTheme, ...val.content.themeOverride },
1273
            });
1274
        },
1275
        [getRowThemeOverride, mangledCols, mangledGetGroupDetails, mergedTheme]
1276
    );
1277

1278
    const reselect = React.useCallback(
648✔
1279
        (bounds: Rectangle, fromKeyboard: boolean, initialValue?: string) => {
1280
            if (gridSelection.current === undefined) return;
16!
1281

1282
            const [col, row] = gridSelection.current.cell;
16✔
1283
            const c = getMangledCellContent([col, row]);
16✔
1284
            if (c.kind !== GridCellKind.Boolean && c.allowOverlay) {
31✔
1285
                let content = c;
15✔
1286
                if (initialValue !== undefined) {
1287
                    switch (content.kind) {
1288
                        case GridCellKind.Number: {
1289
                            const d = maybe(() => (initialValue === "-" ? -0 : Number.parseFloat(initialValue)), 0);
×
1290
                            content = {
×
1291
                                ...content,
1292
                                data: Number.isNaN(d) ? 0 : d,
×
1293
                            };
1294
                            break;
×
1295
                        }
1296
                        case GridCellKind.Text:
21✔
1297
                        case GridCellKind.Markdown:
1298
                        case GridCellKind.Uri:
1299
                            content = {
7✔
1300
                                ...content,
1301
                                data: initialValue,
1302
                            };
1303
                            break;
7✔
1304
                    }
1305
                }
1306

1307
                setOverlaySimple({
15✔
1308
                    target: bounds,
1309
                    content,
1310
                    initialValue,
1311
                    cell: [col, row],
1312
                    highlight: initialValue === undefined,
1313
                    forceEditMode: initialValue !== undefined,
1314
                });
1315
            } else if (c.kind === GridCellKind.Boolean && fromKeyboard && c.readonly !== true) {
3✔
1316
                mangledOnCellsEdited([
×
1317
                    {
1318
                        location: gridSelection.current.cell,
1319
                        value: {
1320
                            ...c,
1321
                            data: toggleBoolean(c.data),
1322
                        },
1323
                    },
1324
                ]);
1325
                gridRef.current?.damage([{ cell: gridSelection.current.cell }]);
×
1326
            }
1327
        },
1328
        [getMangledCellContent, gridSelection, mangledOnCellsEdited, setOverlaySimple]
1329
    );
1330

1331
    const focusOnRowFromTrailingBlankRow = React.useCallback(
648✔
1332
        (col: number, row: number) => {
1333
            const bounds = gridRef.current?.getBounds(col, row);
1!
1334
            if (bounds === undefined || scrollRef.current === null) {
2✔
1335
                return;
×
1336
            }
1337

1338
            const content = getMangledCellContent([col, row]);
1✔
1339
            if (!content.allowOverlay) {
1340
                return;
×
1341
            }
1342

1343
            setOverlaySimple({
1✔
1344
                target: bounds,
1345
                content,
1346
                initialValue: undefined,
1347
                highlight: true,
1348
                cell: [col, row],
1349
                forceEditMode: true,
1350
            });
1351
        },
1352
        [getMangledCellContent, setOverlaySimple]
1353
    );
1354

1355
    const scrollTo = React.useCallback<ScrollToFn>(
648✔
1356
        (col, row, dir = "both", paddingX = 0, paddingY = 0, options = undefined): void => {
161✔
1357
            if (scrollRef.current !== null) {
1358
                const grid = gridRef.current;
43✔
1359
                const canvas = canvasRef.current;
43✔
1360

1361
                const trueCol = typeof col !== "number" ? (col.unit === "cell" ? col.amount : undefined) : col;
43!
1362
                const trueRow = typeof row !== "number" ? (row.unit === "cell" ? row.amount : undefined) : row;
43!
1363
                const desiredX = typeof col !== "number" && col.unit === "px" ? col.amount : undefined;
43!
1364
                const desiredY = typeof row !== "number" && row.unit === "px" ? row.amount : undefined;
43✔
1365
                if (grid !== null && canvas !== null) {
86✔
1366
                    let targetRect: Rectangle = {
43✔
1367
                        x: 0,
1368
                        y: 0,
1369
                        width: 0,
1370
                        height: 0,
1371
                    };
1372

1373
                    let scrollX = 0;
43✔
1374
                    let scrollY = 0;
43✔
1375

1376
                    if (trueCol !== undefined || trueRow !== undefined) {
43!
1377
                        targetRect = grid.getBounds((trueCol ?? 0) + rowMarkerOffset, trueRow ?? 0) ?? targetRect;
43!
1378
                        if (targetRect.width === 0 || targetRect.height === 0) return;
43!
1379
                    }
1380

1381
                    const scrollBounds = canvas.getBoundingClientRect();
43✔
1382
                    const scale = scrollBounds.width / canvas.offsetWidth;
43✔
1383

1384
                    if (desiredX !== undefined) {
1385
                        targetRect = {
×
1386
                            ...targetRect,
1387
                            x: desiredX - scrollBounds.left - scrollRef.current.scrollLeft,
1388
                            width: 1,
1389
                        };
1390
                    }
1391
                    if (desiredY !== undefined) {
1392
                        targetRect = {
4✔
1393
                            ...targetRect,
1394
                            y: desiredY + scrollBounds.top - scrollRef.current.scrollTop,
1395
                            height: 1,
1396
                        };
1397
                    }
1398

1399
                    if (targetRect !== undefined) {
1400
                        const bounds = {
43✔
1401
                            x: targetRect.x - paddingX,
1402
                            y: targetRect.y - paddingY,
1403
                            width: targetRect.width + 2 * paddingX,
1404
                            height: targetRect.height + 2 * paddingY,
1405
                        };
1406

1407
                        let frozenWidth = 0;
43✔
1408
                        for (let i = 0; i < freezeColumns; i++) {
43✔
1409
                            frozenWidth += columns[i].width;
×
1410
                        }
1411
                        let trailingRowHeight = 0;
43✔
1412
                        if (lastRowSticky) {
1413
                            trailingRowHeight = typeof rowHeight === "number" ? rowHeight : rowHeight(rows);
43!
1414
                        }
1415

1416
                        // scrollBounds is already scaled
1417
                        let sLeft = frozenWidth * scale + scrollBounds.left + rowMarkerOffset * rowMarkerWidth * scale;
43✔
1418
                        let sRight = scrollBounds.right;
43✔
1419
                        let sTop = scrollBounds.top + totalHeaderHeight * scale;
43✔
1420
                        let sBottom = scrollBounds.bottom - trailingRowHeight * scale;
43✔
1421

1422
                        const minx = targetRect.width + paddingX * 2;
43✔
1423
                        switch (options?.hAlign) {
129✔
1424
                            case "start":
×
1425
                                sRight = sLeft + minx;
×
1426
                                break;
×
1427
                            case "end":
1428
                                sLeft = sRight - minx;
×
1429
                                break;
×
1430
                            case "center":
1431
                                sLeft = Math.floor((sLeft + sRight) / 2) - minx / 2;
×
1432
                                sRight = sLeft + minx;
×
1433
                                break;
×
1434
                        }
1435

1436
                        const miny = targetRect.height + paddingY * 2;
43✔
1437
                        switch (options?.vAlign) {
129✔
1438
                            case "start":
3✔
1439
                                sBottom = sTop + miny;
1✔
1440
                                break;
1✔
1441
                            case "end":
1442
                                sTop = sBottom - miny;
1✔
1443
                                break;
1✔
1444
                            case "center":
1445
                                sTop = Math.floor((sTop + sBottom) / 2) - miny / 2;
1✔
1446
                                sBottom = sTop + miny;
1✔
1447
                                break;
1✔
1448
                        }
1449

1450
                        if (sLeft > bounds.x) {
1451
                            scrollX = bounds.x - sLeft;
×
1452
                        } else if (sRight < bounds.x + bounds.width) {
1453
                            scrollX = bounds.x + bounds.width - sRight;
4✔
1454
                        }
1455

1456
                        if (sTop > bounds.y) {
1457
                            scrollY = bounds.y - sTop;
×
1458
                        } else if (sBottom < bounds.y + bounds.height) {
1459
                            scrollY = bounds.y + bounds.height - sBottom;
12✔
1460
                        }
1461

1462
                        if (dir === "vertical" || (typeof col === "number" && col < freezeColumns)) {
125✔
1463
                            scrollX = 0;
2✔
1464
                        } else if (dir === "horizontal") {
1465
                            scrollY = 0;
6✔
1466
                        }
1467

1468
                        if (scrollX !== 0 || scrollY !== 0) {
82✔
1469
                            // Remove scaling as scrollTo method is unaffected by transform scale.
1470
                            if (scale !== 1) {
1471
                                scrollX /= scale;
×
1472
                                scrollY /= scale;
×
1473
                            }
1474
                            scrollRef.current.scrollTo(
15✔
1475
                                scrollX + scrollRef.current.scrollLeft,
1476
                                scrollY + scrollRef.current.scrollTop
1477
                            );
1478
                        }
1479
                    }
1480
                }
1481
            }
1482
        },
1483
        [rowMarkerOffset, rowMarkerWidth, totalHeaderHeight, lastRowSticky, freezeColumns, columns, rowHeight, rows]
1484
    );
1485

1486
    const focusCallback = React.useRef(focusOnRowFromTrailingBlankRow);
648✔
1487
    const getCellContentRef = React.useRef(getCellContent);
648✔
1488
    const rowsRef = React.useRef(rows);
648✔
1489
    focusCallback.current = focusOnRowFromTrailingBlankRow;
648✔
1490
    getCellContentRef.current = getCellContent;
648✔
1491
    rowsRef.current = rows;
648✔
1492
    const appendRow = React.useCallback(
648✔
1493
        async (col: number, openOverlay: boolean = true): Promise<void> => {
1✔
1494
            const c = mangledCols[col];
1✔
1495
            if (c?.trailingRowOptions?.disabled === true) {
6!
1496
                return;
×
1497
            }
1498
            const appendResult = onRowAppended?.();
1!
1499

1500
            let r: "top" | "bottom" | number | undefined = undefined;
1✔
1501
            let bottom = true;
1✔
1502
            if (appendResult !== undefined) {
1503
                r = await appendResult;
×
1504
                if (r === "top") bottom = false;
×
1505
                if (typeof r === "number") bottom = false;
×
1506
            }
1507

1508
            let backoff = 0;
1✔
1509
            const doFocus = () => {
1✔
1510
                if (rowsRef.current <= rows) {
1511
                    if (backoff < 500) {
1512
                        window.setTimeout(doFocus, backoff);
1✔
1513
                    }
1514
                    backoff = 50 + backoff * 2;
1✔
1515
                    return;
1✔
1516
                }
1517

1518
                const row = typeof r === "number" ? r : bottom ? rows : 0;
1!
1519
                scrollTo(col - rowMarkerOffset, row);
1✔
1520
                setCurrent(
1✔
1521
                    {
1522
                        cell: [col, row],
1523
                        range: {
1524
                            x: col,
1525
                            y: row,
1526
                            width: 1,
1527
                            height: 1,
1528
                        },
1529
                    },
1530
                    false,
1531
                    false,
1532
                    "edit"
1533
                );
1534

1535
                const cell = getCellContentRef.current([col - rowMarkerOffset, row]);
1✔
1536
                if (cell.allowOverlay && isReadWriteCell(cell) && cell.readonly !== true && openOverlay) {
4✔
1537
                    // wait for scroll to have a chance to process
1538
                    window.setTimeout(() => {
1✔
1539
                        focusCallback.current(col, row);
1✔
1540
                    }, 0);
1541
                }
1542
            };
1543
            // Queue up to allow the consumer to react to the event and let us check if they did
1544
            doFocus();
1✔
1545
        },
1546
        [mangledCols, onRowAppended, rowMarkerOffset, rows, scrollTo, setCurrent]
1547
    );
1548

1549
    const getCustomNewRowTargetColumn = React.useCallback(
648✔
1550
        (col: number): number | undefined => {
1551
            const customTargetColumn =
1552
                columns[col]?.trailingRowOptions?.targetColumn ?? trailingRowOptions?.targetColumn;
1!
1553

1554
            if (typeof customTargetColumn === "number") {
1555
                const customTargetOffset = hasRowMarkers ? 1 : 0;
×
1556
                return customTargetColumn + customTargetOffset;
×
1557
            }
1558

1559
            if (typeof customTargetColumn === "object") {
1560
                const maybeIndex = columnsIn.indexOf(customTargetColumn);
×
1561
                if (maybeIndex >= 0) {
1562
                    const customTargetOffset = hasRowMarkers ? 1 : 0;
×
1563
                    return maybeIndex + customTargetOffset;
×
1564
                }
1565
            }
1566

1567
            return undefined;
1✔
1568
        },
1569
        [columns, columnsIn, hasRowMarkers, trailingRowOptions?.targetColumn]
1,944!
1570
    );
1571

1572
    const lastSelectedRowRef = React.useRef<number>();
648✔
1573
    const lastSelectedColRef = React.useRef<number>();
648✔
1574

1575
    const themeForCell = React.useCallback(
648✔
1576
        (cell: InnerGridCell, pos: Item): Theme => {
1577
            const [col, row] = pos;
20✔
1578
            return {
20✔
1579
                ...mergedTheme,
1580
                ...mangledCols[col]?.themeOverride,
60!
1581
                ...getRowThemeOverride?.(row),
60!
1582
                ...cell.themeOverride,
1583
            };
1584
        },
1585
        [getRowThemeOverride, mangledCols, mergedTheme]
1586
    );
1587

1588
    const handleSelect = React.useCallback(
648✔
1589
        (args: GridMouseEventArgs) => {
1590
            const isMultiKey = browserIsOSX.value ? args.metaKey : args.ctrlKey;
113!
1591
            const isMultiRow = isMultiKey && rowSelect === "multi";
113✔
1592
            const isMultiCol = isMultiKey && columnSelect === "multi";
113✔
1593
            const [col, row] = args.location;
113✔
1594
            const selectedColumns = gridSelection.columns;
113✔
1595
            const selectedRows = gridSelection.rows;
113✔
1596
            const [cellCol, cellRow] = gridSelection.current?.cell ?? [];
113✔
1597
            // eslint-disable-next-line unicorn/prefer-switch
1598
            if (args.kind === "cell") {
1599
                lastSelectedColRef.current = undefined;
97✔
1600

1601
                lastMouseSelectLocation.current = [col, row];
97✔
1602

1603
                if (col === 0 && hasRowMarkers) {
113✔
1604
                    if (
15✔
1605
                        (showTrailingBlankRow === true && row === rows) ||
59✔
1606
                        rowMarkers === "number" ||
1607
                        rowSelect === "none"
1608
                    )
1609
                        return;
1✔
1610

1611
                    const markerCell = getMangledCellContent(args.location);
14✔
1612
                    if (markerCell.kind !== InnerGridCellKind.Marker) {
1613
                        return;
×
1614
                    }
1615

1616
                    if (onRowMoved !== undefined) {
1617
                        const renderer = getCellRenderer(markerCell);
×
1618
                        assert(renderer?.kind === InnerGridCellKind.Marker);
×
1619
                        const postClick = renderer?.onClick?.({
×
1620
                            ...args,
1621
                            cell: markerCell,
1622
                            posX: args.localEventX,
1623
                            posY: args.localEventY,
1624
                            bounds: args.bounds,
1625
                            theme: themeForCell(markerCell, args.location),
1626
                            preventDefault: () => undefined,
×
1627
                        }) as MarkerCell | undefined;
1628
                        if (postClick === undefined || postClick.checked === markerCell.checked) return;
×
1629
                    }
1630

1631
                    setOverlay(undefined);
14✔
1632
                    focus();
14✔
1633
                    const isSelected = selectedRows.hasIndex(row);
14✔
1634

1635
                    const lastHighlighted = lastSelectedRowRef.current;
14✔
1636
                    if (
1637
                        rowSelect === "multi" &&
31✔
1638
                        (args.shiftKey || args.isLongTouch === true) &&
1639
                        lastHighlighted !== undefined &&
1640
                        selectedRows.hasIndex(lastHighlighted)
1641
                    ) {
1642
                        const newSlice: Slice = [Math.min(lastHighlighted, row), Math.max(lastHighlighted, row) + 1];
1✔
1643

1644
                        if (isMultiRow || rowSelectionMode === "multi") {
2✔
1645
                            setSelectedRows(undefined, newSlice, true);
×
1646
                        } else {
1647
                            setSelectedRows(CompactSelection.fromSingleSelection(newSlice), undefined, isMultiRow);
1✔
1648
                        }
1649
                    } else if (isMultiRow || args.isTouch || rowSelectionMode === "multi") {
33✔
1650
                        if (isSelected) {
1651
                            setSelectedRows(selectedRows.remove(row), undefined, true);
1✔
1652
                        } else {
1653
                            setSelectedRows(undefined, row, true);
2✔
1654
                            lastSelectedRowRef.current = row;
2✔
1655
                        }
1656
                    } else if (isSelected && selectedRows.length === 1) {
11✔
1657
                        setSelectedRows(CompactSelection.empty(), undefined, isMultiKey);
1✔
1658
                    } else {
1659
                        setSelectedRows(CompactSelection.fromSingleSelection(row), undefined, isMultiKey);
9✔
1660
                        lastSelectedRowRef.current = row;
9✔
1661
                    }
1662
                } else if (col >= rowMarkerOffset && showTrailingBlankRow && row === rows) {
246✔
1663
                    const customTargetColumn = getCustomNewRowTargetColumn(col);
1✔
1664
                    void appendRow(customTargetColumn ?? col);
1!
1665
                } else {
1666
                    if (cellCol !== col || cellRow !== row) {
92✔
1667
                        const cell = getMangledCellContent(args.location);
75✔
1668
                        const renderer = getCellRenderer(cell);
75✔
1669

1670
                        if (renderer?.onSelect !== undefined) {
225!
1671
                            let prevented = false;
×
1672
                            renderer.onSelect({
×
1673
                                ...args,
1674
                                cell,
1675
                                posX: args.localEventX,
1676
                                posY: args.localEventY,
1677
                                bounds: args.bounds,
1678
                                preventDefault: () => (prevented = true),
×
1679
                                theme: themeForCell(cell, args.location),
1680
                            });
1681
                            if (prevented) {
1682
                                return;
×
1683
                            }
1684
                        }
1685
                        const isLastStickyRow = lastRowSticky && row === rows;
75✔
1686

1687
                        const startedFromLastSticky =
1688
                            lastRowSticky && gridSelection !== undefined && gridSelection.current?.cell[1] === rows;
75✔
1689

1690
                        if (
1691
                            (args.shiftKey || args.isLongTouch === true) &&
168✔
1692
                            cellCol !== undefined &&
1693
                            cellRow !== undefined &&
1694
                            gridSelection.current !== undefined &&
1695
                            !startedFromLastSticky
1696
                        ) {
1697
                            if (isLastStickyRow) {
1698
                                // If we're making a selection and shift click in to the last sticky row,
1699
                                // just drop the event. Don't kill the selection.
1700
                                return;
×
1701
                            }
1702

1703
                            const left = Math.min(col, cellCol);
6✔
1704
                            const right = Math.max(col, cellCol);
6✔
1705
                            const top = Math.min(row, cellRow);
6✔
1706
                            const bottom = Math.max(row, cellRow);
6✔
1707
                            setCurrent(
6✔
1708
                                {
1709
                                    ...gridSelection.current,
1710
                                    range: {
1711
                                        x: left,
1712
                                        y: top,
1713
                                        width: right - left + 1,
1714
                                        height: bottom - top + 1,
1715
                                    },
1716
                                },
1717
                                true,
1718
                                isMultiKey,
1719
                                "click"
1720
                            );
1721
                            lastSelectedRowRef.current = undefined;
6✔
1722
                            focus();
6✔
1723
                        } else {
1724
                            setCurrent(
69✔
1725
                                {
1726
                                    cell: [col, row],
1727
                                    range: { x: col, y: row, width: 1, height: 1 },
1728
                                },
1729
                                true,
1730
                                isMultiKey,
1731
                                "click"
1732
                            );
1733
                            lastSelectedRowRef.current = undefined;
69✔
1734
                            setOverlay(undefined);
69✔
1735
                            focus();
69✔
1736
                        }
1737
                    }
1738
                }
1739
            } else if (args.kind === "header") {
1740
                lastMouseSelectLocation.current = [col, row];
12✔
1741
                setOverlay(undefined);
12✔
1742
                if (hasRowMarkers && col === 0) {
18✔
1743
                    lastSelectedRowRef.current = undefined;
4✔
1744
                    lastSelectedColRef.current = undefined;
4✔
1745
                    if (rowSelect === "multi") {
1746
                        if (selectedRows.length !== rows) {
1747
                            setSelectedRows(CompactSelection.fromSingleSelection([0, rows]), undefined, isMultiKey);
3✔
1748
                        } else {
1749
                            setSelectedRows(CompactSelection.empty(), undefined, isMultiKey);
1✔
1750
                        }
1751
                        focus();
4✔
1752
                    }
1753
                } else {
1754
                    const lastCol = lastSelectedColRef.current;
8✔
1755
                    if (
1756
                        columnSelect === "multi" &&
22!
1757
                        (args.shiftKey || args.isLongTouch === true) &&
1758
                        lastCol !== undefined &&
1759
                        selectedColumns.hasIndex(lastCol)
1760
                    ) {
1761
                        const newSlice: Slice = [Math.min(lastCol, col), Math.max(lastCol, col) + 1];
×
1762

1763
                        if (isMultiCol) {
1764
                            setSelectedColumns(undefined, newSlice, isMultiKey);
×
1765
                        } else {
1766
                            setSelectedColumns(CompactSelection.fromSingleSelection(newSlice), undefined, isMultiKey);
×
1767
                        }
1768
                    } else if (isMultiCol) {
1769
                        if (selectedColumns.hasIndex(col)) {
1770
                            setSelectedColumns(selectedColumns.remove(col), undefined, isMultiKey);
×
1771
                        } else {
1772
                            setSelectedColumns(undefined, col, isMultiKey);
1✔
1773
                        }
1774
                        lastSelectedColRef.current = col;
1✔
1775
                    } else if (columnSelect !== "none") {
1776
                        setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, isMultiKey);
7✔
1777
                        lastSelectedColRef.current = col;
7✔
1778
                    }
1779
                    lastSelectedRowRef.current = undefined;
8✔
1780
                    focus();
8✔
1781
                }
1782
            } else if (args.kind === groupHeaderKind) {
1783
                lastMouseSelectLocation.current = [col, row];
3✔
1784
            } else if (args.kind === outOfBoundsKind && !args.isMaybeScrollbar) {
2✔
1785
                setGridSelection(emptyGridSelection, false);
1✔
1786
                setOverlay(undefined);
1✔
1787
                focus();
1✔
1788
                onSelectionCleared?.();
1!
1789
                lastSelectedRowRef.current = undefined;
1✔
1790
                lastSelectedColRef.current = undefined;
1✔
1791
            }
1792
        },
1793
        [
1794
            appendRow,
1795
            columnSelect,
1796
            focus,
1797
            getCellRenderer,
1798
            getCustomNewRowTargetColumn,
1799
            getMangledCellContent,
1800
            gridSelection,
1801
            hasRowMarkers,
1802
            lastRowSticky,
1803
            onSelectionCleared,
1804
            onRowMoved,
1805
            rowMarkerOffset,
1806
            rowMarkers,
1807
            rowSelect,
1808
            rowSelectionMode,
1809
            rows,
1810
            setCurrent,
1811
            setGridSelection,
1812
            setSelectedColumns,
1813
            setSelectedRows,
1814
            showTrailingBlankRow,
1815
            themeForCell,
1816
        ]
1817
    );
1818
    const isActivelyDraggingHeader = React.useRef(false);
648✔
1819
    const lastMouseSelectLocation = React.useRef<readonly [number, number]>();
648✔
1820
    const touchDownArgs = React.useRef(visibleRegion);
648✔
1821
    const mouseDownData = React.useRef<{
648✔
1822
        wasDoubleClick: boolean;
1823
        time: number;
1824
        button: number;
1825
        location: Item;
1826
    }>();
1827
    const onMouseDown = React.useCallback(
648✔
1828
        (args: GridMouseEventArgs) => {
1829
            isPrevented.current = false;
126✔
1830
            touchDownArgs.current = visibleRegionRef.current;
126✔
1831
            if (args.button !== 0 && args.button !== 1) {
131✔
1832
                mouseDownData.current = undefined;
1✔
1833
                return;
1✔
1834
            }
1835

1836
            const time = performance.now();
125✔
1837
            const wasDoubleClick = time - (mouseDownData.current?.time ?? -1000) < 250;
125✔
1838
            mouseDownData.current = {
125✔
1839
                wasDoubleClick,
1840
                button: args.button,
1841
                time,
1842
                location: args.location,
1843
            };
1844

1845
            if (args?.kind === "header") {
375!
1846
                isActivelyDraggingHeader.current = true;
18✔
1847
            }
1848

1849
            const fh = args.kind === "cell" && args.isFillHandle;
125✔
1850

1851
            if (!fh && args.kind !== "cell" && args.isEdge) return;
125✔
1852

1853
            setMouseState({
118✔
1854
                previousSelection: gridSelection,
1855
                fillHandle: fh,
1856
            });
1857
            lastMouseSelectLocation.current = undefined;
118✔
1858

1859
            if (!args.isTouch && args.button === 0) {
232✔
1860
                handleSelect(args);
110✔
1861
            } else if (!args.isTouch && args.button === 1) {
12✔
1862
                lastMouseSelectLocation.current = args.location;
4✔
1863
            }
1864
        },
1865
        [gridSelection, handleSelect]
1866
    );
1867

1868
    const [renameGroup, setRenameGroup] = React.useState<{
648✔
1869
        group: string;
1870
        bounds: Rectangle;
1871
    }>();
1872

1873
    const handleGroupHeaderSelection = React.useCallback(
648✔
1874
        (args: GridMouseEventArgs) => {
1875
            if (args.kind !== groupHeaderKind || columnSelect !== "multi") {
6✔
1876
                return;
×
1877
            }
1878
            const isMultiKey = browserIsOSX.value ? args.metaKey : args.ctrlKey;
3!
1879
            const [col] = args.location;
3✔
1880
            const selectedColumns = gridSelection.columns;
3✔
1881

1882
            if (col < rowMarkerOffset) return;
3!
1883

1884
            const needle = mangledCols[col];
3✔
1885
            let start = col;
3✔
1886
            let end = col;
3✔
1887
            for (let i = col - 1; i >= rowMarkerOffset; i--) {
3✔
1888
                if (!isGroupEqual(needle.group, mangledCols[i].group)) break;
3!
1889
                start--;
3✔
1890
            }
1891

1892
            for (let i = col + 1; i < mangledCols.length; i++) {
3✔
1893
                if (!isGroupEqual(needle.group, mangledCols[i].group)) break;
27!
1894
                end++;
27✔
1895
            }
1896

1897
            focus();
3✔
1898

1899
            if (isMultiKey) {
1900
                if (selectedColumns.hasAll([start, end + 1])) {
1901
                    let newVal = selectedColumns;
1✔
1902
                    for (let index = start; index <= end; index++) {
1✔
1903
                        newVal = newVal.remove(index);
11✔
1904
                    }
1905
                    setSelectedColumns(newVal, undefined, isMultiKey);
1✔
1906
                } else {
1907
                    setSelectedColumns(undefined, [start, end + 1], isMultiKey);
1✔
1908
                }
1909
            } else {
1910
                setSelectedColumns(CompactSelection.fromSingleSelection([start, end + 1]), undefined, isMultiKey);
1✔
1911
            }
1912
        },
1913
        [columnSelect, focus, gridSelection.columns, mangledCols, rowMarkerOffset, setSelectedColumns]
1914
    );
1915

1916
    const fillDown = React.useCallback(
648✔
1917
        (reverse: boolean) => {
1918
            if (gridSelection.current === undefined) return;
4!
1919
            const v: EditListItem[] = [];
4✔
1920
            const r = gridSelection.current.range;
4✔
1921
            for (let x = 0; x < r.width; x++) {
4✔
1922
                const fillCol = x + r.x;
5✔
1923
                const fillVal = getMangledCellContent([fillCol, reverse ? r.y + r.height - 1 : r.y]);
5!
1924
                if (isInnerOnlyCell(fillVal) || !isReadWriteCell(fillVal)) continue;
5!
1925
                for (let y = 1; y < r.height; y++) {
5✔
1926
                    const fillRow = reverse ? r.y + r.height - (y + 1) : y + r.y;
14!
1927
                    const target = [fillCol, fillRow] as const;
14✔
1928
                    v.push({
14✔
1929
                        location: target,
1930
                        value: { ...fillVal },
1931
                    });
1932
                }
1933
            }
1934

1935
            mangledOnCellsEdited(v);
4✔
1936

1937
            gridRef.current?.damage(
4!
1938
                v.map(c => ({
14✔
1939
                    cell: c.location,
1940
                }))
1941
            );
1942
        },
1943
        [getMangledCellContent, gridSelection, mangledOnCellsEdited]
1944
    );
1945

1946
    const isPrevented = React.useRef(false);
648✔
1947

1948
    const normalSizeColumn = React.useCallback(
648✔
1949
        async (col: number, force: boolean = false): Promise<void> => {
3✔
1950
            if (
1951
                (mouseDownData.current?.wasDoubleClick === true || force) &&
18✔
1952
                getCellsForSelection !== undefined &&
1953
                onColumnResize !== undefined
1954
            ) {
1955
                const start = visibleRegionRef.current.y;
2✔
1956
                const end = visibleRegionRef.current.height;
2✔
1957
                let cells = getCellsForSelection(
2✔
1958
                    {
1959
                        x: col,
1960
                        y: start,
1961
                        width: 1,
1962
                        height: Math.min(end, rows - start),
1963
                    },
1964
                    abortControllerRef.current.signal
1965
                );
1966
                if (typeof cells !== "object") {
1967
                    cells = await cells();
×
1968
                }
1969
                const inputCol = columns[col - rowMarkerOffset];
2✔
1970
                const offscreen = document.createElement("canvas");
2✔
1971
                const ctx = offscreen.getContext("2d", { alpha: false });
2✔
1972
                if (ctx !== null) {
1973
                    ctx.font = `${mergedTheme.baseFontStyle} ${mergedTheme.fontFamily}`;
2✔
1974
                    const newCol = measureColumn(
2✔
1975
                        ctx,
1976
                        mergedTheme,
1977
                        inputCol,
1978
                        0,
1979
                        cells,
1980
                        minColumnWidth,
1981
                        maxColumnWidth,
1982
                        false,
1983
                        getCellRenderer
1984
                    );
1985
                    onColumnResize?.(inputCol, newCol.width, col, newCol.width);
2!
1986
                }
1987
            }
1988
        },
1989
        [
1990
            columns,
1991
            getCellsForSelection,
1992
            maxColumnWidth,
1993
            mergedTheme,
1994
            minColumnWidth,
1995
            onColumnResize,
1996
            rowMarkerOffset,
1997
            rows,
1998
            getCellRenderer,
1999
        ]
2000
    );
2001

2002
    const [scrollDir, setScrollDir] = React.useState<GridMouseEventArgs["scrollEdge"]>();
648✔
2003

2004
    const onMouseUp = React.useCallback(
648✔
2005
        (args: GridMouseEventArgs, isOutside: boolean) => {
2006
            const mouse = mouseState;
126✔
2007
            setMouseState(undefined);
126✔
2008
            setScrollDir(undefined);
126✔
2009
            isActivelyDraggingHeader.current = false;
126✔
2010

2011
            if (isOutside) return;
126✔
2012

2013
            if (mouse?.fillHandle === true && gridSelection.current !== undefined) {
499✔
2014
                fillDown(gridSelection.current.cell[1] !== gridSelection.current.range.y);
3✔
2015
                return;
3✔
2016
            }
2017

2018
            const [col, row] = args.location;
121✔
2019
            const [lastMouseDownCol, lastMouseDownRow] = lastMouseSelectLocation.current ?? [];
121✔
2020

2021
            const preventDefault = () => {
121✔
2022
                isPrevented.current = true;
×
2023
            };
2024

2025
            const handleMaybeClick = (a: GridMouseCellEventArgs): boolean => {
121✔
2026
                const isValidClick = a.isTouch || (lastMouseDownCol === col && lastMouseDownRow === row);
96✔
2027
                if (isValidClick) {
2028
                    onCellClicked?.([col - rowMarkerOffset, row], {
86✔
2029
                        ...a,
2030
                        preventDefault,
2031
                    });
2032
                }
2033
                if (a.button === 1) return !isPrevented.current;
96✔
2034
                if (!isPrevented.current) {
2035
                    const c = getMangledCellContent(args.location);
93✔
2036
                    const r = getCellRenderer(c);
93✔
2037
                    if (r !== undefined && r.onClick !== undefined && isValidClick) {
209✔
2038
                        const newVal = r.onClick({
20✔
2039
                            ...a,
2040
                            cell: c,
2041
                            posX: a.localEventX,
2042
                            posY: a.localEventY,
2043
                            bounds: a.bounds,
2044
                            theme: themeForCell(c, args.location),
2045
                            preventDefault,
2046
                        });
2047
                        if (newVal !== undefined && !isInnerOnlyCell(newVal) && isEditableGridCell(newVal)) {
41✔
2048
                            mangledOnCellsEdited([{ location: a.location, value: newVal }]);
4✔
2049
                            gridRef.current?.damage([
4!
2050
                                {
2051
                                    cell: a.location,
2052
                                },
2053
                            ]);
2054
                        }
2055
                    }
2056
                    if (
2057
                        !isPrevented.current &&
206✔
2058
                        mouse?.previousSelection?.current?.cell !== undefined &&
837!
2059
                        gridSelection.current !== undefined
2060
                    ) {
2061
                        const [selectedCol, selectedRow] = gridSelection.current.cell;
19✔
2062
                        const [prevCol, prevRow] = mouse.previousSelection.current.cell;
19✔
2063
                        if (col === selectedCol && col === prevCol && row === selectedRow && row === prevRow) {
45✔
2064
                            onCellActivated?.([col - rowMarkerOffset, row]);
3✔
2065
                            reselect(a.bounds, false);
3✔
2066
                            return true;
3✔
2067
                        }
2068
                    }
2069
                }
2070
                return false;
90✔
2071
            };
2072

2073
            const clickLocation = args.location[0] - rowMarkerOffset;
121✔
2074
            if (args.isTouch) {
2075
                const vr = visibleRegionRef.current;
4✔
2076
                const touchVr = touchDownArgs.current;
4✔
2077
                if (vr.x !== touchVr.x || vr.y !== touchVr.y) {
8✔
2078
                    // we scrolled, abort
2079
                    return;
×
2080
                }
2081
                // take care of context menus first if long pressed item is already selected
2082
                if (args.isLongTouch === true) {
2083
                    if (
2084
                        args.kind === "cell" &&
×
2085
                        gridSelection?.current?.cell[0] === col &&
×
2086
                        gridSelection?.current?.cell[1] === row
×
2087
                    ) {
2088
                        onCellContextMenu?.([clickLocation, args.location[1]], {
×
2089
                            ...args,
2090
                            preventDefault,
2091
                        });
2092
                        return;
×
2093
                    } else if (args.kind === "header" && gridSelection.columns.hasIndex(col)) {
×
2094
                        onHeaderContextMenu?.(clickLocation, { ...args, preventDefault });
×
2095
                        return;
×
2096
                    } else if (args.kind === groupHeaderKind) {
2097
                        if (clickLocation < 0) {
2098
                            return;
×
2099
                        }
2100

2101
                        onGroupHeaderContextMenu?.(clickLocation, { ...args, preventDefault });
×
2102
                        return;
×
2103
                    }
2104
                }
2105
                if (args.kind === "cell") {
2106
                    // click that cell
2107
                    if (!handleMaybeClick(args)) {
2108
                        handleSelect(args);
2✔
2109
                    }
2110
                } else if (args.kind === groupHeaderKind) {
2111
                    onGroupHeaderClicked?.(clickLocation, { ...args, preventDefault });
1!
2112
                } else {
2113
                    if (args.kind === headerKind) {
2114
                        onHeaderClicked?.(clickLocation, {
1!
2115
                            ...args,
2116
                            preventDefault,
2117
                        });
2118
                    }
2119
                    handleSelect(args);
1✔
2120
                }
2121
                return;
4✔
2122
            }
2123

2124
            if (args.kind === "header") {
2125
                if (clickLocation < 0) {
2126
                    return;
3✔
2127
                }
2128

2129
                if (args.isEdge) {
2130
                    void normalSizeColumn(col);
2✔
2131
                } else if (args.button === 0 && col === lastMouseDownCol && row === lastMouseDownRow) {
27✔
2132
                    onHeaderClicked?.(clickLocation, { ...args, preventDefault });
5✔
2133
                }
2134
            }
2135

2136
            if (args.kind === groupHeaderKind) {
2137
                if (clickLocation < 0) {
2138
                    return;
×
2139
                }
2140

2141
                if (args.button === 0 && col === lastMouseDownCol && row === lastMouseDownRow) {
9✔
2142
                    onGroupHeaderClicked?.(clickLocation, { ...args, preventDefault });
3!
2143
                    if (!isPrevented.current) {
2144
                        handleGroupHeaderSelection(args);
3✔
2145
                    }
2146
                }
2147
            }
2148

2149
            if (args.kind === "cell" && (args.button === 0 || args.button === 1)) {
213✔
2150
                handleMaybeClick(args);
94✔
2151
            }
2152

2153
            lastMouseSelectLocation.current = undefined;
114✔
2154
        },
2155
        [
2156
            mouseState,
2157
            rowMarkerOffset,
2158
            gridSelection,
2159
            onCellClicked,
2160
            fillDown,
2161
            getMangledCellContent,
2162
            getCellRenderer,
2163
            themeForCell,
2164
            mangledOnCellsEdited,
2165
            onCellActivated,
2166
            reselect,
2167
            onCellContextMenu,
2168
            onHeaderContextMenu,
2169
            onGroupHeaderContextMenu,
2170
            handleSelect,
2171
            onGroupHeaderClicked,
2172
            normalSizeColumn,
2173
            onHeaderClicked,
2174
            handleGroupHeaderSelection,
2175
        ]
2176
    );
2177

2178
    const onMouseMoveImpl = React.useCallback(
648✔
2179
        (args: GridMouseEventArgs) => {
2180
            const a: GridMouseEventArgs = {
37✔
2181
                ...args,
2182
                location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
2183
            };
2184
            onMouseMove?.(a);
37✔
2185
            setScrollDir(cv => {
37✔
2186
                if (isActivelyDraggingHeader.current) return [args.scrollEdge[0], 0];
37✔
2187
                if (args.scrollEdge[0] === cv?.[0] && args.scrollEdge[1] === cv[1]) return cv;
26!
2188
                return mouseState === undefined || (mouseDownData.current?.location[0] ?? 0) < rowMarkerOffset
26!
2189
                    ? undefined
2190
                    : args.scrollEdge;
2191
            });
2192
        },
2193
        [mouseState, onMouseMove, rowMarkerOffset]
2194
    );
2195

2196
    useAutoscroll(scrollDir, scrollRef);
648✔
2197

2198
    const onHeaderMenuClickInner = React.useCallback(
648✔
2199
        (col: number, screenPosition: Rectangle) => {
2200
            onHeaderMenuClick?.(col - rowMarkerOffset, screenPosition);
1!
2201
        },
2202
        [onHeaderMenuClick, rowMarkerOffset]
2203
    );
2204

2205
    const currentCell = gridSelection?.current?.cell;
648!
2206
    const onVisibleRegionChangedImpl = React.useCallback(
648✔
2207
        (
2208
            region: Rectangle,
2209
            clientWidth: number,
2210
            clientHeight: number,
2211
            rightElWidth: number,
2212
            tx?: number,
2213
            ty?: number
2214
        ) => {
2215
            hasJustScrolled.current = false;
136✔
2216
            let selected = currentCell;
136✔
2217
            if (selected !== undefined) {
2218
                selected = [selected[0] - rowMarkerOffset, selected[1]];
10✔
2219
            }
2220
            const newRegion = {
136✔
2221
                x: region.x - rowMarkerOffset,
2222
                y: region.y,
2223
                width: region.width,
2224
                height: showTrailingBlankRow && region.y + region.height >= rows ? region.height - 1 : region.height,
408✔
2225
                tx,
2226
                ty,
2227
                extras: {
2228
                    selected,
2229
                    freezeRegion:
2230
                        freezeColumns === 0
2231
                            ? undefined
136✔
2232
                            : {
2233
                                  x: 0,
2234
                                  y: region.y,
2235
                                  width: freezeColumns,
2236
                                  height: region.height,
2237
                              },
2238
                },
2239
            };
2240
            visibleRegionRef.current = newRegion;
136✔
2241
            setVisibleRegion(newRegion);
136✔
2242
            setClientSize([clientWidth, clientHeight, rightElWidth]);
136✔
2243
            onVisibleRegionChanged?.(newRegion, newRegion.tx, newRegion.ty, newRegion.extras);
136!
2244
        },
2245
        [
2246
            currentCell,
2247
            rowMarkerOffset,
2248
            showTrailingBlankRow,
2249
            rows,
2250
            freezeColumns,
2251
            setVisibleRegion,
2252
            onVisibleRegionChanged,
2253
        ]
2254
    );
2255

2256
    const onColumnMovedImpl = whenDefined(
648✔
2257
        onColumnMoved,
2258
        React.useCallback(
2259
            (startIndex: number, endIndex: number) => {
2260
                onColumnMoved?.(startIndex - rowMarkerOffset, endIndex - rowMarkerOffset);
1!
2261
                if (columnSelect !== "none") {
2262
                    setSelectedColumns(CompactSelection.fromSingleSelection(endIndex), undefined, true);
1✔
2263
                }
2264
            },
2265
            [columnSelect, onColumnMoved, rowMarkerOffset, setSelectedColumns]
2266
        )
2267
    );
2268

2269
    const isActivelyDragging = React.useRef(false);
648✔
2270
    const onDragStartImpl = React.useCallback(
648✔
2271
        (args: GridDragEventArgs) => {
2272
            if (args.location[0] === 0 && rowMarkerOffset > 0) {
1!
2273
                args.preventDefault();
×
2274
                return;
×
2275
            }
2276
            onDragStart?.({
1!
2277
                ...args,
2278
                location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
2279
            });
2280

2281
            if (!args.defaultPrevented()) {
2282
                isActivelyDragging.current = true;
1✔
2283
            }
2284
            setMouseState(undefined);
1✔
2285
        },
2286
        [onDragStart, rowMarkerOffset]
2287
    );
2288

2289
    const onDragEnd = React.useCallback(() => {
648✔
2290
        isActivelyDragging.current = false;
×
2291
    }, []);
2292

2293
    const onItemHoveredImpl = React.useCallback(
648✔
2294
        (args: GridMouseEventArgs) => {
2295
            if (mouseDownData?.current?.button !== undefined && mouseDownData.current.button >= 1) return;
32!
2296
            if (
2297
                mouseState !== undefined &&
64✔
2298
                mouseDownData.current?.location[0] === 0 &&
54!
2299
                args.location[0] === 0 &&
2300
                rowMarkerOffset === 1 &&
2301
                rowSelect === "multi" &&
2302
                mouseState.previousSelection &&
2303
                !mouseState.previousSelection.rows.hasIndex(mouseDownData.current.location[1]) &&
2304
                gridSelection.rows.hasIndex(mouseDownData.current.location[1])
2305
            ) {
2306
                const start = Math.min(mouseDownData.current.location[1], args.location[1]);
2✔
2307
                const end = Math.max(mouseDownData.current.location[1], args.location[1]) + 1;
2✔
2308
                setSelectedRows(CompactSelection.fromSingleSelection([start, end]), undefined, false);
2✔
2309
            }
2310
            if (
2311
                mouseState !== undefined &&
87✔
2312
                gridSelection.current !== undefined &&
2313
                !isActivelyDragging.current &&
2314
                (rangeSelect === "rect" || rangeSelect === "multi-rect")
2315
            ) {
2316
                const [selectedCol, selectedRow] = gridSelection.current.cell;
10✔
2317
                // eslint-disable-next-line prefer-const
2318
                let [col, row] = args.location;
10✔
2319

2320
                if (row < 0) {
2321
                    row = visibleRegionRef.current.y;
1✔
2322
                }
2323

2324
                const startedFromLastStickyRow = lastRowSticky && selectedRow === rows;
10✔
2325
                if (startedFromLastStickyRow) return;
10!
2326

2327
                const landedOnLastStickyRow = lastRowSticky && row === rows;
10✔
2328
                if (landedOnLastStickyRow) {
2329
                    if (args.kind === outOfBoundsKind) row--;
3✔
2330
                    else return;
1✔
2331
                }
2332

2333
                col = Math.max(col, rowMarkerOffset);
9✔
2334

2335
                const deltaX = col - selectedCol;
9✔
2336
                const deltaY = row - selectedRow;
9✔
2337

2338
                const newRange: Rectangle = {
9✔
2339
                    x: deltaX >= 0 ? selectedCol : col,
9!
2340
                    y: deltaY >= 0 ? selectedRow : row,
9✔
2341
                    width: Math.abs(deltaX) + 1,
2342
                    height: Math.abs(deltaY) + 1,
2343
                };
2344

2345
                setCurrent(
9✔
2346
                    {
2347
                        ...gridSelection.current,
2348
                        range: newRange,
2349
                    },
2350
                    true,
2351
                    false,
2352
                    "drag"
2353
                );
2354
            }
2355

2356
            onItemHovered?.({ ...args, location: [args.location[0] - rowMarkerOffset, args.location[1]] as any });
30✔
2357
        },
2358
        [
2359
            mouseState,
2360
            rowMarkerOffset,
2361
            rowSelect,
2362
            gridSelection,
2363
            rangeSelect,
2364
            onItemHovered,
2365
            setSelectedRows,
2366
            lastRowSticky,
2367
            rows,
2368
            setCurrent,
2369
        ]
2370
    );
2371

2372
    // 1 === move one
2373
    // 2 === move to end
2374
    const adjustSelection = React.useCallback(
648✔
2375
        (direction: [0 | 1 | -1 | 2 | -2, 0 | 1 | -1 | 2 | -2]) => {
2376
            if (gridSelection.current === undefined) return;
8!
2377

2378
            const [x, y] = direction;
8✔
2379
            const [col, row] = gridSelection.current.cell;
8✔
2380
            const old = gridSelection.current.range;
8✔
2381
            let left = old.x;
8✔
2382
            let right = old.x + old.width;
8✔
2383
            let top = old.y;
8✔
2384
            let bottom = old.y + old.height;
8✔
2385

2386
            // take care of vertical first in case new spans come in
2387
            if (y !== 0) {
2388
                switch (y) {
2389
                    case 2: {
2390
                        // go to end
2391
                        bottom = rows;
1✔
2392
                        top = row;
1✔
2393
                        scrollTo(0, bottom, "vertical");
1✔
2394

2395
                        break;
1✔
2396
                    }
2397
                    case -2: {
2398
                        // go to start
2399
                        top = 0;
×
2400
                        bottom = row + 1;
×
2401
                        scrollTo(0, top, "vertical");
×
2402

2403
                        break;
×
2404
                    }
2405
                    case 1: {
2406
                        // motion down
2407
                        if (top < row) {
2408
                            top++;
×
2409
                            scrollTo(0, top, "vertical");
×
2410
                        } else {
2411
                            bottom = Math.min(rows, bottom + 1);
1✔
2412
                            scrollTo(0, bottom, "vertical");
1✔
2413
                        }
2414

2415
                        break;
1✔
2416
                    }
2417
                    case -1: {
2418
                        // motion up
2419
                        if (bottom > row + 1) {
2420
                            bottom--;
×
2421
                            scrollTo(0, bottom, "vertical");
×
2422
                        } else {
2423
                            top = Math.max(0, top - 1);
×
2424
                            scrollTo(0, top, "vertical");
×
2425
                        }
2426

2427
                        break;
×
2428
                    }
2429
                    default: {
2430
                        assertNever(y);
×
2431
                    }
2432
                }
2433
            }
2434

2435
            if (x !== 0) {
2436
                if (x === 2) {
2437
                    right = mangledCols.length;
1✔
2438
                    left = col;
1✔
2439
                    scrollTo(right - 1 - rowMarkerOffset, 0, "horizontal");
1✔
2440
                } else if (x === -2) {
2441
                    left = rowMarkerOffset;
×
2442
                    right = col + 1;
×
2443
                    scrollTo(left - rowMarkerOffset, 0, "horizontal");
×
2444
                } else {
2445
                    let disallowed: number[] = [];
5✔
2446
                    if (getCellsForSelection !== undefined) {
2447
                        const cells = getCellsForSelection(
5✔
2448
                            {
2449
                                x: left,
2450
                                y: top,
2451
                                width: right - left - rowMarkerOffset,
2452
                                height: bottom - top,
2453
                            },
2454
                            abortControllerRef.current.signal
2455
                        );
2456

2457
                        if (typeof cells === "object") {
2458
                            disallowed = getSpanStops(cells);
5✔
2459
                        }
2460
                    }
2461
                    if (x === 1) {
2462
                        // motion right
2463
                        let done = false;
4✔
2464
                        if (left < col) {
2465
                            if (disallowed.length > 0) {
2466
                                const target = range(left + 1, col + 1).find(
×
2467
                                    n => !disallowed.includes(n - rowMarkerOffset)
×
2468
                                );
2469
                                if (target !== undefined) {
2470
                                    left = target;
×
2471
                                    done = true;
×
2472
                                }
2473
                            } else {
2474
                                left++;
×
2475
                                done = true;
×
2476
                            }
2477
                            if (done) scrollTo(left, 0, "horizontal");
×
2478
                        }
2479
                        if (!done) {
2480
                            right = Math.min(mangledCols.length, right + 1);
4✔
2481
                            scrollTo(right - 1 - rowMarkerOffset, 0, "horizontal");
4✔
2482
                        }
2483
                    } else if (x === -1) {
2484
                        // motion left
2485
                        let done = false;
1✔
2486
                        if (right > col + 1) {
2487
                            if (disallowed.length > 0) {
2488
                                const target = range(right - 1, col, -1).find(
×
2489
                                    n => !disallowed.includes(n - rowMarkerOffset)
×
2490
                                );
2491
                                if (target !== undefined) {
2492
                                    right = target;
×
2493
                                    done = true;
×
2494
                                }
2495
                            } else {
2496
                                right--;
×
2497
                                done = true;
×
2498
                            }
2499
                            if (done) scrollTo(right - rowMarkerOffset, 0, "horizontal");
×
2500
                        }
2501
                        if (!done) {
2502
                            left = Math.max(rowMarkerOffset, left - 1);
1✔
2503
                            scrollTo(left - rowMarkerOffset, 0, "horizontal");
1✔
2504
                        }
2505
                    } else {
2506
                        assertNever(x);
×
2507
                    }
2508
                }
2509
            }
2510

2511
            setCurrent(
8✔
2512
                {
2513
                    cell: gridSelection.current.cell,
2514
                    range: {
2515
                        x: left,
2516
                        y: top,
2517
                        width: right - left,
2518
                        height: bottom - top,
2519
                    },
2520
                },
2521
                true,
2522
                false,
2523
                "keyboard-select"
2524
            );
2525
        },
2526
        [getCellsForSelection, gridSelection, mangledCols.length, rowMarkerOffset, rows, scrollTo, setCurrent]
2527
    );
2528

2529
    const updateSelectedCell = React.useCallback(
648✔
2530
        (col: number, row: number, fromEditingTrailingRow: boolean, freeMove: boolean): boolean => {
2531
            const rowMax = mangledRows - (fromEditingTrailingRow ? 0 : 1);
53!
2532
            col = clamp(col, rowMarkerOffset, columns.length - 1 + rowMarkerOffset);
53✔
2533
            row = clamp(row, 0, rowMax);
53✔
2534

2535
            if (col === currentCell?.[0] && row === currentCell?.[1]) return false;
53!
2536
            if (freeMove && gridSelection.current !== undefined) {
27✔
2537
                const newStack = [...gridSelection.current.rangeStack];
1✔
2538
                if (gridSelection.current.range.width > 1 || gridSelection.current.range.height > 1) {
1!
2539
                    newStack.push(gridSelection.current.range);
1✔
2540
                }
2541
                setGridSelection(
1✔
2542
                    {
2543
                        ...gridSelection,
2544
                        current: {
2545
                            cell: [col, row],
2546
                            range: { x: col, y: row, width: 1, height: 1 },
2547
                            rangeStack: newStack,
2548
                        },
2549
                    },
2550
                    true
2551
                );
2552
            } else {
2553
                setCurrent(
25✔
2554
                    {
2555
                        cell: [col, row],
2556
                        range: { x: col, y: row, width: 1, height: 1 },
2557
                    },
2558
                    true,
2559
                    false,
2560
                    "keyboard-nav"
2561
                );
2562
            }
2563

2564
            if (lastSent.current !== undefined && lastSent.current[0] === col && lastSent.current[1] === row) {
30✔
2565
                lastSent.current = undefined;
2✔
2566
            }
2567

2568
            scrollTo(col - rowMarkerOffset, row);
26✔
2569

2570
            return true;
26✔
2571
        },
2572
        [
2573
            mangledRows,
2574
            rowMarkerOffset,
2575
            columns.length,
2576
            currentCell,
2577
            gridSelection,
2578
            scrollTo,
2579
            setGridSelection,
2580
            setCurrent,
2581
        ]
2582
    );
2583

2584
    const onFinishEditing = React.useCallback(
648✔
2585
        (newValue: GridCell | undefined, movement: readonly [-1 | 0 | 1, -1 | 0 | 1]) => {
2586
            if (overlay?.cell !== undefined && newValue !== undefined && isEditableGridCell(newValue)) {
39!
2587
                mangledOnCellsEdited([{ location: overlay.cell, value: newValue }]);
4✔
2588
                window.requestAnimationFrame(() => {
4✔
2589
                    gridRef.current?.damage([
4!
2590
                        {
2591
                            cell: overlay.cell,
2592
                        },
2593
                    ]);
2594
                });
2595
            }
2596
            focus(true);
7✔
2597
            setOverlay(undefined);
7✔
2598

2599
            const [movX, movY] = movement;
7✔
2600
            if (gridSelection.current !== undefined && (movX !== 0 || movY !== 0)) {
21✔
2601
                const isEditingTrailingRow =
2602
                    gridSelection.current.cell[1] === mangledRows - 1 && newValue !== undefined;
3!
2603
                updateSelectedCell(
3✔
2604
                    clamp(gridSelection.current.cell[0] + movX, 0, mangledCols.length - 1),
2605
                    clamp(gridSelection.current.cell[1] + movY, 0, mangledRows - 1),
2606
                    isEditingTrailingRow,
2607
                    false
2608
                );
2609
            }
2610
            onFinishedEditing?.(newValue, movement);
7✔
2611
        },
2612
        [
2613
            overlay?.cell,
1,944✔
2614
            focus,
2615
            gridSelection,
2616
            onFinishedEditing,
2617
            mangledOnCellsEdited,
2618
            mangledRows,
2619
            updateSelectedCell,
2620
            mangledCols.length,
2621
        ]
2622
    );
2623

2624
    const overlayID = React.useMemo(() => {
648✔
2625
        return `gdg-overlay-${idCounter++}`;
132✔
2626
    }, []);
2627

2628
    const deleteRange = React.useCallback(
648✔
2629
        (r: Rectangle) => {
2630
            focus();
8✔
2631
            const editList: EditListItem[] = [];
8✔
2632
            for (let x = r.x; x < r.x + r.width; x++) {
8✔
2633
                for (let y = r.y; y < r.y + r.height; y++) {
23✔
2634
                    const cellValue = getCellContent([x - rowMarkerOffset, y]);
1,066✔
2635
                    if (!cellValue.allowOverlay && cellValue.kind !== GridCellKind.Boolean) continue;
1,066✔
2636
                    let newVal: InnerGridCell | undefined = undefined;
1,042✔
2637
                    if (cellValue.kind === GridCellKind.Custom) {
2638
                        const toDelete = getCellRenderer(cellValue);
1✔
2639
                        const editor = toDelete?.provideEditor?.(cellValue);
1!
2640
                        if (toDelete?.onDelete !== undefined) {
3!
2641
                            newVal = toDelete.onDelete(cellValue);
1✔
2642
                        } else if (isObjectEditorCallbackResult(editor)) {
2643
                            newVal = editor?.deletedValue?.(cellValue);
×
2644
                        }
2645
                    } else if (
2646
                        (isEditableGridCell(cellValue) && cellValue.allowOverlay) ||
2,083✔
2647
                        cellValue.kind === GridCellKind.Boolean
2648
                    ) {
2649
                        const toDelete = getCellRenderer(cellValue);
1,041✔
2650
                        newVal = toDelete?.onDelete?.(cellValue);
1,041!
2651
                    }
2652
                    if (newVal !== undefined && !isInnerOnlyCell(newVal) && isEditableGridCell(newVal)) {
3,124✔
2653
                        editList.push({ location: [x, y], value: newVal });
1,041✔
2654
                    }
2655
                }
2656
            }
2657
            mangledOnCellsEdited(editList);
8✔
2658
            gridRef.current?.damage(editList.map(x => ({ cell: x.location })));
1,041!
2659
        },
2660
        [focus, getCellContent, getCellRenderer, mangledOnCellsEdited, rowMarkerOffset]
2661
    );
2662

2663
    const onKeyDown = React.useCallback(
648✔
2664
        (event: GridKeyEventArgs) => {
2665
            const fn = async () => {
61✔
2666
                let cancelled = false;
61✔
2667
                if (onKeyDownIn !== undefined) {
2668
                    onKeyDownIn({
1✔
2669
                        ...event,
2670
                        cancel: () => {
2671
                            cancelled = true;
×
2672
                        },
2673
                    });
2674
                }
2675

2676
                if (cancelled) return;
61!
2677

2678
                const cancel = () => {
61✔
2679
                    event.stopPropagation();
42✔
2680
                    event.preventDefault();
42✔
2681
                };
2682

2683
                const overlayOpen = overlay !== undefined;
61✔
2684
                const { altKey, shiftKey, metaKey, ctrlKey, key, bounds } = event;
61✔
2685
                const isOSX = browserIsOSX.value;
61✔
2686
                const isPrimaryKey = isOSX ? metaKey : ctrlKey;
61!
2687
                const isDeleteKey = key === "Delete" || (isOSX && key === "Backspace");
61!
2688
                const vr = visibleRegionRef.current;
61✔
2689
                const selectedColumns = gridSelection.columns;
61✔
2690
                const selectedRows = gridSelection.rows;
61✔
2691

2692
                if (key === "Escape") {
2693
                    if (overlayOpen) {
2694
                        setOverlay(undefined);
2✔
2695
                    } else if (keybindings.clear) {
2696
                        setGridSelection(emptyGridSelection, false);
2✔
2697
                        onSelectionCleared?.();
2!
2698
                    }
2699
                    return;
4✔
2700
                } else if (isHotkey("primary+a", event) && keybindings.selectAll) {
58✔
2701
                    if (!overlayOpen) {
2702
                        setGridSelection(
1✔
2703
                            {
2704
                                columns: CompactSelection.empty(),
2705
                                rows: CompactSelection.empty(),
2706
                                current: {
2707
                                    cell: gridSelection.current?.cell ?? [rowMarkerOffset, 0],
6!
2708
                                    range: {
2709
                                        x: rowMarkerOffset,
2710
                                        y: 0,
2711
                                        width: columnsIn.length,
2712
                                        height: rows,
2713
                                    },
2714
                                    rangeStack: [],
2715
                                },
2716
                            },
2717
                            false
2718
                        );
2719
                    } else {
2720
                        const el = document.getElementById(overlayID);
×
2721
                        if (el !== null) {
2722
                            const s = window.getSelection();
×
2723
                            const r = document.createRange();
×
2724
                            r.selectNodeContents(el);
×
2725
                            s?.removeAllRanges();
×
2726
                            s?.addRange(r);
×
2727
                        }
2728
                    }
2729
                    cancel();
1✔
2730
                    return;
1✔
2731
                } else if (isHotkey("primary+f", event) && keybindings.search) {
56!
2732
                    cancel();
×
2733
                    searchInputRef?.current?.focus({ preventScroll: true });
×
2734
                    setShowSearchInner(true);
×
2735
                }
2736

2737
                if (isDeleteKey) {
2738
                    const callbackResult = onDelete?.(gridSelection) ?? true;
8!
2739
                    cancel();
8✔
2740
                    if (callbackResult !== false) {
2741
                        const toDelete = callbackResult === true ? gridSelection : callbackResult;
8✔
2742

2743
                        // delete order:
2744
                        // 1) primary range
2745
                        // 2) secondary ranges
2746
                        // 3) columns
2747
                        // 4) rows
2748

2749
                        if (toDelete.current !== undefined) {
2750
                            deleteRange(toDelete.current.range);
5✔
2751
                            for (const r of toDelete.current.rangeStack) {
2752
                                deleteRange(r);
×
2753
                            }
2754
                        }
2755

2756
                        for (const r of toDelete.rows) {
2757
                            deleteRange({
1✔
2758
                                x: rowMarkerOffset,
2759
                                y: r,
2760
                                width: mangledCols.length - rowMarkerOffset,
2761
                                height: 1,
2762
                            });
2763
                        }
2764

2765
                        for (const col of toDelete.columns) {
2766
                            deleteRange({
1✔
2767
                                x: col,
2768
                                y: 0,
2769
                                width: 1,
2770
                                height: rows,
2771
                            });
2772
                        }
2773
                    }
2774
                    return;
8✔
2775
                }
2776

2777
                if (gridSelection.current === undefined) return;
48✔
2778
                let [col, row] = gridSelection.current.cell;
45✔
2779
                let freeMove = false;
45✔
2780

2781
                if (keybindings.selectColumn && isHotkey("ctrl+ ", event) && columnSelect !== "none") {
92✔
2782
                    if (selectedColumns.hasIndex(col)) {
2783
                        setSelectedColumns(selectedColumns.remove(col), undefined, true);
×
2784
                    } else {
2785
                        if (columnSelect === "single") {
2786
                            setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, true);
×
2787
                        } else {
2788
                            setSelectedColumns(undefined, col, true);
2✔
2789
                        }
2790
                    }
2791
                } else if (keybindings.selectRow && isHotkey("shift+ ", event) && rowSelect !== "none") {
88✔
2792
                    if (selectedRows.hasIndex(row)) {
2793
                        setSelectedRows(selectedRows.remove(row), undefined, true);
×
2794
                    } else {
2795
                        if (rowSelect === "single") {
2796
                            setSelectedRows(CompactSelection.fromSingleSelection(row), undefined, true);
×
2797
                        } else {
2798
                            setSelectedRows(undefined, row, true);
2✔
2799
                        }
2800
                    }
2801
                } else if (
2802
                    (isHotkey("Enter", event) || isHotkey(" ", event) || isHotkey("shift+Enter", event)) &&
118✔
2803
                    bounds !== undefined
2804
                ) {
2805
                    if (overlayOpen) {
2806
                        setOverlay(undefined);
2✔
2807
                        if (isHotkey("Enter", event)) {
2808
                            row++;
2✔
2809
                        } else if (isHotkey("shift+Enter", event)) {
2810
                            row--;
×
2811
                        }
2812
                    } else if (row === rows && showTrailingBlankRow) {
6!
2813
                        window.setTimeout(() => {
×
2814
                            const customTargetColumn = getCustomNewRowTargetColumn(col);
×
2815
                            void appendRow(customTargetColumn ?? col);
×
2816
                        }, 0);
2817
                    } else {
2818
                        onCellActivated?.([col - rowMarkerOffset, row]);
6✔
2819
                        reselect(bounds, true);
6✔
2820
                        cancel();
6✔
2821
                    }
2822
                } else if (
2823
                    keybindings.downFill &&
35✔
2824
                    isHotkey("primary+_68", event) &&
2825
                    gridSelection.current.range.height > 1
2826
                ) {
2827
                    // ctrl/cmd + d
2828
                    fillDown(false);
1✔
2829
                    cancel();
1✔
2830
                } else if (
2831
                    keybindings.rightFill &&
34✔
2832
                    isHotkey("primary+_82", event) &&
2833
                    gridSelection.current.range.width > 1
2834
                ) {
2835
                    // ctrl/cmd + r
2836
                    const editList: EditListItem[] = [];
1✔
2837
                    const r = gridSelection.current.range;
1✔
2838
                    for (let y = 0; y < r.height; y++) {
1✔
2839
                        const fillRow = y + r.y;
5✔
2840
                        const fillVal = getMangledCellContent([r.x, fillRow]);
5✔
2841
                        if (isInnerOnlyCell(fillVal) || !isReadWriteCell(fillVal)) continue;
5!
2842
                        for (let x = 1; x < r.width; x++) {
5✔
2843
                            const fillCol = x + r.x;
5✔
2844
                            const target = [fillCol, fillRow] as const;
5✔
2845
                            editList.push({
5✔
2846
                                location: target,
2847
                                value: { ...fillVal },
2848
                            });
2849
                        }
2850
                    }
2851
                    mangledOnCellsEdited(editList);
1✔
2852
                    gridRef.current?.damage(
1!
2853
                        editList.map(c => ({
5✔
2854
                            cell: c.location,
2855
                        }))
2856
                    );
2857
                    cancel();
1✔
2858
                } else if (keybindings.pageDown && isHotkey("PageDown", event)) {
31!
2859
                    row += Math.max(1, visibleRegionRef.current.height - 4); // partial cell accounting
×
2860
                    cancel();
×
2861
                } else if (keybindings.pageUp && isHotkey("PageUp", event)) {
31!
2862
                    row -= Math.max(1, visibleRegionRef.current.height - 4); // partial cell accounting
×
2863
                    cancel();
×
2864
                } else if (keybindings.first && isHotkey("primary+Home", event)) {
62✔
2865
                    setOverlay(undefined);
×
2866
                    row = 0;
×
2867
                    col = 0;
×
2868
                } else if (keybindings.last && isHotkey("primary+End", event)) {
62✔
2869
                    setOverlay(undefined);
×
2870
                    row = Number.MAX_SAFE_INTEGER;
×
2871
                    col = Number.MAX_SAFE_INTEGER;
×
2872
                } else if (keybindings.first && isHotkey("primary+shift+Home", event)) {
62✔
2873
                    setOverlay(undefined);
×
2874
                    adjustSelection([-2, -2]);
×
2875
                } else if (keybindings.last && isHotkey("primary+shift+End", event)) {
62✔
2876
                    setOverlay(undefined);
×
2877
                    adjustSelection([2, 2]);
×
2878
                    // eslint-disable-next-line unicorn/prefer-switch
2879
                } else if (key === "ArrowDown") {
2880
                    if (ctrlKey && altKey) {
10✔
2881
                        return;
×
2882
                    }
2883
                    setOverlay(undefined);
8✔
2884
                    if (shiftKey && (rangeSelect === "rect" || rangeSelect === "multi-rect")) {
10!
2885
                        // ctrl + alt is used as a screen reader command, let's not nuke it.
2886
                        adjustSelection([0, isPrimaryKey && !altKey ? 2 : 1]);
2✔
2887
                    } else {
2888
                        if (altKey && !isPrimaryKey) {
6!
2889
                            freeMove = true;
×
2890
                        }
2891
                        if (isPrimaryKey && !altKey) {
7✔
2892
                            row = rows - 1;
1✔
2893
                        } else {
2894
                            row += 1;
5✔
2895
                        }
2896
                    }
2897
                } else if (key === "ArrowUp" || key === "Home") {
44✔
2898
                    const asPrimary = key === "Home" || isPrimaryKey;
2✔
2899
                    setOverlay(undefined);
2✔
2900
                    if (shiftKey && (rangeSelect === "rect" || rangeSelect === "multi-rect")) {
2!
2901
                        // ctrl + alt is used as a screen reader command, let's not nuke it.
2902
                        adjustSelection([0, asPrimary && !altKey ? -2 : -1]);
×
2903
                    } else {
2904
                        if (altKey && !asPrimary) {
2!
2905
                            freeMove = true;
×
2906
                        }
2907
                        row += asPrimary && !altKey ? Number.MIN_SAFE_INTEGER : -1;
2✔
2908
                    }
2909
                } else if (key === "ArrowRight" || key === "End") {
34✔
2910
                    const asPrimary = key === "End" || isPrimaryKey;
8✔
2911
                    setOverlay(undefined);
8✔
2912
                    if (shiftKey && (rangeSelect === "rect" || rangeSelect === "multi-rect")) {
13!
2913
                        // ctrl + alt is used as a screen reader command, let's not nuke it.
2914
                        adjustSelection([asPrimary && !altKey ? 2 : 1, 0]);
5✔
2915
                    } else {
2916
                        if (altKey && !asPrimary) {
3!
2917
                            freeMove = true;
×
2918
                        }
2919
                        col += asPrimary && !altKey ? Number.MAX_SAFE_INTEGER : 1;
3✔
2920
                    }
2921
                } else if (key === "ArrowLeft") {
2922
                    setOverlay(undefined);
4✔
2923
                    if (shiftKey && (rangeSelect === "rect" || rangeSelect === "multi-rect")) {
5!
2924
                        // ctrl + alt is used as a screen reader command, let's not nuke it.
2925
                        adjustSelection([isPrimaryKey && !altKey ? -2 : -1, 0]);
1!
2926
                    } else {
2927
                        if (altKey && !isPrimaryKey) {
4✔
2928
                            freeMove = true;
1✔
2929
                        }
2930
                        col += isPrimaryKey && !altKey ? Number.MIN_SAFE_INTEGER : -1;
3✔
2931
                    }
2932
                } else if (key === "Tab") {
2933
                    setOverlay(undefined);
2✔
2934
                    if (shiftKey) {
2935
                        col--;
1✔
2936
                    } else {
2937
                        col++;
1✔
2938
                    }
2939
                } else if (
2940
                    !metaKey &&
49✔
2941
                    !ctrlKey &&
2942
                    gridSelection.current !== undefined &&
2943
                    key.length === 1 &&
2944
                    /[ -~]/g.test(key) &&
2945
                    bounds !== undefined &&
2946
                    isReadWriteCell(getCellContent([col - rowMarkerOffset, Math.max(0, Math.min(row, rows - 1))]))
2947
                ) {
2948
                    if (
2949
                        (!lastRowSticky || row !== rows) &&
42✔
2950
                        (vr.y > row || row > vr.y + vr.height || vr.x > col || col > vr.x + vr.width)
2951
                    ) {
2952
                        return;
×
2953
                    }
2954
                    reselect(bounds, true, key);
7✔
2955
                    cancel();
7✔
2956
                }
2957

2958
                const moved = updateSelectedCell(col, row, false, freeMove);
45✔
2959
                if (moved) {
2960
                    cancel();
18✔
2961
                }
2962
            };
2963
            void fn();
61✔
2964
        },
2965
        [
2966
            onKeyDownIn,
2967
            deleteRange,
2968
            overlay,
2969
            gridSelection,
2970
            keybindings.selectAll,
2971
            keybindings.search,
2972
            keybindings.selectColumn,
2973
            keybindings.selectRow,
2974
            keybindings.downFill,
2975
            keybindings.rightFill,
2976
            keybindings.pageDown,
2977
            keybindings.pageUp,
2978
            keybindings.first,
2979
            keybindings.last,
2980
            keybindings.clear,
2981
            columnSelect,
2982
            rowSelect,
2983
            getCellContent,
2984
            rowMarkerOffset,
2985
            updateSelectedCell,
2986
            setGridSelection,
2987
            onSelectionCleared,
2988
            columnsIn.length,
2989
            rows,
2990
            overlayID,
2991
            mangledOnCellsEdited,
2992
            onDelete,
2993
            mangledCols.length,
2994
            setSelectedColumns,
2995
            setSelectedRows,
2996
            showTrailingBlankRow,
2997
            getCustomNewRowTargetColumn,
2998
            appendRow,
2999
            onCellActivated,
3000
            reselect,
3001
            fillDown,
3002
            getMangledCellContent,
3003
            adjustSelection,
3004
            rangeSelect,
3005
            lastRowSticky,
3006
        ]
3007
    );
3008

3009
    const onContextMenu = React.useCallback(
648✔
3010
        (args: GridMouseEventArgs, preventDefault: () => void) => {
3011
            const adjustedCol = args.location[0] - rowMarkerOffset;
7✔
3012
            if (args.kind === "header") {
3013
                onHeaderContextMenu?.(adjustedCol, { ...args, preventDefault });
×
3014
            }
3015

3016
            if (args.kind === groupHeaderKind) {
3017
                if (adjustedCol < 0) {
3018
                    return;
×
3019
                }
3020
                onGroupHeaderContextMenu?.(adjustedCol, { ...args, preventDefault });
×
3021
            }
3022

3023
            if (args.kind === "cell") {
3024
                const [col, row] = args.location;
7✔
3025
                onCellContextMenu?.([adjustedCol, row], {
7!
3026
                    ...args,
3027
                    preventDefault,
3028
                });
3029

3030
                if (!gridSelectionHasItem(gridSelection, args.location)) {
3031
                    updateSelectedCell(col, row, false, false);
3✔
3032
                }
3033
            }
3034
        },
3035
        [
3036
            gridSelection,
3037
            onCellContextMenu,
3038
            onGroupHeaderContextMenu,
3039
            onHeaderContextMenu,
3040
            rowMarkerOffset,
3041
            updateSelectedCell,
3042
        ]
3043
    );
3044

3045
    const onPasteInternal = React.useCallback(
648✔
3046
        async (e?: ClipboardEvent) => {
6✔
3047
            if (!keybindings.paste) return;
6!
3048
            function pasteToCell(
3049
                inner: InnerGridCell,
3050
                target: Item,
3051
                rawValue: string | boolean | string[] | number | boolean | BooleanEmpty | BooleanIndeterminate,
3052
                formatted?: string | string[]
3053
            ): EditListItem | undefined {
3054
                const stringifiedRawValue =
3055
                    typeof rawValue === "object" ? rawValue?.join("\n") ?? "" : rawValue?.toString() ?? "";
51!
3056

3057
                if (!isInnerOnlyCell(inner) && isReadWriteCell(inner) && inner.readonly !== true) {
153✔
3058
                    const coerced = coercePasteValue?.(stringifiedRawValue, inner);
51!
3059
                    if (coerced !== undefined && isEditableGridCell(coerced)) {
51!
3060
                        if (process.env.NODE_ENV !== "production" && coerced.kind !== inner.kind) {
×
3061
                            // eslint-disable-next-line no-console
3062
                            console.warn("Coercion should not change cell kind.");
×
3063
                        }
3064
                        return {
×
3065
                            location: target,
3066
                            value: coerced,
3067
                        };
3068
                    }
3069
                    const r = getCellRenderer(inner);
51✔
3070
                    if (r === undefined) return undefined;
51!
3071
                    if (r.kind === GridCellKind.Custom) {
3072
                        assert(inner.kind === GridCellKind.Custom);
1✔
3073
                        const newVal = (r as unknown as CustomRenderer<CustomCell<any>>).onPaste?.(
1!
3074
                            stringifiedRawValue,
3075
                            inner.data
3076
                        );
3077
                        if (newVal === undefined) return undefined;
1✔
3078
                        return {
×
3079
                            location: target,
3080
                            value: {
3081
                                ...inner,
3082
                                data: newVal,
3083
                            },
3084
                        };
3085
                    } else {
3086
                        const newVal = r.onPaste?.(stringifiedRawValue, inner, {
50!
3087
                            formatted,
3088
                            formattedString: typeof formatted === "string" ? formatted : formatted?.join("\n"),
53!
3089
                            rawValue,
3090
                        });
3091
                        if (newVal === undefined) return undefined;
50✔
3092
                        assert(newVal.kind === inner.kind);
36✔
3093
                        return {
36✔
3094
                            location: target,
3095
                            value: newVal,
3096
                        };
3097
                    }
3098
                }
3099
                return undefined;
×
3100
            }
3101

3102
            const selectedColumns = gridSelection.columns;
6✔
3103
            const selectedRows = gridSelection.rows;
6✔
3104
            const focused =
3105
                scrollRef.current?.contains(document.activeElement) === true ||
6!
3106
                canvasRef.current?.contains(document.activeElement) === true;
18!
3107

3108
            let target = gridSelection.current?.cell;
6✔
3109
            if (target === undefined && selectedColumns.length === 1) {
7✔
3110
                target = [selectedColumns.first() ?? 0, 0];
×
3111
            }
3112
            if (target === undefined && selectedRows.length === 1) {
7✔
3113
                target = [rowMarkerOffset, selectedRows.first() ?? 0];
×
3114
            }
3115

3116
            if (focused && target !== undefined) {
11✔
3117
                let data: CopyBuffer | undefined;
3118
                let text: string | undefined;
3119

3120
                const textPlain = "text/plain";
5✔
3121
                const textHtml = "text/html";
5✔
3122

3123
                if (navigator.clipboard.read !== undefined) {
3124
                    const clipboardContent = await navigator.clipboard.read();
×
3125

3126
                    for (const item of clipboardContent) {
3127
                        if (item.types.includes(textHtml)) {
3128
                            const htmlBlob = await item.getType(textHtml);
×
3129
                            const html = await htmlBlob.text();
×
3130
                            const decoded = decodeHTML(html);
×
3131
                            if (decoded !== undefined) {
3132
                                data = decoded;
×
3133
                                break;
×
3134
                            }
3135
                        }
3136
                        if (item.types.includes(textPlain)) {
3137
                            // eslint-disable-next-line unicorn/no-await-expression-member
3138
                            text = await (await item.getType(textPlain)).text();
×
3139
                        }
3140
                    }
3141
                } else if (navigator.clipboard.readText !== undefined) {
3142
                    text = await navigator.clipboard.readText();
5✔
3143
                } else if (e !== undefined && e?.clipboardData !== null) {
×
3144
                    if (e.clipboardData.types.includes(textHtml)) {
3145
                        const html = e.clipboardData.getData(textHtml);
×
3146
                        data = decodeHTML(html);
×
3147
                    }
3148
                    if (data === undefined && e.clipboardData.types.includes(textPlain)) {
×
3149
                        text = e.clipboardData.getData(textPlain);
×
3150
                    }
3151
                } else {
3152
                    return; // I didn't want to read that paste value anyway
×
3153
                }
3154

3155
                const [gridCol, gridRow] = target;
5✔
3156

3157
                const editList: EditListItem[] = [];
5✔
3158
                do {
5✔
3159
                    if (onPaste === undefined) {
3160
                        const cellData = getMangledCellContent(target);
2✔
3161
                        const rawValue = text ?? data?.map(r => r.map(cb => cb.rawValue).join("\t")).join("\t") ?? "";
2!
3162
                        const newVal = pasteToCell(cellData, target, rawValue, undefined);
2✔
3163
                        if (newVal !== undefined) {
3164
                            editList.push(newVal);
1✔
3165
                        }
3166
                        break;
2✔
3167
                    }
3168

3169
                    if (data === undefined) {
3170
                        if (text === undefined) return;
3!
3171
                        data = unquote(text);
3✔
3172
                    }
3173

3174
                    if (
3175
                        onPaste === false ||
8✔
3176
                        (typeof onPaste === "function" &&
3177
                            onPaste?.(
6!
3178
                                [target[0] - rowMarkerOffset, target[1]],
3179
                                data.map(r => r.map(cb => cb.rawValue?.toString() ?? ""))
42!
3180
                            ) !== true)
3181
                    ) {
3182
                        return;
×
3183
                    }
3184

3185
                    for (const [row, dataRow] of data.entries()) {
3186
                        if (row + gridRow >= rows) break;
21!
3187
                        for (const [col, dataItem] of dataRow.entries()) {
3188
                            const index = [col + gridCol, row + gridRow] as const;
63✔
3189
                            const [writeCol, writeRow] = index;
63✔
3190
                            if (writeCol >= mangledCols.length) continue;
63✔
3191
                            if (writeRow >= mangledRows) continue;
49!
3192
                            const cellData = getMangledCellContent(index);
49✔
3193
                            const newVal = pasteToCell(cellData, index, dataItem.rawValue, dataItem.formatted);
49✔
3194
                            if (newVal !== undefined) {
3195
                                editList.push(newVal);
35✔
3196
                            }
3197
                        }
3198
                    }
3199
                    // eslint-disable-next-line no-constant-condition
3200
                } while (false);
3201

3202
                mangledOnCellsEdited(editList);
5✔
3203

3204
                gridRef.current?.damage(
5!
3205
                    editList.map(c => ({
36✔
3206
                        cell: c.location,
3207
                    }))
3208
                );
3209
            }
3210
        },
3211
        [
3212
            coercePasteValue,
3213
            getCellRenderer,
3214
            getMangledCellContent,
3215
            gridSelection,
3216
            keybindings.paste,
3217
            mangledCols.length,
3218
            mangledOnCellsEdited,
3219
            mangledRows,
3220
            onPaste,
3221
            rowMarkerOffset,
3222
            rows,
3223
        ]
3224
    );
3225

3226
    useEventListener("paste", onPasteInternal, window, false, true);
648✔
3227

3228
    // While this function is async, we deeply prefer not to await if we don't have to. This will lead to unpacking
3229
    // promises in rather awkward ways when possible to avoid awaiting. We have to use fallback copy mechanisms when
3230
    // an await has happened.
3231
    const onCopy = React.useCallback(
648✔
3232
        async (e?: ClipboardEvent, ignoreFocus?: boolean) => {
6✔
3233
            if (!keybindings.copy) return;
6!
3234
            const focused =
3235
                ignoreFocus === true ||
6✔
3236
                scrollRef.current?.contains(document.activeElement) === true ||
15!
3237
                canvasRef.current?.contains(document.activeElement) === true;
15!
3238

3239
            const selectedColumns = gridSelection.columns;
6✔
3240
            const selectedRows = gridSelection.rows;
6✔
3241

3242
            const copyToClipboardWithHeaders = (
6✔
3243
                cells: readonly (readonly GridCell[])[],
3244
                columnIndexes: readonly number[]
3245
            ) => {
3246
                if (!copyHeaders) {
3247
                    copyToClipboard(cells, columnIndexes, e);
5✔
3248
                } else {
3249
                    const headers = columnIndexes.map(index => ({
×
3250
                        kind: GridCellKind.Text,
3251
                        data: columnsIn[index].title,
3252
                        displayData: columnsIn[index].title,
3253
                        allowOverlay: false,
3254
                    })) as GridCell[];
3255
                    copyToClipboard([headers, ...cells], columnIndexes, e);
×
3256
                }
3257
            };
3258

3259
            if (focused && getCellsForSelection !== undefined) {
12✔
3260
                if (gridSelection.current !== undefined) {
3261
                    let thunk = getCellsForSelection(gridSelection.current.range, abortControllerRef.current.signal);
3✔
3262
                    if (typeof thunk !== "object") {
3263
                        thunk = await thunk();
×
3264
                    }
3265
                    copyToClipboardWithHeaders(
3✔
3266
                        thunk,
3267
                        range(
3268
                            gridSelection.current.range.x - rowMarkerOffset,
3269
                            gridSelection.current.range.x + gridSelection.current.range.width - rowMarkerOffset
3270
                        )
3271
                    );
3272
                } else if (selectedRows !== undefined && selectedRows.length > 0) {
6✔
3273
                    const toCopy = [...selectedRows];
1✔
3274
                    const cells = toCopy.map(rowIndex => {
1✔
3275
                        const thunk = getCellsForSelection(
1✔
3276
                            {
3277
                                x: rowMarkerOffset,
3278
                                y: rowIndex,
3279
                                width: columnsIn.length,
3280
                                height: 1,
3281
                            },
3282
                            abortControllerRef.current.signal
3283
                        );
3284
                        if (typeof thunk === "object") {
3285
                            return thunk[0];
1✔
3286
                        }
3287
                        return thunk().then(v => v[0]);
×
3288
                    });
3289
                    if (cells.some(x => x instanceof Promise)) {
1✔
3290
                        const settled = await Promise.all(cells);
×
3291
                        copyToClipboardWithHeaders(settled, range(columnsIn.length));
×
3292
                    } else {
3293
                        copyToClipboardWithHeaders(cells as (readonly GridCell[])[], range(columnsIn.length));
1✔
3294
                    }
3295
                } else if (selectedColumns.length > 0) {
3296
                    const results: (readonly (readonly GridCell[])[])[] = [];
1✔
3297
                    const cols: number[] = [];
1✔
3298
                    for (const col of selectedColumns) {
3299
                        let thunk = getCellsForSelection(
3✔
3300
                            {
3301
                                x: col,
3302
                                y: 0,
3303
                                width: 1,
3304
                                height: rows,
3305
                            },
3306
                            abortControllerRef.current.signal
3307
                        );
3308
                        if (typeof thunk !== "object") {
3309
                            thunk = await thunk();
×
3310
                        }
3311
                        results.push(thunk);
3✔
3312
                        cols.push(col - rowMarkerOffset);
3✔
3313
                    }
3314
                    if (results.length === 1) {
3315
                        copyToClipboardWithHeaders(results[0], cols);
×
3316
                    } else {
3317
                        // FIXME: this is dumb
3318
                        const toCopy = results.reduce((pv, cv) => pv.map((row, index) => [...row, ...cv[index]]));
2,000✔
3319
                        copyToClipboardWithHeaders(toCopy, cols);
1✔
3320
                    }
3321
                }
3322
            }
3323
        },
3324
        [columnsIn, getCellsForSelection, gridSelection, keybindings.copy, rowMarkerOffset, rows, copyHeaders]
3325
    );
3326

3327
    useEventListener("copy", onCopy, window, false, false);
648✔
3328

3329
    const onCut = React.useCallback(
648✔
3330
        async (e?: ClipboardEvent) => {
1✔
3331
            if (!keybindings.cut) return;
1!
3332
            const focused =
3333
                scrollRef.current?.contains(document.activeElement) === true ||
1!
3334
                canvasRef.current?.contains(document.activeElement) === true;
3!
3335

3336
            if (!focused) return;
1!
3337
            await onCopy(e);
1✔
3338
            if (gridSelection.current !== undefined) {
3339
                deleteRange(gridSelection.current.range);
1✔
3340
            }
3341
        },
3342
        [deleteRange, gridSelection, keybindings.cut, onCopy]
3343
    );
3344

3345
    useEventListener("cut", onCut, window, false, false);
648✔
3346

3347
    const onSearchResultsChanged = React.useCallback(
648✔
3348
        (results: readonly Item[], navIndex: number) => {
3349
            if (onSearchResultsChangedIn !== undefined) {
3350
                if (rowMarkerOffset !== 0) {
3351
                    results = results.map(item => [item[0] - rowMarkerOffset, item[1]]);
×
3352
                }
3353
                onSearchResultsChangedIn(results, navIndex);
×
3354
                return;
×
3355
            }
3356
            if (results.length === 0 || navIndex === -1) return;
7✔
3357

3358
            const [col, row] = results[navIndex];
2✔
3359
            if (lastSent.current !== undefined && lastSent.current[0] === col && lastSent.current[1] === row) {
2!
3360
                return;
×
3361
            }
3362
            lastSent.current = [col, row];
2✔
3363
            updateSelectedCell(col, row, false, false);
2✔
3364
        },
3365
        [onSearchResultsChangedIn, rowMarkerOffset, updateSelectedCell]
3366
    );
3367

3368
    // this effects purpose in life is to scroll the newly selected cell into view when and ONLY when that cell
3369
    // is from an external gridSelection change. Also note we want the unmangled out selection because scrollTo
3370
    // expects unmangled indexes
3371
    const [outCol, outRow] = gridSelectionOuter?.current?.cell ?? [];
648✔
3372
    const scrollToRef = React.useRef(scrollTo);
648✔
3373
    scrollToRef.current = scrollTo;
648✔
3374
    React.useLayoutEffect(() => {
648✔
3375
        if (
3376
            !hasJustScrolled.current &&
638✔
3377
            outCol !== undefined &&
3378
            outRow !== undefined &&
3379
            (outCol !== expectedExternalGridSelection.current?.current?.cell[0] ||
456!
3380
                outRow !== expectedExternalGridSelection.current?.current?.cell[1])
456!
3381
        ) {
3382
            scrollToRef.current(outCol, outRow);
×
3383
        }
3384
        hasJustScrolled.current = false; //only allow skipping a single scroll
205✔
3385
    }, [outCol, outRow]);
3386

3387
    const selectionOutOfBounds =
3388
        gridSelection.current !== undefined &&
648✔
3389
        (gridSelection.current.cell[0] >= mangledCols.length || gridSelection.current.cell[1] >= mangledRows);
3390
    React.useLayoutEffect(() => {
648✔
3391
        if (selectionOutOfBounds) {
3392
            setGridSelection(emptyGridSelection, false);
1✔
3393
        }
3394
    }, [selectionOutOfBounds, setGridSelection]);
3395

3396
    const disabledRows = React.useMemo(() => {
648✔
3397
        if (showTrailingBlankRow === true && trailingRowOptions?.tint === true) {
670!
3398
            return CompactSelection.fromSingleSelection(mangledRows - 1);
131✔
3399
        }
3400
        return CompactSelection.empty();
3✔
3401
    }, [mangledRows, showTrailingBlankRow, trailingRowOptions?.tint]);
1,944!
3402

3403
    const mangledVerticalBorder = React.useCallback(
648✔
3404
        (col: number) => {
3405
            return typeof verticalBorder === "boolean"
7,117!
3406
                ? verticalBorder
3407
                : verticalBorder?.(col - rowMarkerOffset) ?? true;
42,702!
3408
        },
3409
        [rowMarkerOffset, verticalBorder]
3410
    );
3411

3412
    const renameGroupNode = React.useMemo(() => {
648✔
3413
        if (renameGroup === undefined || canvasRef.current === null) return null;
135✔
3414
        const { bounds, group } = renameGroup;
2✔
3415
        const canvasBounds = canvasRef.current.getBoundingClientRect();
2✔
3416
        return (
2✔
3417
            <GroupRename
3418
                bounds={bounds}
3419
                group={group}
3420
                canvasBounds={canvasBounds}
3421
                onClose={() => setRenameGroup(undefined)}
×
3422
                onFinish={newVal => {
3423
                    setRenameGroup(undefined);
1✔
3424
                    onGroupHeaderRenamed?.(group, newVal);
1!
3425
                }}
3426
            />
3427
        );
3428
    }, [onGroupHeaderRenamed, renameGroup]);
3429

3430
    const mangledFreezeColumns = Math.min(mangledCols.length, freezeColumns + (hasRowMarkers ? 1 : 0));
648✔
3431

3432
    React.useImperativeHandle(
648✔
3433
        forwardedRef,
3434
        () => ({
26✔
3435
            appendRow: (col: number, openOverlay?: boolean) => appendRow(col + rowMarkerOffset, openOverlay),
×
3436
            updateCells: damageList => {
3437
                if (rowMarkerOffset !== 0) {
3438
                    damageList = damageList.map(x => ({ cell: [x.cell[0] + rowMarkerOffset, x.cell[1]] }));
1✔
3439
                }
3440
                return gridRef.current?.damage(damageList);
2!
3441
            },
3442
            getBounds: (col, row) => {
3443
                return gridRef.current?.getBounds(col + rowMarkerOffset, row);
×
3444
            },
3445
            focus: () => gridRef.current?.focus(),
3!
3446
            emit: async e => {
5✔
3447
                switch (e) {
3448
                    case "delete":
5✔
3449
                        onKeyDown({
1✔
3450
                            bounds: undefined,
3451
                            cancel: () => undefined,
×
3452
                            stopPropagation: () => undefined,
1✔
3453
                            preventDefault: () => undefined,
1✔
3454
                            ctrlKey: false,
3455
                            key: "Delete",
3456
                            keyCode: 46,
3457
                            metaKey: false,
3458
                            shiftKey: false,
3459
                            altKey: false,
3460
                            rawEvent: undefined,
3461
                            location: undefined,
3462
                        });
3463
                        break;
1✔
3464
                    case "fill-right":
3465
                        onKeyDown({
1✔
3466
                            bounds: undefined,
3467
                            cancel: () => undefined,
×
3468
                            stopPropagation: () => undefined,
×
3469
                            preventDefault: () => undefined,
×
3470
                            ctrlKey: true,
3471
                            key: "r",
3472
                            keyCode: 82,
3473
                            metaKey: false,
3474
                            shiftKey: false,
3475
                            altKey: false,
3476
                            rawEvent: undefined,
3477
                            location: undefined,
3478
                        });
3479
                        break;
1✔
3480
                    case "fill-down":
3481
                        onKeyDown({
1✔
3482
                            bounds: undefined,
3483
                            cancel: () => undefined,
×
3484
                            stopPropagation: () => undefined,
×
3485
                            preventDefault: () => undefined,
×
3486
                            ctrlKey: true,
3487
                            key: "d",
3488
                            keyCode: 68,
3489
                            metaKey: false,
3490
                            shiftKey: false,
3491
                            altKey: false,
3492
                            rawEvent: undefined,
3493
                            location: undefined,
3494
                        });
3495
                        break;
1✔
3496
                    case "copy":
3497
                        await onCopy(undefined, true);
1✔
3498
                        break;
1✔
3499
                    case "paste":
3500
                        await onPasteInternal();
1✔
3501
                        break;
1✔
3502
                }
3503
            },
3504
            scrollTo,
3505
            remeasureColumns: cols => {
3506
                for (const col of cols) {
3507
                    void normalSizeColumn(col + rowMarkerOffset, true);
1✔
3508
                }
3509
            },
3510
        }),
3511
        [appendRow, normalSizeColumn, onCopy, onKeyDown, onPasteInternal, rowMarkerOffset, scrollTo]
3512
    );
3513

3514
    const [selCol, selRow] = currentCell ?? [];
648✔
3515
    const onCellFocused = React.useCallback(
648✔
3516
        (cell: Item) => {
3517
            const [col, row] = cell;
24✔
3518

3519
            if (row === -1) {
3520
                if (columnSelect !== "none") {
3521
                    setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, false);
×
3522
                    focus();
×
3523
                }
3524
                return;
×
3525
            }
3526

3527
            if (selCol === col && selRow === row) return;
24✔
3528
            setCurrent(
1✔
3529
                {
3530
                    cell,
3531
                    range: { x: col, y: row, width: 1, height: 1 },
3532
                },
3533
                true,
3534
                false,
3535
                "keyboard-nav"
3536
            );
3537
            scrollTo(col, row);
1✔
3538
        },
3539
        [columnSelect, focus, scrollTo, selCol, selRow, setCurrent, setSelectedColumns]
3540
    );
3541

3542
    const [isFocused, setIsFocused] = React.useState(false);
648✔
3543
    const setIsFocusedDebounced = React.useRef(
648✔
3544
        debounce((val: boolean) => {
3545
            setIsFocused(val);
46✔
3546
        }, 5)
3547
    );
3548

3549
    const onCanvasFocused = React.useCallback(() => {
648✔
3550
        setIsFocusedDebounced.current(true);
57✔
3551

3552
        // check for mouse state, don't do anything if the user is clicked to focus.
3553
        if (
3554
            gridSelection.current === undefined &&
73✔
3555
            gridSelection.columns.length === 0 &&
3556
            gridSelection.rows.length === 0 &&
3557
            mouseState === undefined
3558
        ) {
3559
            setCurrent(
5✔
3560
                {
3561
                    cell: [rowMarkerOffset, cellYOffset],
3562
                    range: {
3563
                        x: rowMarkerOffset,
3564
                        y: cellYOffset,
3565
                        width: 1,
3566
                        height: 1,
3567
                    },
3568
                },
3569
                true,
3570
                false,
3571
                "keyboard-select"
3572
            );
3573
        }
3574
    }, [cellYOffset, gridSelection, mouseState, rowMarkerOffset, setCurrent]);
3575

3576
    const onFocusOut = React.useCallback(() => {
648✔
3577
        setIsFocusedDebounced.current(false);
27✔
3578
    }, []);
3579

3580
    const [idealWidth, idealHeight] = React.useMemo(() => {
648✔
3581
        let h: number;
3582
        const scrollbarWidth = experimental?.scrollbarWidthOverride ?? getScrollBarWidth();
147!
3583
        const rowsCountWithTrailingRow = rows + (showTrailingBlankRow ? 1 : 0);
147!
3584
        if (typeof rowHeight === "number") {
3585
            h = totalHeaderHeight + rowsCountWithTrailingRow * rowHeight;
146✔
3586
        } else {
3587
            let avg = 0;
1✔
3588
            const toAverage = Math.min(rowsCountWithTrailingRow, 10);
1✔
3589
            for (let i = 0; i < toAverage; i++) {
1✔
3590
                avg += rowHeight(i);
10✔
3591
            }
3592
            avg = Math.floor(avg / toAverage);
1✔
3593

3594
            h = totalHeaderHeight + rowsCountWithTrailingRow * avg;
1✔
3595
        }
3596
        h += scrollbarWidth;
147✔
3597

3598
        const w = mangledCols.reduce((acc, x) => x.width + acc, 0) + scrollbarWidth;
1,627✔
3599

3600
        // We need to set a reasonable cap here as some browsers will just ignore huge values
3601
        // rather than treat them as huge values.
3602
        return [`${Math.min(100_000, w)}px`, `${Math.min(100_000, h)}px`];
147✔
3603
    }, [mangledCols, experimental?.scrollbarWidthOverride, rowHeight, rows, showTrailingBlankRow, totalHeaderHeight]);
1,944✔
3604

3605
    return (
648✔
3606
        <ThemeContext.Provider value={mergedTheme}>
3607
            <DataEditorContainer
3608
                style={makeCSSStyle(mergedTheme)}
3609
                className={className}
3610
                inWidth={width ?? idealWidth}
1,944!
3611
                inHeight={height ?? idealHeight}>
1,944!
3612
                <DataGridSearch
3613
                    fillHandle={fillHandle}
3614
                    drawFocusRing={drawFocusRing}
3615
                    experimental={experimental}
3616
                    fixedShadowX={fixedShadowX}
3617
                    fixedShadowY={fixedShadowY}
3618
                    getRowThemeOverride={getRowThemeOverride}
3619
                    headerIcons={headerIcons}
3620
                    imageWindowLoader={imageWindowLoader}
3621
                    initialSize={initialSize}
3622
                    isDraggable={isDraggable}
3623
                    onDragLeave={onDragLeave}
3624
                    onRowMoved={onRowMoved}
3625
                    overscrollX={overscrollX}
3626
                    overscrollY={overscrollY}
3627
                    preventDiagonalScrolling={preventDiagonalScrolling}
3628
                    rightElement={rightElement}
3629
                    rightElementProps={rightElementProps}
3630
                    showMinimap={showMinimap}
3631
                    smoothScrollX={smoothScrollX}
3632
                    smoothScrollY={smoothScrollY}
3633
                    className={className}
3634
                    enableGroups={enableGroups}
3635
                    onCanvasFocused={onCanvasFocused}
3636
                    onCanvasBlur={onFocusOut}
3637
                    canvasRef={canvasRef}
3638
                    onContextMenu={onContextMenu}
3639
                    theme={mergedTheme}
3640
                    cellXOffset={cellXOffset}
3641
                    cellYOffset={cellYOffset}
3642
                    accessibilityHeight={visibleRegion.height}
3643
                    onDragEnd={onDragEnd}
3644
                    columns={mangledCols}
3645
                    drawCustomCell={drawCell}
3646
                    drawHeader={drawHeader}
3647
                    disabledRows={disabledRows}
3648
                    freezeColumns={mangledFreezeColumns}
3649
                    lockColumns={rowMarkerOffset}
3650
                    firstColAccessible={rowMarkerOffset === 0}
3651
                    getCellContent={getMangledCellContent}
3652
                    minColumnWidth={minColumnWidth}
3653
                    maxColumnWidth={maxColumnWidth}
3654
                    searchInputRef={searchInputRef}
3655
                    showSearch={showSearch}
3656
                    onSearchClose={onSearchClose}
3657
                    highlightRegions={highlightRegions}
3658
                    getCellsForSelection={getCellsForSelection}
3659
                    getGroupDetails={mangledGetGroupDetails}
3660
                    headerHeight={headerHeight}
3661
                    isFocused={isFocused}
3662
                    groupHeaderHeight={enableGroups ? groupHeaderHeight : 0}
648✔
3663
                    trailingRowType={
3664
                        !showTrailingBlankRow ? "none" : trailingRowOptions?.sticky === true ? "sticky" : "appended"
3,240!
3665
                    }
3666
                    onColumnResize={onColumnResize}
3667
                    onColumnResizeEnd={onColumnResizeEnd}
3668
                    onColumnResizeStart={onColumnResizeStart}
3669
                    onCellFocused={onCellFocused}
3670
                    onColumnMoved={onColumnMovedImpl}
3671
                    onDragStart={onDragStartImpl}
3672
                    onHeaderMenuClick={onHeaderMenuClickInner}
3673
                    onItemHovered={onItemHoveredImpl}
3674
                    isFilling={mouseState?.fillHandle === true}
1,944✔
3675
                    onMouseMove={onMouseMoveImpl}
3676
                    onKeyDown={onKeyDown}
3677
                    onKeyUp={onKeyUpIn}
3678
                    onMouseDown={onMouseDown}
3679
                    onMouseUp={onMouseUp}
3680
                    onDragOverCell={onDragOverCell}
3681
                    onDrop={onDrop}
3682
                    onSearchResultsChanged={onSearchResultsChanged}
3683
                    onVisibleRegionChanged={onVisibleRegionChangedImpl}
3684
                    clientSize={[clientSize[0], clientSize[1]]}
3685
                    rowHeight={rowHeight}
3686
                    searchResults={searchResults}
3687
                    searchValue={searchValue}
3688
                    onSearchValueChange={onSearchValueChange}
3689
                    rows={mangledRows}
3690
                    scrollRef={scrollRef}
3691
                    selection={gridSelection}
3692
                    translateX={visibleRegion.tx}
3693
                    translateY={visibleRegion.ty}
3694
                    verticalBorder={mangledVerticalBorder}
3695
                    gridRef={gridRef}
3696
                    getCellRenderer={getCellRenderer}
3697
                    scrollToEnd={scrollToEnd}
3698
                />
3699
                {renameGroupNode}
3700
                {overlay !== undefined && (
672✔
3701
                    <DataGridOverlayEditor
3702
                        {...overlay}
3703
                        validateCell={validateCell}
3704
                        id={overlayID}
3705
                        getCellRenderer={getCellRenderer}
3706
                        className={experimental?.isSubGrid === true ? "click-outside-ignore" : undefined}
96!
3707
                        provideEditor={provideEditor}
3708
                        imageEditorOverride={imageEditorOverride}
3709
                        onFinishEditing={onFinishEditing}
3710
                        markdownDivCreateNode={markdownDivCreateNode}
3711
                        isOutsideClick={isOutsideClick}
3712
                    />
3713
                )}
3714
            </DataEditorContainer>
3715
        </ThemeContext.Provider>
3716
    );
3717
};
3718

3719
/**
3720
 * The primary component of Glide Data Grid.
3721
 * @category DataEditor
3722
 * @param {DataEditorProps} props
3723
 */
3724
export const DataEditor = React.forwardRef(DataEditorImpl);
9✔
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