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

glideapps / glide-data-grid / 7028070260

29 Nov 2023 04:34AM UTC coverage: 91.033% (+4.6%) from 86.42%
7028070260

Pull #810

github

jassmith
5.99.9-alpha5
Pull Request #810: 6.0.0

2458 of 3061 branches covered (0.0%)

1024 of 1167 new or added lines in 54 files covered. (87.75%)

286 existing lines in 12 files now uncovered.

15066 of 16550 relevant lines covered (91.03%)

2907.47 hits per line

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

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

1✔
68
let idCounter = 0;
1✔
69

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

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

1✔
125
type EditListItem = { location: Item; value: EditableGridCell };
1✔
126

1✔
127
type EmitEvents = "copy" | "paste" | "delete" | "fill-right" | "fill-down";
1✔
128

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
607
    readonly renderers?: readonly InternalCellRenderer<InnerGridCell>[];
1✔
608

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

1✔
615
    readonly scaleToRem?: boolean;
1✔
616

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

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

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

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

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

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

649✔
700
    const safeWindow = typeof window === "undefined" ? null : window;
649!
701

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

649✔
808
    const minColumnWidth = Math.max(minColumnWidthIn, 20);
649✔
809
    const maxColumnWidth = Math.max(maxColumnWidthIn, minColumnWidth);
649✔
810
    const maxColumnAutoWidth = Math.max(maxColumnAutoWidthIn ?? maxColumnWidth, minColumnWidth);
649✔
811

649✔
812
    const docStyle = React.useMemo(() => {
649✔
813
        if (typeof window === "undefined") return { fontSize: "16px" };
132!
814
        return window.getComputedStyle(document.documentElement);
132✔
815
    }, []);
649✔
816

649✔
817
    const fontSizeStr = docStyle.fontSize;
649✔
818

649✔
819
    const remSize = React.useMemo(() => Number.parseFloat(fontSizeStr), [fontSizeStr]);
649✔
820

649✔
821
    const { rowHeight, headerHeight, groupHeaderHeight, theme, overscrollX, overscrollY } = useRemAdjuster({
649✔
822
        groupHeaderHeight: groupHeaderHeightIn,
649✔
823
        headerHeight: headerHeightIn,
649✔
824
        overscrollX: overscrollXIn,
649✔
825
        overscrollY: overscrollYIn,
649✔
826
        remSize,
649✔
827
        rowHeight: rowHeightIn,
649✔
828
        scaleToRem,
649✔
829
        theme: themeIn,
649✔
830
    });
649✔
831

649✔
832
    const keybindings = React.useMemo(() => {
649✔
833
        return keybindingsIn === undefined
132✔
834
            ? keybindingDefaults
129✔
835
            : {
3✔
836
                  ...keybindingDefaults,
3✔
837
                  ...keybindingsIn,
3✔
838
              };
3✔
839
    }, [keybindingsIn]);
649✔
840

649✔
841
    const rowMarkerWidth = rowMarkerWidthRaw ?? (rows > 10_000 ? 48 : rows > 1000 ? 44 : rows > 100 ? 36 : 32);
649!
842
    const hasRowMarkers = rowMarkers !== "none";
649✔
843
    const rowMarkerOffset = hasRowMarkers ? 1 : 0;
649✔
844
    const showTrailingBlankRow = onRowAppended !== undefined;
649✔
845
    const lastRowSticky = trailingRowOptions?.sticky === true;
649✔
846

649✔
847
    const [showSearchInner, setShowSearchInner] = React.useState(false);
649✔
848
    const showSearch = showSearchIn ?? showSearchInner;
649✔
849

649✔
850
    const onSearchClose = React.useCallback(() => {
649✔
851
        if (onSearchCloseIn !== undefined) {
2✔
852
            onSearchCloseIn();
2✔
853
        } else {
2!
854
            setShowSearchInner(false);
×
UNCOV
855
        }
×
856
    }, [onSearchCloseIn]);
649✔
857

649✔
858
    const gridSelectionOuterMangled: GridSelection | undefined = React.useMemo((): GridSelection | undefined => {
649✔
859
        return gridSelectionOuter === undefined ? undefined : shiftSelection(gridSelectionOuter, rowMarkerOffset);
255✔
860
    }, [gridSelectionOuter, rowMarkerOffset]);
649✔
861
    const gridSelection = gridSelectionOuterMangled ?? gridSelectionInner;
649✔
862

649✔
863
    const abortControllerRef = React.useRef(new AbortController());
649✔
864
    React.useEffect(() => {
649✔
865
        return () => {
132✔
866
            // eslint-disable-next-line react-hooks/exhaustive-deps
132✔
867
            abortControllerRef?.current.abort();
132✔
868
        };
132✔
869
    }, []);
649✔
870

649✔
871
    const [getCellsForSelection, getCellsForSeletionDirect] = useCellsForSelection(
649✔
872
        getCellsForSelectionIn,
649✔
873
        getCellContent,
649✔
874
        rowMarkerOffset,
649✔
875
        abortControllerRef.current,
649✔
876
        rows
649✔
877
    );
649✔
878

649✔
879
    const validateCell = React.useCallback<NonNullable<typeof validateCellIn>>(
649✔
880
        (cell, newValue, prevValue) => {
649✔
881
            if (validateCellIn === undefined) return true;
16✔
882
            const item: Item = [cell[0] - rowMarkerOffset, cell[1]];
1✔
883
            return validateCellIn?.(item, newValue, prevValue);
1✔
884
        },
16✔
885
        [rowMarkerOffset, validateCellIn]
649✔
886
    );
649✔
887

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

649✔
910
    const onColumnResize = whenDefined(
649✔
911
        onColumnResizeIn,
649✔
912
        React.useCallback<NonNullable<typeof onColumnResizeIn>>(
649✔
913
            (_, w, ind, wg) => {
649✔
914
                onColumnResizeIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
11✔
915
            },
11✔
916
            [onColumnResizeIn, rowMarkerOffset, columnsIn]
649✔
917
        )
649✔
918
    );
649✔
919

649✔
920
    const onColumnResizeEnd = whenDefined(
649✔
921
        onColumnResizeEndIn,
649✔
922
        React.useCallback<NonNullable<typeof onColumnResizeEndIn>>(
649✔
923
            (_, w, ind, wg) => {
649✔
924
                onColumnResizeEndIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
2✔
925
            },
2✔
926
            [onColumnResizeEndIn, rowMarkerOffset, columnsIn]
649✔
927
        )
649✔
928
    );
649✔
929

649✔
930
    const onColumnResizeStart = whenDefined(
649✔
931
        onColumnResizeStartIn,
649✔
932
        React.useCallback<NonNullable<typeof onColumnResizeStartIn>>(
649✔
933
            (_, w, ind, wg) => {
649✔
934
                onColumnResizeStartIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
×
UNCOV
935
            },
×
936
            [onColumnResizeStartIn, rowMarkerOffset, columnsIn]
649✔
937
        )
649✔
938
    );
649✔
939

649✔
940
    const drawHeader = whenDefined(
649✔
941
        drawHeaderIn,
649✔
942
        React.useCallback<NonNullable<typeof drawHeaderIn>>(
649✔
943
            args => {
649✔
944
                return drawHeaderIn?.({ ...args, columnIndex: args.columnIndex - rowMarkerOffset }) ?? false;
×
UNCOV
945
            },
×
946
            [drawHeaderIn, rowMarkerOffset]
649✔
947
        )
649✔
948
    );
649✔
949

649✔
950
    const onDelete = React.useCallback<NonNullable<DataEditorProps["onDelete"]>>(
649✔
951
        sel => {
649✔
952
            if (onDeleteIn !== undefined) {
8✔
953
                const result = onDeleteIn(shiftSelection(sel, -rowMarkerOffset));
5✔
954
                if (typeof result === "boolean") {
5!
955
                    return result;
×
UNCOV
956
                }
×
957
                return shiftSelection(result, rowMarkerOffset);
5✔
958
            }
5✔
959
            return true;
3✔
960
        },
8✔
961
        [onDeleteIn, rowMarkerOffset]
649✔
962
    );
649✔
963

649✔
964
    const [setCurrent, setSelectedRows, setSelectedColumns] = useSelectionBehavior(
649✔
965
        gridSelection,
649✔
966
        setGridSelection,
649✔
967
        rangeSelectionBlending,
649✔
968
        columnSelectionBlending,
649✔
969
        rowSelectionBlending,
649✔
970
        rangeSelect
649✔
971
    );
649✔
972

649✔
973
    const mergedTheme = React.useMemo(() => {
649✔
974
        return { ...getDataEditorTheme(), ...theme };
132✔
975
    }, [theme]);
649✔
976

649✔
977
    const [clientSize, setClientSize] = React.useState<readonly [number, number, number]>([10, 10, 0]);
649✔
978

649✔
979
    const rendererMap = React.useMemo(() => {
649✔
980
        if (renderers === undefined) return {};
132!
981
        const result: Partial<Record<InnerGridCellKind | GridCellKind, InternalCellRenderer<InnerGridCell>>> = {};
132✔
982
        for (const r of renderers) {
132✔
983
            result[r.kind] = r;
1,716✔
984
        }
1,716✔
985
        return result;
132✔
986
    }, [renderers]);
649✔
987

649✔
988
    const getCellRenderer: <T extends InnerGridCell>(cell: T) => CellRenderer<T> | undefined = React.useCallback(
649✔
989
        <T extends InnerGridCell>(cell: T) => {
649✔
990
            if (cell.kind !== GridCellKind.Custom) {
133,307✔
991
                return rendererMap[cell.kind] as unknown as CellRenderer<T>;
129,577✔
992
            }
129,577✔
993
            return additionalRenderers?.find(x => x.isMatch(cell)) as CellRenderer<T>;
133,307✔
994
        },
133,307✔
995
        [additionalRenderers, rendererMap]
649✔
996
    );
649✔
997

649✔
998
    const columns = useColumnSizer(
649✔
999
        columnsIn,
649✔
1000
        rows,
649✔
1001
        getCellsForSeletionDirect,
649✔
1002
        clientSize[0] - (rowMarkerOffset === 0 ? 0 : rowMarkerWidth) - clientSize[2],
649✔
1003
        minColumnWidth,
649✔
1004
        maxColumnAutoWidth,
649✔
1005
        mergedTheme,
649✔
1006
        getCellRenderer,
649✔
1007
        abortControllerRef.current
649✔
1008
    );
649✔
1009

649✔
1010
    const enableGroups = React.useMemo(() => {
649✔
1011
        return columns.some(c => c.group !== undefined);
132✔
1012
    }, [columns]);
649✔
1013

649✔
1014
    const totalHeaderHeight = enableGroups ? headerHeight + groupHeaderHeight : headerHeight;
649✔
1015

649✔
1016
    const numSelectedRows = gridSelection.rows.length;
649✔
1017
    const rowMarkerHeader =
649✔
1018
        rowMarkers === "none"
649✔
1019
            ? ""
517✔
1020
            : numSelectedRows === 0
131✔
1021
            ? headerCellUnheckedMarker
87✔
1022
            : numSelectedRows === rows
44✔
1023
            ? headerCellCheckedMarker
6✔
1024
            : headerCellIndeterminateMarker;
38✔
1025

649✔
1026
    const mangledCols = React.useMemo(() => {
649✔
1027
        if (rowMarkers === "none") return columns;
146✔
1028
        return [
45✔
1029
            {
45✔
1030
                title: rowMarkerHeader,
45✔
1031
                width: rowMarkerWidth,
45✔
1032
                icon: undefined,
45✔
1033
                hasMenu: false,
45✔
1034
                style: "normal" as const,
45✔
1035
                themeOverride: rowMarkerTheme,
45✔
1036
            },
45✔
1037
            ...columns,
45✔
1038
        ];
45✔
1039
    }, [columns, rowMarkerWidth, rowMarkers, rowMarkerHeader, rowMarkerTheme]);
649✔
1040

649✔
1041
    const [visibleRegionY, visibleRegionTy] = React.useMemo(() => {
649✔
1042
        return [
132✔
1043
            scrollOffsetY !== undefined && typeof rowHeight === "number" ? Math.floor(scrollOffsetY / rowHeight) : 0,
132!
1044
            scrollOffsetY !== undefined && typeof rowHeight === "number" ? -(scrollOffsetY % rowHeight) : 0,
132!
1045
        ];
132✔
1046
    }, [scrollOffsetY, rowHeight]);
649✔
1047

649✔
1048
    type VisibleRegion = Rectangle & {
649✔
1049
        /** value in px */
649✔
1050
        tx?: number;
649✔
1051
        /** value in px */
649✔
1052
        ty?: number;
649✔
1053
        extras?: {
649✔
1054
            selected?: Item;
649✔
1055
            freezeRegion?: Rectangle;
649✔
1056
        };
649✔
1057
    };
649✔
1058

649✔
1059
    const visibleRegionRef = React.useRef<VisibleRegion>({
649✔
1060
        height: 1,
649✔
1061
        width: 1,
649✔
1062
        x: 0,
649✔
1063
        y: 0,
649✔
1064
    });
649✔
1065
    const visibleRegionInput = React.useMemo<VisibleRegion>(
649✔
1066
        () => ({
649✔
1067
            x: visibleRegionRef.current.x,
132✔
1068
            y: visibleRegionY,
132✔
1069
            width: visibleRegionRef.current.width ?? 1,
132!
1070
            height: visibleRegionRef.current.height ?? 1,
132!
1071
            // tx: 'TODO',
132✔
1072
            ty: visibleRegionTy,
132✔
1073
        }),
132✔
1074
        [visibleRegionTy, visibleRegionY]
649✔
1075
    );
649✔
1076

649✔
1077
    const hasJustScrolled = React.useRef(false);
649✔
1078

649✔
1079
    const [visibleRegion, setVisibleRegion, empty] = useStateWithReactiveInput<VisibleRegion>(visibleRegionInput);
649✔
1080

649✔
1081
    const vScrollReady = (visibleRegion.height ?? 1) > 1;
649!
1082
    React.useLayoutEffect(() => {
649✔
1083
        if (scrollOffsetY !== undefined && scrollRef.current !== null && vScrollReady) {
264!
1084
            if (scrollRef.current.scrollTop === scrollOffsetY) return;
×
1085
            scrollRef.current.scrollTop = scrollOffsetY;
×
UNCOV
1086
            if (scrollRef.current.scrollTop !== scrollOffsetY) {
×
1087
                empty();
×
UNCOV
1088
            }
×
1089
            hasJustScrolled.current = true;
×
UNCOV
1090
        }
×
1091
    }, [scrollOffsetY, vScrollReady, empty]);
649✔
1092

649✔
1093
    const hScrollReady = (visibleRegion.width ?? 1) > 1;
649!
1094
    React.useLayoutEffect(() => {
649✔
1095
        if (scrollOffsetX !== undefined && scrollRef.current !== null && hScrollReady) {
264!
1096
            if (scrollRef.current.scrollLeft === scrollOffsetX) return;
×
1097
            scrollRef.current.scrollLeft = scrollOffsetX;
×
UNCOV
1098
            if (scrollRef.current.scrollLeft !== scrollOffsetX) {
×
1099
                empty();
×
UNCOV
1100
            }
×
1101
            hasJustScrolled.current = true;
×
UNCOV
1102
        }
×
1103
    }, [scrollOffsetX, hScrollReady, empty]);
649✔
1104

649✔
1105
    const cellXOffset = visibleRegion.x + rowMarkerOffset;
649✔
1106
    const cellYOffset = visibleRegion.y;
649✔
1107

649✔
1108
    const gridRef = React.useRef<DataGridRef | null>(null);
649✔
1109

649✔
1110
    const focus = React.useCallback((immediate?: boolean) => {
649✔
1111
        if (immediate === true) {
120✔
1112
            gridRef.current?.focus();
7✔
1113
        } else {
120✔
1114
            window.requestAnimationFrame(() => {
113✔
1115
                gridRef.current?.focus();
112✔
1116
            });
113✔
1117
        }
113✔
1118
    }, []);
649✔
1119

649✔
1120
    const mangledRows = showTrailingBlankRow ? rows + 1 : rows;
649!
1121

649✔
1122
    const mangledOnCellsEdited = React.useCallback<NonNullable<typeof onCellsEdited>>(
649✔
1123
        (items: readonly EditListItem[]) => {
649✔
1124
            const mangledItems =
26✔
1125
                rowMarkerOffset === 0
26✔
1126
                    ? items
21✔
1127
                    : items.map(x => ({
5✔
1128
                          ...x,
29✔
1129
                          location: [x.location[0] - rowMarkerOffset, x.location[1]] as const,
29✔
1130
                      }));
5✔
1131
            const r = onCellsEdited?.(mangledItems);
26✔
1132

26✔
1133
            if (r !== true) {
26✔
1134
                for (const i of mangledItems) onCellEdited?.(i.location, i.value);
25✔
1135
            }
25✔
1136

26✔
1137
            return r;
26✔
1138
        },
26✔
1139
        [onCellEdited, onCellsEdited, rowMarkerOffset]
649✔
1140
    );
649✔
1141

649✔
1142
    const highlightRegions = React.useMemo(() => {
649✔
1143
        if (highlightRegionsIn === undefined) return undefined;
132✔
1144
        if (rowMarkerOffset === 0) return highlightRegionsIn;
1!
UNCOV
1145

×
1146
        return highlightRegionsIn
×
UNCOV
1147
            .map(r => {
×
1148
                const maxWidth = mangledCols.length - r.range.x - rowMarkerOffset;
×
1149
                if (maxWidth <= 0) return undefined;
×
1150
                return {
×
UNCOV
1151
                    color: r.color,
×
UNCOV
1152
                    range: {
×
UNCOV
1153
                        ...r.range,
×
UNCOV
1154
                        x: r.range.x + rowMarkerOffset,
×
UNCOV
1155
                        width: Math.min(maxWidth, r.range.width),
×
UNCOV
1156
                    },
×
UNCOV
1157
                    style: r.style,
×
UNCOV
1158
                };
×
UNCOV
1159
            })
×
1160
            .filter(x => x !== undefined) as typeof highlightRegionsIn;
×
1161
    }, [highlightRegionsIn, mangledCols.length, rowMarkerOffset]);
649✔
1162

649✔
1163
    const mangledColsRef = React.useRef(mangledCols);
649✔
1164
    mangledColsRef.current = mangledCols;
649✔
1165
    const getMangledCellContent = React.useCallback(
649✔
1166
        ([col, row]: Item, forceStrict: boolean = false): InnerGridCell => {
649✔
1167
            const isTrailing = showTrailingBlankRow && row === mangledRows - 1;
130,457✔
1168
            const isRowMarkerCol = col === 0 && hasRowMarkers;
130,457✔
1169
            if (isRowMarkerCol) {
130,457✔
1170
                if (isTrailing) {
2,183✔
1171
                    return loadingCell;
98✔
1172
                }
98✔
1173
                return {
2,085✔
1174
                    kind: InnerGridCellKind.Marker,
2,085✔
1175
                    allowOverlay: false,
2,085✔
1176
                    checked: gridSelection?.rows.hasIndex(row) === true,
2,183✔
1177
                    markerKind: rowMarkers === "clickable-number" ? "number" : rowMarkers,
2,183!
1178
                    row: rowMarkerStartIndex + row,
2,183✔
1179
                    drawHandle: onRowMoved !== undefined,
2,183✔
1180
                    cursor: rowMarkers === "clickable-number" ? "pointer" : undefined,
2,183!
1181
                };
2,183✔
1182
            } else if (isTrailing) {
130,457✔
1183
                //If the grid is empty, we will return text
3,647✔
1184
                const isFirst = col === rowMarkerOffset;
3,647✔
1185

3,647✔
1186
                const maybeFirstColumnHint = isFirst ? trailingRowOptions?.hint ?? "" : "";
3,647✔
1187
                const c = mangledColsRef.current[col];
3,647✔
1188

3,647✔
1189
                if (c?.trailingRowOptions?.disabled === true) {
3,647!
1190
                    return loadingCell;
×
1191
                } else {
3,647✔
1192
                    const hint = c?.trailingRowOptions?.hint ?? maybeFirstColumnHint;
3,647!
1193
                    const icon = c?.trailingRowOptions?.addIcon ?? trailingRowOptions?.addIcon;
3,647!
1194
                    return {
3,647✔
1195
                        kind: InnerGridCellKind.NewRow,
3,647✔
1196
                        hint,
3,647✔
1197
                        allowOverlay: false,
3,647✔
1198
                        icon,
3,647✔
1199
                    };
3,647✔
1200
                }
3,647✔
1201
            } else {
128,274✔
1202
                const outerCol = col - rowMarkerOffset;
124,627✔
1203
                if (forceStrict || experimental?.strict === true) {
124,627✔
1204
                    const vr = visibleRegionRef.current;
21,679✔
1205
                    const isOutsideMainArea =
21,679✔
1206
                        vr.x > outerCol || outerCol > vr.x + vr.width || vr.y > row || row > vr.y + vr.height;
21,679✔
1207
                    const isSelected = outerCol === vr.extras?.selected?.[0] && row === vr.extras?.selected[1];
21,679!
1208
                    const isOutsideFreezeArea =
21,679✔
1209
                        vr.extras?.freezeRegion === undefined ||
21,679!
UNCOV
1210
                        vr.extras.freezeRegion.x > outerCol ||
×
UNCOV
1211
                        outerCol > vr.extras.freezeRegion.x + vr.extras.freezeRegion.width ||
×
UNCOV
1212
                        vr.extras.freezeRegion.y > row ||
×
UNCOV
1213
                        row > vr.extras.freezeRegion.y + vr.extras.freezeRegion.height;
×
1214
                    if (isOutsideMainArea && !isSelected && isOutsideFreezeArea) {
21,679!
1215
                        return {
×
UNCOV
1216
                            kind: GridCellKind.Loading,
×
UNCOV
1217
                            allowOverlay: false,
×
UNCOV
1218
                        };
×
UNCOV
1219
                    }
×
1220
                }
21,679✔
1221
                let result = getCellContent([outerCol, row]);
124,627✔
1222
                if (rowMarkerOffset !== 0 && result.span !== undefined) {
124,627!
1223
                    result = {
×
UNCOV
1224
                        ...result,
×
UNCOV
1225
                        span: [result.span[0] + rowMarkerOffset, result.span[1] + rowMarkerOffset],
×
UNCOV
1226
                    };
×
UNCOV
1227
                }
×
1228
                return result;
124,627✔
1229
            }
124,627✔
1230
        },
130,457✔
1231
        [
649✔
1232
            showTrailingBlankRow,
649✔
1233
            mangledRows,
649✔
1234
            hasRowMarkers,
649✔
1235
            gridSelection?.rows,
649✔
1236
            onRowMoved,
649✔
1237
            rowMarkers,
649✔
1238
            rowMarkerOffset,
649✔
1239
            trailingRowOptions?.hint,
649✔
1240
            trailingRowOptions?.addIcon,
649✔
1241
            experimental?.strict,
649✔
1242
            getCellContent,
649✔
1243
            rowMarkerStartIndex,
649✔
1244
        ]
649✔
1245
    );
649✔
1246

649✔
1247
    const mangledGetGroupDetails = React.useCallback<NonNullable<DataEditorProps["getGroupDetails"]>>(
649✔
1248
        group => {
649✔
1249
            let result = getGroupDetails?.(group) ?? { name: group };
7,810✔
1250
            if (onGroupHeaderRenamed !== undefined && group !== "") {
7,810✔
1251
                result = {
91✔
1252
                    icon: result.icon,
91✔
1253
                    name: result.name,
91✔
1254
                    overrideTheme: result.overrideTheme,
91✔
1255
                    actions: [
91✔
1256
                        ...(result.actions ?? []),
91✔
1257
                        {
91✔
1258
                            title: "Rename",
91✔
1259
                            icon: "renameIcon",
91✔
1260
                            onClick: e =>
91✔
1261
                                setRenameGroup({
2✔
1262
                                    group: result.name,
2✔
1263
                                    bounds: e.bounds,
2✔
1264
                                }),
2✔
1265
                        },
91✔
1266
                    ],
91✔
1267
                };
91✔
1268
            }
91✔
1269
            return result;
7,810✔
1270
        },
7,810✔
1271
        [getGroupDetails, onGroupHeaderRenamed]
649✔
1272
    );
649✔
1273

649✔
1274
    const setOverlaySimple = React.useCallback(
649✔
1275
        (val: Omit<NonNullable<typeof overlay>, "theme">) => {
649✔
1276
            const [col, row] = val.cell;
16✔
1277
            const column = mangledCols[col];
16✔
1278
            const groupTheme =
16✔
1279
                column?.group !== undefined ? mangledGetGroupDetails(column.group)?.overrideTheme : undefined;
16!
1280
            const colTheme = column?.themeOverride;
16✔
1281
            const rowTheme = getRowThemeOverride?.(row);
16!
1282

16✔
1283
            setOverlay({
16✔
1284
                ...val,
16✔
1285
                theme: { ...mergedTheme, ...groupTheme, ...colTheme, ...rowTheme, ...val.content.themeOverride },
16✔
1286
            });
16✔
1287
        },
16✔
1288
        [getRowThemeOverride, mangledCols, mangledGetGroupDetails, mergedTheme]
649✔
1289
    );
649✔
1290

649✔
1291
    const reselect = React.useCallback(
649✔
1292
        (bounds: Rectangle, fromKeyboard: boolean, initialValue?: string) => {
649✔
1293
            if (gridSelection.current === undefined) return;
16!
1294

16✔
1295
            const [col, row] = gridSelection.current.cell;
16✔
1296
            const c = getMangledCellContent([col, row]);
16✔
1297
            if (c.kind !== GridCellKind.Boolean && c.allowOverlay) {
16✔
1298
                let content = c;
15✔
1299
                if (initialValue !== undefined) {
15✔
1300
                    switch (content.kind) {
7✔
1301
                        case GridCellKind.Number: {
7!
1302
                            const d = maybe(() => (initialValue === "-" ? -0 : Number.parseFloat(initialValue)), 0);
×
1303
                            content = {
×
UNCOV
1304
                                ...content,
×
UNCOV
1305
                                data: Number.isNaN(d) ? 0 : d,
×
UNCOV
1306
                            };
×
1307
                            break;
×
UNCOV
1308
                        }
×
1309
                        case GridCellKind.Text:
7✔
1310
                        case GridCellKind.Markdown:
7✔
1311
                        case GridCellKind.Uri:
7✔
1312
                            content = {
7✔
1313
                                ...content,
7✔
1314
                                data: initialValue,
7✔
1315
                            };
7✔
1316
                            break;
7✔
1317
                    }
7✔
1318
                }
7✔
1319

15✔
1320
                setOverlaySimple({
15✔
1321
                    target: bounds,
15✔
1322
                    content,
15✔
1323
                    initialValue,
15✔
1324
                    cell: [col, row],
15✔
1325
                    highlight: initialValue === undefined,
15✔
1326
                    forceEditMode: initialValue !== undefined,
15✔
1327
                });
15✔
1328
            } else if (c.kind === GridCellKind.Boolean && fromKeyboard && c.readonly !== true) {
16!
1329
                mangledOnCellsEdited([
×
UNCOV
1330
                    {
×
UNCOV
1331
                        location: gridSelection.current.cell,
×
UNCOV
1332
                        value: {
×
UNCOV
1333
                            ...c,
×
UNCOV
1334
                            data: toggleBoolean(c.data),
×
UNCOV
1335
                        },
×
UNCOV
1336
                    },
×
UNCOV
1337
                ]);
×
1338
                gridRef.current?.damage([{ cell: gridSelection.current.cell }]);
×
UNCOV
1339
            }
×
1340
        },
16✔
1341
        [getMangledCellContent, gridSelection, mangledOnCellsEdited, setOverlaySimple]
649✔
1342
    );
649✔
1343

649✔
1344
    const focusOnRowFromTrailingBlankRow = React.useCallback(
649✔
1345
        (col: number, row: number) => {
649✔
1346
            const bounds = gridRef.current?.getBounds(col, row);
1✔
1347
            if (bounds === undefined || scrollRef.current === null) {
1!
1348
                return;
×
UNCOV
1349
            }
×
1350

1✔
1351
            const content = getMangledCellContent([col, row]);
1✔
1352
            if (!content.allowOverlay) {
1!
1353
                return;
×
UNCOV
1354
            }
×
1355

1✔
1356
            setOverlaySimple({
1✔
1357
                target: bounds,
1✔
1358
                content,
1✔
1359
                initialValue: undefined,
1✔
1360
                highlight: true,
1✔
1361
                cell: [col, row],
1✔
1362
                forceEditMode: true,
1✔
1363
            });
1✔
1364
        },
1✔
1365
        [getMangledCellContent, setOverlaySimple]
649✔
1366
    );
649✔
1367

649✔
1368
    const scrollTo = React.useCallback<ScrollToFn>(
649✔
1369
        (col, row, dir = "both", paddingX = 0, paddingY = 0, options = undefined): void => {
649✔
1370
            if (scrollRef.current !== null) {
43✔
1371
                const grid = gridRef.current;
43✔
1372
                const canvas = canvasRef.current;
43✔
1373

43✔
1374
                const trueCol = typeof col !== "number" ? (col.unit === "cell" ? col.amount : undefined) : col;
43!
1375
                const trueRow = typeof row !== "number" ? (row.unit === "cell" ? row.amount : undefined) : row;
43!
1376
                const desiredX = typeof col !== "number" && col.unit === "px" ? col.amount : undefined;
43!
1377
                const desiredY = typeof row !== "number" && row.unit === "px" ? row.amount : undefined;
43✔
1378
                if (grid !== null && canvas !== null) {
43✔
1379
                    let targetRect: Rectangle = {
43✔
1380
                        x: 0,
43✔
1381
                        y: 0,
43✔
1382
                        width: 0,
43✔
1383
                        height: 0,
43✔
1384
                    };
43✔
1385

43✔
1386
                    let scrollX = 0;
43✔
1387
                    let scrollY = 0;
43✔
1388

43✔
1389
                    if (trueCol !== undefined || trueRow !== undefined) {
43!
1390
                        targetRect = grid.getBounds((trueCol ?? 0) + rowMarkerOffset, trueRow ?? 0) ?? targetRect;
43!
1391
                        if (targetRect.width === 0 || targetRect.height === 0) return;
43!
1392
                    }
43✔
1393

43✔
1394
                    const scrollBounds = canvas.getBoundingClientRect();
43✔
1395
                    const scale = scrollBounds.width / canvas.offsetWidth;
43✔
1396

43✔
1397
                    if (desiredX !== undefined) {
43!
1398
                        targetRect = {
×
UNCOV
1399
                            ...targetRect,
×
UNCOV
1400
                            x: desiredX - scrollBounds.left - scrollRef.current.scrollLeft,
×
UNCOV
1401
                            width: 1,
×
UNCOV
1402
                        };
×
UNCOV
1403
                    }
×
1404
                    if (desiredY !== undefined) {
43✔
1405
                        targetRect = {
4✔
1406
                            ...targetRect,
4✔
1407
                            y: desiredY + scrollBounds.top - scrollRef.current.scrollTop,
4✔
1408
                            height: 1,
4✔
1409
                        };
4✔
1410
                    }
4✔
1411

43✔
1412
                    if (targetRect !== undefined) {
43✔
1413
                        const bounds = {
43✔
1414
                            x: targetRect.x - paddingX,
43✔
1415
                            y: targetRect.y - paddingY,
43✔
1416
                            width: targetRect.width + 2 * paddingX,
43✔
1417
                            height: targetRect.height + 2 * paddingY,
43✔
1418
                        };
43✔
1419

43✔
1420
                        let frozenWidth = 0;
43✔
1421
                        for (let i = 0; i < freezeColumns; i++) {
43!
1422
                            frozenWidth += columns[i].width;
×
UNCOV
1423
                        }
×
1424
                        let trailingRowHeight = 0;
43✔
1425
                        if (lastRowSticky) {
43✔
1426
                            trailingRowHeight = typeof rowHeight === "number" ? rowHeight : rowHeight(rows);
43!
1427
                        }
43✔
1428

43✔
1429
                        // scrollBounds is already scaled
43✔
1430
                        let sLeft = frozenWidth * scale + scrollBounds.left + rowMarkerOffset * rowMarkerWidth * scale;
43✔
1431
                        let sRight = scrollBounds.right;
43✔
1432
                        let sTop = scrollBounds.top + totalHeaderHeight * scale;
43✔
1433
                        let sBottom = scrollBounds.bottom - trailingRowHeight * scale;
43✔
1434

43✔
1435
                        const minx = targetRect.width + paddingX * 2;
43✔
1436
                        switch (options?.hAlign) {
43✔
1437
                            case "start":
43!
1438
                                sRight = sLeft + minx;
×
1439
                                break;
×
1440
                            case "end":
43!
1441
                                sLeft = sRight - minx;
×
1442
                                break;
×
1443
                            case "center":
43!
1444
                                sLeft = Math.floor((sLeft + sRight) / 2) - minx / 2;
×
1445
                                sRight = sLeft + minx;
×
1446
                                break;
×
1447
                        }
43✔
1448

43✔
1449
                        const miny = targetRect.height + paddingY * 2;
43✔
1450
                        switch (options?.vAlign) {
43✔
1451
                            case "start":
43✔
1452
                                sBottom = sTop + miny;
1✔
1453
                                break;
1✔
1454
                            case "end":
43✔
1455
                                sTop = sBottom - miny;
1✔
1456
                                break;
1✔
1457
                            case "center":
43✔
1458
                                sTop = Math.floor((sTop + sBottom) / 2) - miny / 2;
1✔
1459
                                sBottom = sTop + miny;
1✔
1460
                                break;
1✔
1461
                        }
43✔
1462

43✔
1463
                        if (sLeft > bounds.x) {
43!
1464
                            scrollX = bounds.x - sLeft;
×
1465
                        } else if (sRight < bounds.x + bounds.width) {
43✔
1466
                            scrollX = bounds.x + bounds.width - sRight;
4✔
1467
                        }
4✔
1468

43✔
1469
                        if (sTop > bounds.y) {
43!
1470
                            scrollY = bounds.y - sTop;
×
1471
                        } else if (sBottom < bounds.y + bounds.height) {
43✔
1472
                            scrollY = bounds.y + bounds.height - sBottom;
12✔
1473
                        }
12✔
1474

43✔
1475
                        if (dir === "vertical" || (typeof col === "number" && col < freezeColumns)) {
43✔
1476
                            scrollX = 0;
2✔
1477
                        } else if (dir === "horizontal") {
43✔
1478
                            scrollY = 0;
6✔
1479
                        }
6✔
1480

43✔
1481
                        if (scrollX !== 0 || scrollY !== 0) {
43✔
1482
                            // Remove scaling as scrollTo method is unaffected by transform scale.
15✔
1483
                            if (scale !== 1) {
15!
1484
                                scrollX /= scale;
×
1485
                                scrollY /= scale;
×
UNCOV
1486
                            }
×
1487
                            scrollRef.current.scrollTo(
15✔
1488
                                scrollX + scrollRef.current.scrollLeft,
15✔
1489
                                scrollY + scrollRef.current.scrollTop
15✔
1490
                            );
15✔
1491
                        }
15✔
1492
                    }
43✔
1493
                }
43✔
1494
            }
43✔
1495
        },
43✔
1496
        [rowMarkerOffset, rowMarkerWidth, totalHeaderHeight, lastRowSticky, freezeColumns, columns, rowHeight, rows]
649✔
1497
    );
649✔
1498

649✔
1499
    const focusCallback = React.useRef(focusOnRowFromTrailingBlankRow);
649✔
1500
    const getCellContentRef = React.useRef(getCellContent);
649✔
1501
    const rowsRef = React.useRef(rows);
649✔
1502
    focusCallback.current = focusOnRowFromTrailingBlankRow;
649✔
1503
    getCellContentRef.current = getCellContent;
649✔
1504
    rowsRef.current = rows;
649✔
1505
    const appendRow = React.useCallback(
649✔
1506
        async (col: number, openOverlay: boolean = true): Promise<void> => {
649✔
1507
            const c = mangledCols[col];
1✔
1508
            if (c?.trailingRowOptions?.disabled === true) {
1!
1509
                return;
×
UNCOV
1510
            }
×
1511
            const appendResult = onRowAppended?.();
1✔
1512

1✔
1513
            let r: "top" | "bottom" | number | undefined = undefined;
1✔
1514
            let bottom = true;
1✔
1515
            if (appendResult !== undefined) {
1!
1516
                r = await appendResult;
×
1517
                if (r === "top") bottom = false;
×
1518
                if (typeof r === "number") bottom = false;
×
UNCOV
1519
            }
×
1520

1✔
1521
            let backoff = 0;
1✔
1522
            const doFocus = () => {
1✔
1523
                if (rowsRef.current <= rows) {
2✔
1524
                    if (backoff < 500) {
1✔
1525
                        window.setTimeout(doFocus, backoff);
1✔
1526
                    }
1✔
1527
                    backoff = 50 + backoff * 2;
1✔
1528
                    return;
1✔
1529
                }
1✔
1530

1✔
1531
                const row = typeof r === "number" ? r : bottom ? rows : 0;
2!
1532
                scrollTo(col - rowMarkerOffset, row);
2✔
1533
                setCurrent(
2✔
1534
                    {
2✔
1535
                        cell: [col, row],
2✔
1536
                        range: {
2✔
1537
                            x: col,
2✔
1538
                            y: row,
2✔
1539
                            width: 1,
2✔
1540
                            height: 1,
2✔
1541
                        },
2✔
1542
                    },
2✔
1543
                    false,
2✔
1544
                    false,
2✔
1545
                    "edit"
2✔
1546
                );
2✔
1547

2✔
1548
                const cell = getCellContentRef.current([col - rowMarkerOffset, row]);
2✔
1549
                if (cell.allowOverlay && isReadWriteCell(cell) && cell.readonly !== true && openOverlay) {
2✔
1550
                    // wait for scroll to have a chance to process
1✔
1551
                    window.setTimeout(() => {
1✔
1552
                        focusCallback.current(col, row);
1✔
1553
                    }, 0);
1✔
1554
                }
1✔
1555
            };
2✔
1556
            // Queue up to allow the consumer to react to the event and let us check if they did
1✔
1557
            doFocus();
1✔
1558
        },
1✔
1559
        [mangledCols, onRowAppended, rowMarkerOffset, rows, scrollTo, setCurrent]
649✔
1560
    );
649✔
1561

649✔
1562
    const getCustomNewRowTargetColumn = React.useCallback(
649✔
1563
        (col: number): number | undefined => {
649✔
1564
            const customTargetColumn =
1✔
1565
                columns[col]?.trailingRowOptions?.targetColumn ?? trailingRowOptions?.targetColumn;
1!
1566

1✔
1567
            if (typeof customTargetColumn === "number") {
1!
1568
                const customTargetOffset = hasRowMarkers ? 1 : 0;
×
1569
                return customTargetColumn + customTargetOffset;
×
UNCOV
1570
            }
×
1571

1✔
1572
            if (typeof customTargetColumn === "object") {
1!
1573
                const maybeIndex = columnsIn.indexOf(customTargetColumn);
×
UNCOV
1574
                if (maybeIndex >= 0) {
×
1575
                    const customTargetOffset = hasRowMarkers ? 1 : 0;
×
1576
                    return maybeIndex + customTargetOffset;
×
UNCOV
1577
                }
×
UNCOV
1578
            }
×
1579

1✔
1580
            return undefined;
1✔
1581
        },
1✔
1582
        [columns, columnsIn, hasRowMarkers, trailingRowOptions?.targetColumn]
649✔
1583
    );
649✔
1584

649✔
1585
    const lastSelectedRowRef = React.useRef<number>();
649✔
1586
    const lastSelectedColRef = React.useRef<number>();
649✔
1587

649✔
1588
    const themeForCell = React.useCallback(
649✔
1589
        (cell: InnerGridCell, pos: Item): Theme => {
649✔
1590
            const [col, row] = pos;
20✔
1591
            return {
20✔
1592
                ...mergedTheme,
20✔
1593
                ...mangledCols[col]?.themeOverride,
20✔
1594
                ...getRowThemeOverride?.(row),
20!
1595
                ...cell.themeOverride,
20✔
1596
            };
20✔
1597
        },
20✔
1598
        [getRowThemeOverride, mangledCols, mergedTheme]
649✔
1599
    );
649✔
1600

649✔
1601
    const handleSelect = React.useCallback(
649✔
1602
        (args: GridMouseEventArgs) => {
649✔
1603
            const isMultiKey = browserIsOSX.value ? args.metaKey : args.ctrlKey;
113!
1604
            const isMultiRow = isMultiKey && rowSelect === "multi";
113✔
1605
            const isMultiCol = isMultiKey && columnSelect === "multi";
113✔
1606
            const [col, row] = args.location;
113✔
1607
            const selectedColumns = gridSelection.columns;
113✔
1608
            const selectedRows = gridSelection.rows;
113✔
1609
            const [cellCol, cellRow] = gridSelection.current?.cell ?? [];
113✔
1610
            // eslint-disable-next-line unicorn/prefer-switch
113✔
1611
            if (args.kind === "cell") {
113✔
1612
                lastSelectedColRef.current = undefined;
97✔
1613

97✔
1614
                lastMouseSelectLocation.current = [col, row];
97✔
1615

97✔
1616
                if (col === 0 && hasRowMarkers) {
97✔
1617
                    if (
15✔
1618
                        (showTrailingBlankRow === true && row === rows) ||
15✔
1619
                        rowMarkers === "number" ||
15✔
1620
                        rowSelect === "none"
14✔
1621
                    )
15✔
1622
                        return;
15✔
1623

14✔
1624
                    const markerCell = getMangledCellContent(args.location);
14✔
1625
                    if (markerCell.kind !== InnerGridCellKind.Marker) {
15!
1626
                        return;
×
UNCOV
1627
                    }
✔
1628

14✔
1629
                    if (onRowMoved !== undefined) {
15!
1630
                        const renderer = getCellRenderer(markerCell);
×
1631
                        assert(renderer?.kind === InnerGridCellKind.Marker);
×
1632
                        const postClick = renderer?.onClick?.({
×
UNCOV
1633
                            ...args,
×
UNCOV
1634
                            cell: markerCell,
×
UNCOV
1635
                            posX: args.localEventX,
×
UNCOV
1636
                            posY: args.localEventY,
×
UNCOV
1637
                            bounds: args.bounds,
×
UNCOV
1638
                            theme: themeForCell(markerCell, args.location),
×
1639
                            preventDefault: () => undefined,
×
UNCOV
1640
                        }) as MarkerCell | undefined;
×
1641
                        if (postClick === undefined || postClick.checked === markerCell.checked) return;
×
UNCOV
1642
                    }
✔
1643

14✔
1644
                    setOverlay(undefined);
14✔
1645
                    focus();
14✔
1646
                    const isSelected = selectedRows.hasIndex(row);
14✔
1647

14✔
1648
                    const lastHighlighted = lastSelectedRowRef.current;
14✔
1649
                    if (
14✔
1650
                        rowSelect === "multi" &&
14✔
1651
                        (args.shiftKey || args.isLongTouch === true) &&
8✔
1652
                        lastHighlighted !== undefined &&
1✔
1653
                        selectedRows.hasIndex(lastHighlighted)
1✔
1654
                    ) {
15✔
1655
                        const newSlice: Slice = [Math.min(lastHighlighted, row), Math.max(lastHighlighted, row) + 1];
1✔
1656

1✔
1657
                        if (isMultiRow || rowSelectionMode === "multi") {
1!
1658
                            setSelectedRows(undefined, newSlice, true);
×
1659
                        } else {
1✔
1660
                            setSelectedRows(CompactSelection.fromSingleSelection(newSlice), undefined, isMultiRow);
1✔
1661
                        }
1✔
1662
                    } else if (isMultiRow || args.isTouch || rowSelectionMode === "multi") {
15✔
1663
                        if (isSelected) {
3✔
1664
                            setSelectedRows(selectedRows.remove(row), undefined, true);
1✔
1665
                        } else {
2✔
1666
                            setSelectedRows(undefined, row, true);
2✔
1667
                            lastSelectedRowRef.current = row;
2✔
1668
                        }
2✔
1669
                    } else if (isSelected && selectedRows.length === 1) {
13✔
1670
                        setSelectedRows(CompactSelection.empty(), undefined, isMultiKey);
1✔
1671
                    } else {
10✔
1672
                        setSelectedRows(CompactSelection.fromSingleSelection(row), undefined, isMultiKey);
9✔
1673
                        lastSelectedRowRef.current = row;
9✔
1674
                    }
9✔
1675
                } else if (col >= rowMarkerOffset && showTrailingBlankRow && row === rows) {
97✔
1676
                    const customTargetColumn = getCustomNewRowTargetColumn(col);
1✔
1677
                    void appendRow(customTargetColumn ?? col);
1✔
1678
                } else {
82✔
1679
                    if (cellCol !== col || cellRow !== row) {
81✔
1680
                        const cell = getMangledCellContent(args.location);
75✔
1681
                        const renderer = getCellRenderer(cell);
75✔
1682

75✔
1683
                        if (renderer?.onSelect !== undefined) {
75!
1684
                            let prevented = false;
×
1685
                            renderer.onSelect({
×
UNCOV
1686
                                ...args,
×
UNCOV
1687
                                cell,
×
UNCOV
1688
                                posX: args.localEventX,
×
UNCOV
1689
                                posY: args.localEventY,
×
UNCOV
1690
                                bounds: args.bounds,
×
1691
                                preventDefault: () => (prevented = true),
×
UNCOV
1692
                                theme: themeForCell(cell, args.location),
×
UNCOV
1693
                            });
×
UNCOV
1694
                            if (prevented) {
×
1695
                                return;
×
UNCOV
1696
                            }
×
UNCOV
1697
                        }
×
1698
                        const isLastStickyRow = lastRowSticky && row === rows;
75✔
1699

75✔
1700
                        const startedFromLastSticky =
75✔
1701
                            lastRowSticky && gridSelection !== undefined && gridSelection.current?.cell[1] === rows;
75✔
1702

75✔
1703
                        if (
75✔
1704
                            (args.shiftKey || args.isLongTouch === true) &&
75✔
1705
                            cellCol !== undefined &&
6✔
1706
                            cellRow !== undefined &&
6✔
1707
                            gridSelection.current !== undefined &&
6✔
1708
                            !startedFromLastSticky
6✔
1709
                        ) {
75✔
1710
                            if (isLastStickyRow) {
6!
UNCOV
1711
                                // If we're making a selection and shift click in to the last sticky row,
×
UNCOV
1712
                                // just drop the event. Don't kill the selection.
×
1713
                                return;
×
UNCOV
1714
                            }
×
1715

6✔
1716
                            const left = Math.min(col, cellCol);
6✔
1717
                            const right = Math.max(col, cellCol);
6✔
1718
                            const top = Math.min(row, cellRow);
6✔
1719
                            const bottom = Math.max(row, cellRow);
6✔
1720
                            setCurrent(
6✔
1721
                                {
6✔
1722
                                    ...gridSelection.current,
6✔
1723
                                    range: {
6✔
1724
                                        x: left,
6✔
1725
                                        y: top,
6✔
1726
                                        width: right - left + 1,
6✔
1727
                                        height: bottom - top + 1,
6✔
1728
                                    },
6✔
1729
                                },
6✔
1730
                                true,
6✔
1731
                                isMultiKey,
6✔
1732
                                "click"
6✔
1733
                            );
6✔
1734
                            lastSelectedRowRef.current = undefined;
6✔
1735
                            focus();
6✔
1736
                        } else {
75✔
1737
                            setCurrent(
69✔
1738
                                {
69✔
1739
                                    cell: [col, row],
69✔
1740
                                    range: { x: col, y: row, width: 1, height: 1 },
69✔
1741
                                },
69✔
1742
                                true,
69✔
1743
                                isMultiKey,
69✔
1744
                                "click"
69✔
1745
                            );
69✔
1746
                            lastSelectedRowRef.current = undefined;
69✔
1747
                            setOverlay(undefined);
69✔
1748
                            focus();
69✔
1749
                        }
69✔
1750
                    }
75✔
1751
                }
81✔
1752
            } else if (args.kind === "header") {
113✔
1753
                lastMouseSelectLocation.current = [col, row];
12✔
1754
                setOverlay(undefined);
12✔
1755
                if (hasRowMarkers && col === 0) {
12✔
1756
                    lastSelectedRowRef.current = undefined;
4✔
1757
                    lastSelectedColRef.current = undefined;
4✔
1758
                    if (rowSelect === "multi") {
4✔
1759
                        if (selectedRows.length !== rows) {
4✔
1760
                            setSelectedRows(CompactSelection.fromSingleSelection([0, rows]), undefined, isMultiKey);
3✔
1761
                        } else {
4✔
1762
                            setSelectedRows(CompactSelection.empty(), undefined, isMultiKey);
1✔
1763
                        }
1✔
1764
                        focus();
4✔
1765
                    }
4✔
1766
                } else {
12✔
1767
                    const lastCol = lastSelectedColRef.current;
8✔
1768
                    if (
8✔
1769
                        columnSelect === "multi" &&
8✔
1770
                        (args.shiftKey || args.isLongTouch === true) &&
7!
UNCOV
1771
                        lastCol !== undefined &&
×
UNCOV
1772
                        selectedColumns.hasIndex(lastCol)
×
1773
                    ) {
8!
1774
                        const newSlice: Slice = [Math.min(lastCol, col), Math.max(lastCol, col) + 1];
×
UNCOV
1775

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

125✔
1849
            const time = performance.now();
125✔
1850
            const wasDoubleClick = time - (mouseDownData.current?.time ?? -1000) < 250;
126✔
1851
            mouseDownData.current = {
126✔
1852
                wasDoubleClick,
126✔
1853
                button: args.button,
126✔
1854
                time,
126✔
1855
                location: args.location,
126✔
1856
            };
126✔
1857

126✔
1858
            if (args?.kind === "header") {
126✔
1859
                isActivelyDraggingHeader.current = true;
18✔
1860
            }
18✔
1861

125✔
1862
            const fh = args.kind === "cell" && args.isFillHandle;
126✔
1863

126✔
1864
            if (!fh && args.kind !== "cell" && args.isEdge) return;
126✔
1865

118✔
1866
            setMouseState({
118✔
1867
                previousSelection: gridSelection,
118✔
1868
                fillHandle: fh,
118✔
1869
            });
118✔
1870
            lastMouseSelectLocation.current = undefined;
118✔
1871

118✔
1872
            if (!args.isTouch && args.button === 0) {
126✔
1873
                handleSelect(args);
110✔
1874
            } else if (!args.isTouch && args.button === 1) {
126✔
1875
                lastMouseSelectLocation.current = args.location;
4✔
1876
            }
4✔
1877
        },
126✔
1878
        [gridSelection, handleSelect]
649✔
1879
    );
649✔
1880

649✔
1881
    const [renameGroup, setRenameGroup] = React.useState<{
649✔
1882
        group: string;
649✔
1883
        bounds: Rectangle;
649✔
1884
    }>();
649✔
1885

649✔
1886
    const handleGroupHeaderSelection = React.useCallback(
649✔
1887
        (args: GridMouseEventArgs) => {
649✔
1888
            if (args.kind !== groupHeaderKind || columnSelect !== "multi") {
3!
1889
                return;
×
UNCOV
1890
            }
×
1891
            const isMultiKey = browserIsOSX.value ? args.metaKey : args.ctrlKey;
3!
1892
            const [col] = args.location;
3✔
1893
            const selectedColumns = gridSelection.columns;
3✔
1894

3✔
1895
            if (col < rowMarkerOffset) return;
3!
1896

3✔
1897
            const needle = mangledCols[col];
3✔
1898
            let start = col;
3✔
1899
            let end = col;
3✔
1900
            for (let i = col - 1; i >= rowMarkerOffset; i--) {
3✔
1901
                if (!isGroupEqual(needle.group, mangledCols[i].group)) break;
3!
1902
                start--;
3✔
1903
            }
3✔
1904

3✔
1905
            for (let i = col + 1; i < mangledCols.length; i++) {
3✔
1906
                if (!isGroupEqual(needle.group, mangledCols[i].group)) break;
27!
1907
                end++;
27✔
1908
            }
27✔
1909

3✔
1910
            focus();
3✔
1911

3✔
1912
            if (isMultiKey) {
3✔
1913
                if (selectedColumns.hasAll([start, end + 1])) {
2✔
1914
                    let newVal = selectedColumns;
1✔
1915
                    for (let index = start; index <= end; index++) {
1✔
1916
                        newVal = newVal.remove(index);
11✔
1917
                    }
11✔
1918
                    setSelectedColumns(newVal, undefined, isMultiKey);
1✔
1919
                } else {
1✔
1920
                    setSelectedColumns(undefined, [start, end + 1], isMultiKey);
1✔
1921
                }
1✔
1922
            } else {
3✔
1923
                setSelectedColumns(CompactSelection.fromSingleSelection([start, end + 1]), undefined, isMultiKey);
1✔
1924
            }
1✔
1925
        },
3✔
1926
        [columnSelect, focus, gridSelection.columns, mangledCols, rowMarkerOffset, setSelectedColumns]
649✔
1927
    );
649✔
1928

649✔
1929
    const fillDown = React.useCallback(
649✔
1930
        (reverse: boolean) => {
649✔
1931
            if (gridSelection.current === undefined) return;
4!
1932
            const v: EditListItem[] = [];
4✔
1933
            const r = gridSelection.current.range;
4✔
1934
            for (let x = 0; x < r.width; x++) {
4✔
1935
                const fillCol = x + r.x;
5✔
1936
                const fillVal = getMangledCellContent([fillCol, reverse ? r.y + r.height - 1 : r.y]);
5!
1937
                if (isInnerOnlyCell(fillVal) || !isReadWriteCell(fillVal)) continue;
5!
1938
                for (let y = 1; y < r.height; y++) {
5✔
1939
                    const fillRow = reverse ? r.y + r.height - (y + 1) : y + r.y;
14!
1940
                    const target = [fillCol, fillRow] as const;
14✔
1941
                    v.push({
14✔
1942
                        location: target,
14✔
1943
                        value: { ...fillVal },
14✔
1944
                    });
14✔
1945
                }
14✔
1946
            }
5✔
1947

4✔
1948
            mangledOnCellsEdited(v);
4✔
1949

4✔
1950
            gridRef.current?.damage(
4✔
1951
                v.map(c => ({
4✔
1952
                    cell: c.location,
14✔
1953
                }))
4✔
1954
            );
4✔
1955
        },
4✔
1956
        [getMangledCellContent, gridSelection, mangledOnCellsEdited]
649✔
1957
    );
649✔
1958

649✔
1959
    const isPrevented = React.useRef(false);
649✔
1960

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

649✔
2015
    const [scrollDir, setScrollDir] = React.useState<GridMouseEventArgs["scrollEdge"]>();
649✔
2016

649✔
2017
    const onMouseUp = React.useCallback(
649✔
2018
        (args: GridMouseEventArgs, isOutside: boolean) => {
649✔
2019
            const mouse = mouseState;
126✔
2020
            setMouseState(undefined);
126✔
2021
            setScrollDir(undefined);
126✔
2022
            isActivelyDraggingHeader.current = false;
126✔
2023

126✔
2024
            if (isOutside) return;
126✔
2025

124✔
2026
            if (mouse?.fillHandle === true && gridSelection.current !== undefined) {
126✔
2027
                fillDown(gridSelection.current.cell[1] !== gridSelection.current.range.y);
3✔
2028
                return;
3✔
2029
            }
3✔
2030

121✔
2031
            const [col, row] = args.location;
121✔
2032
            const [lastMouseDownCol, lastMouseDownRow] = lastMouseSelectLocation.current ?? [];
126✔
2033

126✔
2034
            const preventDefault = () => {
126✔
2035
                isPrevented.current = true;
×
UNCOV
2036
            };
×
2037

126✔
2038
            const handleMaybeClick = (a: GridMouseCellEventArgs): boolean => {
126✔
2039
                const isValidClick = a.isTouch || (lastMouseDownCol === col && lastMouseDownRow === row);
96✔
2040
                if (isValidClick) {
96✔
2041
                    onCellClicked?.([col - rowMarkerOffset, row], {
86✔
2042
                        ...a,
4✔
2043
                        preventDefault,
4✔
2044
                    });
4✔
2045
                }
86✔
2046
                if (a.button === 1) return !isPrevented.current;
96✔
2047
                if (!isPrevented.current) {
93✔
2048
                    const c = getMangledCellContent(args.location);
93✔
2049
                    const r = getCellRenderer(c);
93✔
2050
                    if (r !== undefined && r.onClick !== undefined && isValidClick) {
93✔
2051
                        const newVal = r.onClick({
20✔
2052
                            ...a,
20✔
2053
                            cell: c,
20✔
2054
                            posX: a.localEventX,
20✔
2055
                            posY: a.localEventY,
20✔
2056
                            bounds: a.bounds,
20✔
2057
                            theme: themeForCell(c, args.location),
20✔
2058
                            preventDefault,
20✔
2059
                        });
20✔
2060
                        if (newVal !== undefined && !isInnerOnlyCell(newVal) && isEditableGridCell(newVal)) {
20✔
2061
                            mangledOnCellsEdited([{ location: a.location, value: newVal }]);
4✔
2062
                            gridRef.current?.damage([
4✔
2063
                                {
4✔
2064
                                    cell: a.location,
4✔
2065
                                },
4✔
2066
                            ]);
4✔
2067
                        }
4✔
2068
                    }
20✔
2069
                    if (
93✔
2070
                        !isPrevented.current &&
93✔
2071
                        mouse?.previousSelection?.current?.cell !== undefined &&
93✔
2072
                        gridSelection.current !== undefined
20✔
2073
                    ) {
93✔
2074
                        const [selectedCol, selectedRow] = gridSelection.current.cell;
19✔
2075
                        const [prevCol, prevRow] = mouse.previousSelection.current.cell;
19✔
2076
                        if (col === selectedCol && col === prevCol && row === selectedRow && row === prevRow) {
19✔
2077
                            onCellActivated?.([col - rowMarkerOffset, row]);
3✔
2078
                            reselect(a.bounds, false);
3✔
2079
                            return true;
3✔
2080
                        }
3✔
2081
                    }
19✔
2082
                }
93✔
2083
                return false;
90✔
2084
            };
96✔
2085

126✔
2086
            const clickLocation = args.location[0] - rowMarkerOffset;
126✔
2087
            if (args.isTouch) {
126✔
2088
                const vr = visibleRegionRef.current;
4✔
2089
                const touchVr = touchDownArgs.current;
4✔
2090
                if (vr.x !== touchVr.x || vr.y !== touchVr.y) {
4!
UNCOV
2091
                    // we scrolled, abort
×
2092
                    return;
×
UNCOV
2093
                }
×
2094
                // take care of context menus first if long pressed item is already selected
4✔
2095
                if (args.isLongTouch === true) {
4!
UNCOV
2096
                    if (
×
UNCOV
2097
                        args.kind === "cell" &&
×
UNCOV
2098
                        gridSelection?.current?.cell[0] === col &&
×
UNCOV
2099
                        gridSelection?.current?.cell[1] === row
×
UNCOV
2100
                    ) {
×
2101
                        onCellContextMenu?.([clickLocation, args.location[1]], {
×
UNCOV
2102
                            ...args,
×
UNCOV
2103
                            preventDefault,
×
UNCOV
2104
                        });
×
2105
                        return;
×
UNCOV
2106
                    } else if (args.kind === "header" && gridSelection.columns.hasIndex(col)) {
×
2107
                        onHeaderContextMenu?.(clickLocation, { ...args, preventDefault });
×
2108
                        return;
×
UNCOV
2109
                    } else if (args.kind === groupHeaderKind) {
×
UNCOV
2110
                        if (clickLocation < 0) {
×
2111
                            return;
×
UNCOV
2112
                        }
×
UNCOV
2113

×
2114
                        onGroupHeaderContextMenu?.(clickLocation, { ...args, preventDefault });
×
2115
                        return;
×
UNCOV
2116
                    }
×
UNCOV
2117
                }
×
2118
                if (args.kind === "cell") {
4✔
2119
                    // click that cell
2✔
2120
                    if (!handleMaybeClick(args)) {
2✔
2121
                        handleSelect(args);
2✔
2122
                    }
2✔
2123
                } else if (args.kind === groupHeaderKind) {
2✔
2124
                    onGroupHeaderClicked?.(clickLocation, { ...args, preventDefault });
1✔
2125
                } else {
1✔
2126
                    if (args.kind === headerKind) {
1✔
2127
                        onHeaderClicked?.(clickLocation, {
1✔
2128
                            ...args,
1✔
2129
                            preventDefault,
1✔
2130
                        });
1✔
2131
                    }
1✔
2132
                    handleSelect(args);
1✔
2133
                }
1✔
2134
                return;
4✔
2135
            }
4✔
2136

117✔
2137
            if (args.kind === "header") {
126✔
2138
                if (clickLocation < 0) {
16✔
2139
                    return;
3✔
2140
                }
3✔
2141

13✔
2142
                if (args.isEdge) {
16✔
2143
                    void normalSizeColumn(col);
2✔
2144
                } else if (args.button === 0 && col === lastMouseDownCol && row === lastMouseDownRow) {
16✔
2145
                    onHeaderClicked?.(clickLocation, { ...args, preventDefault });
5✔
2146
                }
5✔
2147
            }
16✔
2148

114✔
2149
            if (args.kind === groupHeaderKind) {
126✔
2150
                if (clickLocation < 0) {
3!
2151
                    return;
×
UNCOV
2152
                }
×
2153

3✔
2154
                if (args.button === 0 && col === lastMouseDownCol && row === lastMouseDownRow) {
3✔
2155
                    onGroupHeaderClicked?.(clickLocation, { ...args, preventDefault });
3!
2156
                    if (!isPrevented.current) {
3✔
2157
                        handleGroupHeaderSelection(args);
3✔
2158
                    }
3✔
2159
                }
3✔
2160
            }
3✔
2161

114✔
2162
            if (args.kind === "cell" && (args.button === 0 || args.button === 1)) {
126✔
2163
                handleMaybeClick(args);
94✔
2164
            }
94✔
2165

114✔
2166
            lastMouseSelectLocation.current = undefined;
114✔
2167
        },
126✔
2168
        [
649✔
2169
            mouseState,
649✔
2170
            rowMarkerOffset,
649✔
2171
            gridSelection,
649✔
2172
            onCellClicked,
649✔
2173
            fillDown,
649✔
2174
            getMangledCellContent,
649✔
2175
            getCellRenderer,
649✔
2176
            themeForCell,
649✔
2177
            mangledOnCellsEdited,
649✔
2178
            onCellActivated,
649✔
2179
            reselect,
649✔
2180
            onCellContextMenu,
649✔
2181
            onHeaderContextMenu,
649✔
2182
            onGroupHeaderContextMenu,
649✔
2183
            handleSelect,
649✔
2184
            onGroupHeaderClicked,
649✔
2185
            normalSizeColumn,
649✔
2186
            onHeaderClicked,
649✔
2187
            handleGroupHeaderSelection,
649✔
2188
        ]
649✔
2189
    );
649✔
2190

649✔
2191
    const onMouseMoveImpl = React.useCallback(
649✔
2192
        (args: GridMouseEventArgs) => {
649✔
2193
            const a: GridMouseEventArgs = {
37✔
2194
                ...args,
37✔
2195
                location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
37✔
2196
            };
37✔
2197
            onMouseMove?.(a);
37✔
2198
            setScrollDir(cv => {
37✔
2199
                if (isActivelyDraggingHeader.current) return [args.scrollEdge[0], 0];
37✔
2200
                if (args.scrollEdge[0] === cv?.[0] && args.scrollEdge[1] === cv[1]) return cv;
37!
2201
                return mouseState === undefined || (mouseDownData.current?.location[0] ?? 0) < rowMarkerOffset
35!
2202
                    ? undefined
13✔
2203
                    : args.scrollEdge;
13✔
2204
            });
37✔
2205
        },
37✔
2206
        [mouseState, onMouseMove, rowMarkerOffset]
649✔
2207
    );
649✔
2208

649✔
2209
    useAutoscroll(scrollDir, scrollRef);
649✔
2210

649✔
2211
    const onHeaderMenuClickInner = React.useCallback(
649✔
2212
        (col: number, screenPosition: Rectangle) => {
649✔
2213
            onHeaderMenuClick?.(col - rowMarkerOffset, screenPosition);
1✔
2214
        },
1✔
2215
        [onHeaderMenuClick, rowMarkerOffset]
649✔
2216
    );
649✔
2217

649✔
2218
    const currentCell = gridSelection?.current?.cell;
649✔
2219
    const onVisibleRegionChangedImpl = React.useCallback(
649✔
2220
        (
649✔
2221
            region: Rectangle,
136✔
2222
            clientWidth: number,
136✔
2223
            clientHeight: number,
136✔
2224
            rightElWidth: number,
136✔
2225
            tx?: number,
136✔
2226
            ty?: number
136✔
2227
        ) => {
136✔
2228
            hasJustScrolled.current = false;
136✔
2229
            let selected = currentCell;
136✔
2230
            if (selected !== undefined) {
136✔
2231
                selected = [selected[0] - rowMarkerOffset, selected[1]];
10✔
2232
            }
10✔
2233
            const newRegion = {
136✔
2234
                x: region.x - rowMarkerOffset,
136✔
2235
                y: region.y,
136✔
2236
                width: region.width,
136✔
2237
                height: showTrailingBlankRow && region.y + region.height >= rows ? region.height - 1 : region.height,
136✔
2238
                tx,
136✔
2239
                ty,
136✔
2240
                extras: {
136✔
2241
                    selected,
136✔
2242
                    freezeRegion:
136✔
2243
                        freezeColumns === 0
136✔
2244
                            ? undefined
136!
UNCOV
2245
                            : {
×
UNCOV
2246
                                  x: 0,
×
UNCOV
2247
                                  y: region.y,
×
UNCOV
2248
                                  width: freezeColumns,
×
UNCOV
2249
                                  height: region.height,
×
UNCOV
2250
                              },
×
2251
                },
136✔
2252
            };
136✔
2253
            visibleRegionRef.current = newRegion;
136✔
2254
            setVisibleRegion(newRegion);
136✔
2255
            setClientSize([clientWidth, clientHeight, rightElWidth]);
136✔
2256
            onVisibleRegionChanged?.(newRegion, newRegion.tx, newRegion.ty, newRegion.extras);
136!
2257
        },
136✔
2258
        [
649✔
2259
            currentCell,
649✔
2260
            rowMarkerOffset,
649✔
2261
            showTrailingBlankRow,
649✔
2262
            rows,
649✔
2263
            freezeColumns,
649✔
2264
            setVisibleRegion,
649✔
2265
            onVisibleRegionChanged,
649✔
2266
        ]
649✔
2267
    );
649✔
2268

649✔
2269
    const onColumnMovedImpl = whenDefined(
649✔
2270
        onColumnMoved,
649✔
2271
        React.useCallback(
649✔
2272
            (startIndex: number, endIndex: number) => {
649✔
2273
                onColumnMoved?.(startIndex - rowMarkerOffset, endIndex - rowMarkerOffset);
1✔
2274
                if (columnSelect !== "none") {
1✔
2275
                    setSelectedColumns(CompactSelection.fromSingleSelection(endIndex), undefined, true);
1✔
2276
                }
1✔
2277
            },
1✔
2278
            [columnSelect, onColumnMoved, rowMarkerOffset, setSelectedColumns]
649✔
2279
        )
649✔
2280
    );
649✔
2281

649✔
2282
    const isActivelyDragging = React.useRef(false);
649✔
2283
    const onDragStartImpl = React.useCallback(
649✔
2284
        (args: GridDragEventArgs) => {
649✔
2285
            if (args.location[0] === 0 && rowMarkerOffset > 0) {
1!
2286
                args.preventDefault();
×
2287
                return;
×
UNCOV
2288
            }
×
2289
            onDragStart?.({
1✔
2290
                ...args,
1✔
2291
                location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
1✔
2292
            });
1✔
2293

1✔
2294
            if (!args.defaultPrevented()) {
1✔
2295
                isActivelyDragging.current = true;
1✔
2296
            }
1✔
2297
            setMouseState(undefined);
1✔
2298
        },
1✔
2299
        [onDragStart, rowMarkerOffset]
649✔
2300
    );
649✔
2301

649✔
2302
    const onDragEnd = React.useCallback(() => {
649✔
2303
        isActivelyDragging.current = false;
×
2304
    }, []);
649✔
2305

649✔
2306
    const onItemHoveredImpl = React.useCallback(
649✔
2307
        (args: GridMouseEventArgs) => {
649✔
2308
            if (mouseDownData?.current?.button !== undefined && mouseDownData.current.button >= 1) return;
32✔
2309
            if (
31✔
2310
                mouseState !== undefined &&
31✔
2311
                mouseDownData.current?.location[0] === 0 &&
18✔
2312
                args.location[0] === 0 &&
3✔
2313
                rowMarkerOffset === 1 &&
3✔
2314
                rowSelect === "multi" &&
3✔
2315
                mouseState.previousSelection &&
2✔
2316
                !mouseState.previousSelection.rows.hasIndex(mouseDownData.current.location[1]) &&
2✔
2317
                gridSelection.rows.hasIndex(mouseDownData.current.location[1])
2✔
2318
            ) {
32✔
2319
                const start = Math.min(mouseDownData.current.location[1], args.location[1]);
2✔
2320
                const end = Math.max(mouseDownData.current.location[1], args.location[1]) + 1;
2✔
2321
                setSelectedRows(CompactSelection.fromSingleSelection([start, end]), undefined, false);
2✔
2322
            }
2✔
2323
            if (
31✔
2324
                mouseState !== undefined &&
31✔
2325
                gridSelection.current !== undefined &&
18✔
2326
                !isActivelyDragging.current &&
16✔
2327
                (rangeSelect === "rect" || rangeSelect === "multi-rect")
16✔
2328
            ) {
32✔
2329
                const [selectedCol, selectedRow] = gridSelection.current.cell;
10✔
2330
                // eslint-disable-next-line prefer-const
10✔
2331
                let [col, row] = args.location;
10✔
2332

10✔
2333
                if (row < 0) {
10✔
2334
                    row = visibleRegionRef.current.y;
1✔
2335
                }
1✔
2336

10✔
2337
                const startedFromLastStickyRow = lastRowSticky && selectedRow === rows;
10✔
2338
                if (startedFromLastStickyRow) return;
10!
2339

10✔
2340
                const landedOnLastStickyRow = lastRowSticky && row === rows;
10✔
2341
                if (landedOnLastStickyRow) {
10✔
2342
                    if (args.kind === outOfBoundsKind) row--;
3✔
2343
                    else return;
1✔
2344
                }
3✔
2345

9✔
2346
                col = Math.max(col, rowMarkerOffset);
9✔
2347

9✔
2348
                const deltaX = col - selectedCol;
9✔
2349
                const deltaY = row - selectedRow;
9✔
2350

9✔
2351
                const newRange: Rectangle = {
9✔
2352
                    x: deltaX >= 0 ? selectedCol : col,
10!
2353
                    y: deltaY >= 0 ? selectedRow : row,
10✔
2354
                    width: Math.abs(deltaX) + 1,
10✔
2355
                    height: Math.abs(deltaY) + 1,
10✔
2356
                };
10✔
2357

10✔
2358
                setCurrent(
10✔
2359
                    {
10✔
2360
                        ...gridSelection.current,
10✔
2361
                        range: newRange,
10✔
2362
                    },
10✔
2363
                    true,
10✔
2364
                    false,
10✔
2365
                    "drag"
10✔
2366
                );
10✔
2367
            }
10✔
2368

30✔
2369
            onItemHovered?.({ ...args, location: [args.location[0] - rowMarkerOffset, args.location[1]] as any });
32✔
2370
        },
32✔
2371
        [
649✔
2372
            mouseState,
649✔
2373
            rowMarkerOffset,
649✔
2374
            rowSelect,
649✔
2375
            gridSelection,
649✔
2376
            rangeSelect,
649✔
2377
            onItemHovered,
649✔
2378
            setSelectedRows,
649✔
2379
            lastRowSticky,
649✔
2380
            rows,
649✔
2381
            setCurrent,
649✔
2382
        ]
649✔
2383
    );
649✔
2384

649✔
2385
    // 1 === move one
649✔
2386
    // 2 === move to end
649✔
2387
    const adjustSelection = React.useCallback(
649✔
2388
        (direction: [0 | 1 | -1 | 2 | -2, 0 | 1 | -1 | 2 | -2]) => {
649✔
2389
            if (gridSelection.current === undefined) return;
8!
2390

8✔
2391
            const [x, y] = direction;
8✔
2392
            const [col, row] = gridSelection.current.cell;
8✔
2393
            const old = gridSelection.current.range;
8✔
2394
            let left = old.x;
8✔
2395
            let right = old.x + old.width;
8✔
2396
            let top = old.y;
8✔
2397
            let bottom = old.y + old.height;
8✔
2398

8✔
2399
            // take care of vertical first in case new spans come in
8✔
2400
            if (y !== 0) {
8✔
2401
                switch (y) {
2✔
2402
                    case 2: {
2✔
2403
                        // go to end
1✔
2404
                        bottom = rows;
1✔
2405
                        top = row;
1✔
2406
                        scrollTo(0, bottom, "vertical");
1✔
2407

1✔
2408
                        break;
1✔
2409
                    }
1✔
2410
                    case -2: {
2!
UNCOV
2411
                        // go to start
×
2412
                        top = 0;
×
2413
                        bottom = row + 1;
×
2414
                        scrollTo(0, top, "vertical");
×
UNCOV
2415

×
2416
                        break;
×
UNCOV
2417
                    }
×
2418
                    case 1: {
2✔
2419
                        // motion down
1✔
2420
                        if (top < row) {
1!
2421
                            top++;
×
2422
                            scrollTo(0, top, "vertical");
×
2423
                        } else {
1✔
2424
                            bottom = Math.min(rows, bottom + 1);
1✔
2425
                            scrollTo(0, bottom, "vertical");
1✔
2426
                        }
1✔
2427

1✔
2428
                        break;
1✔
2429
                    }
1✔
2430
                    case -1: {
2!
UNCOV
2431
                        // motion up
×
UNCOV
2432
                        if (bottom > row + 1) {
×
2433
                            bottom--;
×
2434
                            scrollTo(0, bottom, "vertical");
×
UNCOV
2435
                        } else {
×
2436
                            top = Math.max(0, top - 1);
×
2437
                            scrollTo(0, top, "vertical");
×
UNCOV
2438
                        }
×
UNCOV
2439

×
2440
                        break;
×
UNCOV
2441
                    }
×
2442
                    default: {
2!
2443
                        assertNever(y);
×
UNCOV
2444
                    }
×
2445
                }
2✔
2446
            }
2✔
2447

8✔
2448
            if (x !== 0) {
8✔
2449
                if (x === 2) {
6✔
2450
                    right = mangledCols.length;
1✔
2451
                    left = col;
1✔
2452
                    scrollTo(right - 1 - rowMarkerOffset, 0, "horizontal");
1✔
2453
                } else if (x === -2) {
6!
2454
                    left = rowMarkerOffset;
×
2455
                    right = col + 1;
×
2456
                    scrollTo(left - rowMarkerOffset, 0, "horizontal");
×
2457
                } else {
5✔
2458
                    let disallowed: number[] = [];
5✔
2459
                    if (getCellsForSelection !== undefined) {
5✔
2460
                        const cells = getCellsForSelection(
5✔
2461
                            {
5✔
2462
                                x: left,
5✔
2463
                                y: top,
5✔
2464
                                width: right - left - rowMarkerOffset,
5✔
2465
                                height: bottom - top,
5✔
2466
                            },
5✔
2467
                            abortControllerRef.current.signal
5✔
2468
                        );
5✔
2469

5✔
2470
                        if (typeof cells === "object") {
5✔
2471
                            disallowed = getSpanStops(cells);
5✔
2472
                        }
5✔
2473
                    }
5✔
2474
                    if (x === 1) {
5✔
2475
                        // motion right
4✔
2476
                        let done = false;
4✔
2477
                        if (left < col) {
4!
UNCOV
2478
                            if (disallowed.length > 0) {
×
2479
                                const target = range(left + 1, col + 1).find(
×
2480
                                    n => !disallowed.includes(n - rowMarkerOffset)
×
UNCOV
2481
                                );
×
UNCOV
2482
                                if (target !== undefined) {
×
2483
                                    left = target;
×
2484
                                    done = true;
×
UNCOV
2485
                                }
×
UNCOV
2486
                            } else {
×
2487
                                left++;
×
2488
                                done = true;
×
UNCOV
2489
                            }
×
2490
                            if (done) scrollTo(left, 0, "horizontal");
×
UNCOV
2491
                        }
×
2492
                        if (!done) {
4✔
2493
                            right = Math.min(mangledCols.length, right + 1);
4✔
2494
                            scrollTo(right - 1 - rowMarkerOffset, 0, "horizontal");
4✔
2495
                        }
4✔
2496
                    } else if (x === -1) {
5✔
2497
                        // motion left
1✔
2498
                        let done = false;
1✔
2499
                        if (right > col + 1) {
1!
UNCOV
2500
                            if (disallowed.length > 0) {
×
2501
                                const target = range(right - 1, col, -1).find(
×
2502
                                    n => !disallowed.includes(n - rowMarkerOffset)
×
UNCOV
2503
                                );
×
UNCOV
2504
                                if (target !== undefined) {
×
2505
                                    right = target;
×
2506
                                    done = true;
×
UNCOV
2507
                                }
×
UNCOV
2508
                            } else {
×
2509
                                right--;
×
2510
                                done = true;
×
UNCOV
2511
                            }
×
2512
                            if (done) scrollTo(right - rowMarkerOffset, 0, "horizontal");
×
UNCOV
2513
                        }
×
2514
                        if (!done) {
1✔
2515
                            left = Math.max(rowMarkerOffset, left - 1);
1✔
2516
                            scrollTo(left - rowMarkerOffset, 0, "horizontal");
1✔
2517
                        }
1✔
2518
                    } else {
1!
2519
                        assertNever(x);
×
UNCOV
2520
                    }
×
2521
                }
5✔
2522
            }
6✔
2523

8✔
2524
            setCurrent(
8✔
2525
                {
8✔
2526
                    cell: gridSelection.current.cell,
8✔
2527
                    range: {
8✔
2528
                        x: left,
8✔
2529
                        y: top,
8✔
2530
                        width: right - left,
8✔
2531
                        height: bottom - top,
8✔
2532
                    },
8✔
2533
                },
8✔
2534
                true,
8✔
2535
                false,
8✔
2536
                "keyboard-select"
8✔
2537
            );
8✔
2538
        },
8✔
2539
        [getCellsForSelection, gridSelection, mangledCols.length, rowMarkerOffset, rows, scrollTo, setCurrent]
649✔
2540
    );
649✔
2541

649✔
2542
    const updateSelectedCell = React.useCallback(
649✔
2543
        (col: number, row: number, fromEditingTrailingRow: boolean, freeMove: boolean): boolean => {
649✔
2544
            const rowMax = mangledRows - (fromEditingTrailingRow ? 0 : 1);
53!
2545
            col = clamp(col, rowMarkerOffset, columns.length - 1 + rowMarkerOffset);
53✔
2546
            row = clamp(row, 0, rowMax);
53✔
2547

53✔
2548
            if (col === currentCell?.[0] && row === currentCell?.[1]) return false;
53✔
2549
            if (freeMove && gridSelection.current !== undefined) {
53✔
2550
                const newStack = [...gridSelection.current.rangeStack];
1✔
2551
                if (gridSelection.current.range.width > 1 || gridSelection.current.range.height > 1) {
1!
2552
                    newStack.push(gridSelection.current.range);
1✔
2553
                }
1✔
2554
                setGridSelection(
1✔
2555
                    {
1✔
2556
                        ...gridSelection,
1✔
2557
                        current: {
1✔
2558
                            cell: [col, row],
1✔
2559
                            range: { x: col, y: row, width: 1, height: 1 },
1✔
2560
                            rangeStack: newStack,
1✔
2561
                        },
1✔
2562
                    },
1✔
2563
                    true
1✔
2564
                );
1✔
2565
            } else {
53✔
2566
                setCurrent(
25✔
2567
                    {
25✔
2568
                        cell: [col, row],
25✔
2569
                        range: { x: col, y: row, width: 1, height: 1 },
25✔
2570
                    },
25✔
2571
                    true,
25✔
2572
                    false,
25✔
2573
                    "keyboard-nav"
25✔
2574
                );
25✔
2575
            }
25✔
2576

26✔
2577
            if (lastSent.current !== undefined && lastSent.current[0] === col && lastSent.current[1] === row) {
53✔
2578
                lastSent.current = undefined;
2✔
2579
            }
2✔
2580

26✔
2581
            scrollTo(col - rowMarkerOffset, row);
26✔
2582

26✔
2583
            return true;
26✔
2584
        },
53✔
2585
        [
649✔
2586
            mangledRows,
649✔
2587
            rowMarkerOffset,
649✔
2588
            columns.length,
649✔
2589
            currentCell,
649✔
2590
            gridSelection,
649✔
2591
            scrollTo,
649✔
2592
            setGridSelection,
649✔
2593
            setCurrent,
649✔
2594
        ]
649✔
2595
    );
649✔
2596

649✔
2597
    const onFinishEditing = React.useCallback(
649✔
2598
        (newValue: GridCell | undefined, movement: readonly [-1 | 0 | 1, -1 | 0 | 1]) => {
649✔
2599
            if (overlay?.cell !== undefined && newValue !== undefined && isEditableGridCell(newValue)) {
7✔
2600
                mangledOnCellsEdited([{ location: overlay.cell, value: newValue }]);
4✔
2601
                window.requestAnimationFrame(() => {
4✔
2602
                    gridRef.current?.damage([
4✔
2603
                        {
4✔
2604
                            cell: overlay.cell,
4✔
2605
                        },
4✔
2606
                    ]);
4✔
2607
                });
4✔
2608
            }
4✔
2609
            focus(true);
7✔
2610
            setOverlay(undefined);
7✔
2611

7✔
2612
            const [movX, movY] = movement;
7✔
2613
            if (gridSelection.current !== undefined && (movX !== 0 || movY !== 0)) {
7✔
2614
                const isEditingTrailingRow =
3✔
2615
                    gridSelection.current.cell[1] === mangledRows - 1 && newValue !== undefined;
3!
2616
                updateSelectedCell(
3✔
2617
                    clamp(gridSelection.current.cell[0] + movX, 0, mangledCols.length - 1),
3✔
2618
                    clamp(gridSelection.current.cell[1] + movY, 0, mangledRows - 1),
3✔
2619
                    isEditingTrailingRow,
3✔
2620
                    false
3✔
2621
                );
3✔
2622
            }
3✔
2623
            onFinishedEditing?.(newValue, movement);
7✔
2624
        },
7✔
2625
        [
649✔
2626
            overlay?.cell,
649✔
2627
            focus,
649✔
2628
            gridSelection,
649✔
2629
            onFinishedEditing,
649✔
2630
            mangledOnCellsEdited,
649✔
2631
            mangledRows,
649✔
2632
            updateSelectedCell,
649✔
2633
            mangledCols.length,
649✔
2634
        ]
649✔
2635
    );
649✔
2636

649✔
2637
    const overlayID = React.useMemo(() => {
649✔
2638
        return `gdg-overlay-${idCounter++}`;
132✔
2639
    }, []);
649✔
2640

649✔
2641
    const deleteRange = React.useCallback(
649✔
2642
        (r: Rectangle) => {
649✔
2643
            focus();
8✔
2644
            const editList: EditListItem[] = [];
8✔
2645
            for (let x = r.x; x < r.x + r.width; x++) {
8✔
2646
                for (let y = r.y; y < r.y + r.height; y++) {
23✔
2647
                    const cellValue = getCellContent([x - rowMarkerOffset, y]);
1,066✔
2648
                    if (!cellValue.allowOverlay && cellValue.kind !== GridCellKind.Boolean) continue;
1,066✔
2649
                    let newVal: InnerGridCell | undefined = undefined;
1,042✔
2650
                    if (cellValue.kind === GridCellKind.Custom) {
1,066✔
2651
                        const toDelete = getCellRenderer(cellValue);
1✔
2652
                        const editor = toDelete?.provideEditor?.(cellValue);
1!
2653
                        if (toDelete?.onDelete !== undefined) {
1✔
2654
                            newVal = toDelete.onDelete(cellValue);
1✔
2655
                        } else if (isObjectEditorCallbackResult(editor)) {
1!
2656
                            newVal = editor?.deletedValue?.(cellValue);
×
UNCOV
2657
                        }
×
2658
                    } else if (
1✔
2659
                        (isEditableGridCell(cellValue) && cellValue.allowOverlay) ||
1,041✔
2660
                        cellValue.kind === GridCellKind.Boolean
1✔
2661
                    ) {
1,041✔
2662
                        const toDelete = getCellRenderer(cellValue);
1,041✔
2663
                        newVal = toDelete?.onDelete?.(cellValue);
1,041✔
2664
                    }
1,041✔
2665
                    if (newVal !== undefined && !isInnerOnlyCell(newVal) && isEditableGridCell(newVal)) {
1,066✔
2666
                        editList.push({ location: [x, y], value: newVal });
1,041✔
2667
                    }
1,041✔
2668
                }
1,066✔
2669
            }
23✔
2670
            mangledOnCellsEdited(editList);
8✔
2671
            gridRef.current?.damage(editList.map(x => ({ cell: x.location })));
8✔
2672
        },
8✔
2673
        [focus, getCellContent, getCellRenderer, mangledOnCellsEdited, rowMarkerOffset]
649✔
2674
    );
649✔
2675

649✔
2676
    const onKeyDown = React.useCallback(
649✔
2677
        (event: GridKeyEventArgs) => {
649✔
2678
            const fn = async () => {
61✔
2679
                let cancelled = false;
61✔
2680
                if (onKeyDownIn !== undefined) {
61✔
2681
                    onKeyDownIn({
1✔
2682
                        ...event,
1✔
2683
                        cancel: () => {
1✔
2684
                            cancelled = true;
×
UNCOV
2685
                        },
×
2686
                    });
1✔
2687
                }
1✔
2688

61✔
2689
                if (cancelled) return;
61!
2690

61✔
2691
                const cancel = () => {
61✔
2692
                    event.stopPropagation();
42✔
2693
                    event.preventDefault();
42✔
2694
                };
42✔
2695

61✔
2696
                const overlayOpen = overlay !== undefined;
61✔
2697
                const { altKey, shiftKey, metaKey, ctrlKey, key, bounds } = event;
61✔
2698
                const isOSX = browserIsOSX.value;
61✔
2699
                const isPrimaryKey = isOSX ? metaKey : ctrlKey;
61!
2700
                const isDeleteKey = key === "Delete" || (isOSX && key === "Backspace");
61!
2701
                const vr = visibleRegionRef.current;
61✔
2702
                const selectedColumns = gridSelection.columns;
61✔
2703
                const selectedRows = gridSelection.rows;
61✔
2704

61✔
2705
                if (key === "Escape") {
61✔
2706
                    if (overlayOpen) {
4✔
2707
                        setOverlay(undefined);
2✔
2708
                    } else if (keybindings.clear) {
2✔
2709
                        setGridSelection(emptyGridSelection, false);
2✔
2710
                        onSelectionCleared?.();
2!
2711
                    }
2✔
2712
                    return;
4✔
2713
                } else if (isHotkey("primary+a", event) && keybindings.selectAll) {
61✔
2714
                    if (!overlayOpen) {
1✔
2715
                        setGridSelection(
1✔
2716
                            {
1✔
2717
                                columns: CompactSelection.empty(),
1✔
2718
                                rows: CompactSelection.empty(),
1✔
2719
                                current: {
1✔
2720
                                    cell: gridSelection.current?.cell ?? [rowMarkerOffset, 0],
1!
2721
                                    range: {
1✔
2722
                                        x: rowMarkerOffset,
1✔
2723
                                        y: 0,
1✔
2724
                                        width: columnsIn.length,
1✔
2725
                                        height: rows,
1✔
2726
                                    },
1✔
2727
                                    rangeStack: [],
1✔
2728
                                },
1✔
2729
                            },
1✔
2730
                            false
1✔
2731
                        );
1✔
2732
                    } else {
1!
2733
                        const el = document.getElementById(overlayID);
×
UNCOV
2734
                        if (el !== null) {
×
2735
                            const s = window.getSelection();
×
2736
                            const r = document.createRange();
×
2737
                            r.selectNodeContents(el);
×
2738
                            s?.removeAllRanges();
×
2739
                            s?.addRange(r);
×
UNCOV
2740
                        }
×
UNCOV
2741
                    }
×
2742
                    cancel();
1✔
2743
                    return;
1✔
2744
                } else if (isHotkey("primary+f", event) && keybindings.search) {
57!
2745
                    cancel();
×
2746
                    searchInputRef?.current?.focus({ preventScroll: true });
×
2747
                    setShowSearchInner(true);
×
UNCOV
2748
                }
✔
2749

56✔
2750
                if (isDeleteKey) {
61✔
2751
                    const callbackResult = onDelete?.(gridSelection) ?? true;
8✔
2752
                    cancel();
8✔
2753
                    if (callbackResult !== false) {
8✔
2754
                        const toDelete = callbackResult === true ? gridSelection : callbackResult;
8✔
2755

8✔
2756
                        // delete order:
8✔
2757
                        // 1) primary range
8✔
2758
                        // 2) secondary ranges
8✔
2759
                        // 3) columns
8✔
2760
                        // 4) rows
8✔
2761

8✔
2762
                        if (toDelete.current !== undefined) {
8✔
2763
                            deleteRange(toDelete.current.range);
5✔
2764
                            for (const r of toDelete.current.rangeStack) {
5!
2765
                                deleteRange(r);
×
UNCOV
2766
                            }
×
2767
                        }
5✔
2768

8✔
2769
                        for (const r of toDelete.rows) {
8✔
2770
                            deleteRange({
1✔
2771
                                x: rowMarkerOffset,
1✔
2772
                                y: r,
1✔
2773
                                width: mangledCols.length - rowMarkerOffset,
1✔
2774
                                height: 1,
1✔
2775
                            });
1✔
2776
                        }
1✔
2777

8✔
2778
                        for (const col of toDelete.columns) {
8✔
2779
                            deleteRange({
1✔
2780
                                x: col,
1✔
2781
                                y: 0,
1✔
2782
                                width: 1,
1✔
2783
                                height: rows,
1✔
2784
                            });
1✔
2785
                        }
1✔
2786
                    }
8✔
2787
                    return;
8✔
2788
                }
8✔
2789

48✔
2790
                if (gridSelection.current === undefined) return;
48✔
2791
                let [col, row] = gridSelection.current.cell;
45✔
2792
                let freeMove = false;
45✔
2793

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

45✔
2971
                const moved = updateSelectedCell(col, row, false, freeMove);
45✔
2972
                if (moved) {
61✔
2973
                    cancel();
18✔
2974
                }
18✔
2975
            };
61✔
2976
            void fn();
61✔
2977
        },
61✔
2978
        [
649✔
2979
            onKeyDownIn,
649✔
2980
            deleteRange,
649✔
2981
            overlay,
649✔
2982
            gridSelection,
649✔
2983
            keybindings.selectAll,
649✔
2984
            keybindings.search,
649✔
2985
            keybindings.selectColumn,
649✔
2986
            keybindings.selectRow,
649✔
2987
            keybindings.downFill,
649✔
2988
            keybindings.rightFill,
649✔
2989
            keybindings.pageDown,
649✔
2990
            keybindings.pageUp,
649✔
2991
            keybindings.first,
649✔
2992
            keybindings.last,
649✔
2993
            keybindings.clear,
649✔
2994
            columnSelect,
649✔
2995
            rowSelect,
649✔
2996
            getCellContent,
649✔
2997
            rowMarkerOffset,
649✔
2998
            updateSelectedCell,
649✔
2999
            setGridSelection,
649✔
3000
            onSelectionCleared,
649✔
3001
            columnsIn.length,
649✔
3002
            rows,
649✔
3003
            overlayID,
649✔
3004
            mangledOnCellsEdited,
649✔
3005
            onDelete,
649✔
3006
            mangledCols.length,
649✔
3007
            setSelectedColumns,
649✔
3008
            setSelectedRows,
649✔
3009
            showTrailingBlankRow,
649✔
3010
            getCustomNewRowTargetColumn,
649✔
3011
            appendRow,
649✔
3012
            onCellActivated,
649✔
3013
            reselect,
649✔
3014
            fillDown,
649✔
3015
            getMangledCellContent,
649✔
3016
            adjustSelection,
649✔
3017
            rangeSelect,
649✔
3018
            lastRowSticky,
649✔
3019
        ]
649✔
3020
    );
649✔
3021

649✔
3022
    const onContextMenu = React.useCallback(
649✔
3023
        (args: GridMouseEventArgs, preventDefault: () => void) => {
649✔
3024
            const adjustedCol = args.location[0] - rowMarkerOffset;
7✔
3025
            if (args.kind === "header") {
7!
3026
                onHeaderContextMenu?.(adjustedCol, { ...args, preventDefault });
×
UNCOV
3027
            }
×
3028

7✔
3029
            if (args.kind === groupHeaderKind) {
7!
UNCOV
3030
                if (adjustedCol < 0) {
×
3031
                    return;
×
UNCOV
3032
                }
×
3033
                onGroupHeaderContextMenu?.(adjustedCol, { ...args, preventDefault });
×
UNCOV
3034
            }
×
3035

7✔
3036
            if (args.kind === "cell") {
7✔
3037
                const [col, row] = args.location;
7✔
3038
                onCellContextMenu?.([adjustedCol, row], {
7✔
3039
                    ...args,
7✔
3040
                    preventDefault,
7✔
3041
                });
7✔
3042

7✔
3043
                if (!gridSelectionHasItem(gridSelection, args.location)) {
7✔
3044
                    updateSelectedCell(col, row, false, false);
3✔
3045
                }
3✔
3046
            }
7✔
3047
        },
7✔
3048
        [
649✔
3049
            gridSelection,
649✔
3050
            onCellContextMenu,
649✔
3051
            onGroupHeaderContextMenu,
649✔
3052
            onHeaderContextMenu,
649✔
3053
            rowMarkerOffset,
649✔
3054
            updateSelectedCell,
649✔
3055
        ]
649✔
3056
    );
649✔
3057

649✔
3058
    const onPasteInternal = React.useCallback(
649✔
3059
        async (e?: ClipboardEvent) => {
649✔
3060
            if (!keybindings.paste) return;
6!
3061
            function pasteToCell(
6✔
3062
                inner: InnerGridCell,
51✔
3063
                target: Item,
51✔
3064
                rawValue: string | boolean | string[] | number | boolean | BooleanEmpty | BooleanIndeterminate,
51✔
3065
                formatted?: string | string[]
51✔
3066
            ): EditListItem | undefined {
51✔
3067
                const stringifiedRawValue =
51✔
3068
                    typeof rawValue === "object" ? rawValue?.join("\n") ?? "" : rawValue?.toString() ?? "";
51!
3069

51✔
3070
                if (!isInnerOnlyCell(inner) && isReadWriteCell(inner) && inner.readonly !== true) {
51✔
3071
                    const coerced = coercePasteValue?.(stringifiedRawValue, inner);
51!
3072
                    if (coerced !== undefined && isEditableGridCell(coerced)) {
51!
UNCOV
3073
                        if (process.env.NODE_ENV !== "production" && coerced.kind !== inner.kind) {
×
UNCOV
3074
                            // eslint-disable-next-line no-console
×
3075
                            console.warn("Coercion should not change cell kind.");
×
UNCOV
3076
                        }
×
3077
                        return {
×
UNCOV
3078
                            location: target,
×
UNCOV
3079
                            value: coerced,
×
UNCOV
3080
                        };
×
UNCOV
3081
                    }
×
3082
                    const r = getCellRenderer(inner);
51✔
3083
                    if (r === undefined) return undefined;
51!
3084
                    if (r.kind === GridCellKind.Custom) {
51✔
3085
                        assert(inner.kind === GridCellKind.Custom);
1✔
3086
                        const newVal = (r as unknown as CustomRenderer<CustomCell<any>>).onPaste?.(
1✔
3087
                            stringifiedRawValue,
1✔
3088
                            inner.data
1✔
3089
                        );
1✔
3090
                        if (newVal === undefined) return undefined;
1!
3091
                        return {
×
UNCOV
3092
                            location: target,
×
UNCOV
3093
                            value: {
×
UNCOV
3094
                                ...inner,
×
UNCOV
3095
                                data: newVal,
×
UNCOV
3096
                            },
×
UNCOV
3097
                        };
×
3098
                    } else {
51✔
3099
                        const newVal = r.onPaste?.(stringifiedRawValue, inner, {
50✔
3100
                            formatted,
50✔
3101
                            formattedString: typeof formatted === "string" ? formatted : formatted?.join("\n"),
50!
3102
                            rawValue,
50✔
3103
                        });
50✔
3104
                        if (newVal === undefined) return undefined;
50✔
3105
                        assert(newVal.kind === inner.kind);
36✔
3106
                        return {
36✔
3107
                            location: target,
36✔
3108
                            value: newVal,
36✔
3109
                        };
36✔
3110
                    }
36✔
3111
                }
51!
3112
                return undefined;
×
3113
            }
51✔
3114

6✔
3115
            const selectedColumns = gridSelection.columns;
6✔
3116
            const selectedRows = gridSelection.rows;
6✔
3117
            const focused =
6✔
3118
                scrollRef.current?.contains(document.activeElement) === true ||
6✔
3119
                canvasRef.current?.contains(document.activeElement) === true;
6✔
3120

6✔
3121
            let target = gridSelection.current?.cell;
6✔
3122
            if (target === undefined && selectedColumns.length === 1) {
6!
3123
                target = [selectedColumns.first() ?? 0, 0];
×
UNCOV
3124
            }
×
3125
            if (target === undefined && selectedRows.length === 1) {
6!
3126
                target = [rowMarkerOffset, selectedRows.first() ?? 0];
×
UNCOV
3127
            }
×
3128

6✔
3129
            if (focused && target !== undefined) {
6✔
3130
                let data: CopyBuffer | undefined;
5✔
3131
                let text: string | undefined;
5✔
3132

5✔
3133
                const textPlain = "text/plain";
5✔
3134
                const textHtml = "text/html";
5✔
3135

5✔
3136
                if (navigator.clipboard.read !== undefined) {
5!
3137
                    const clipboardContent = await navigator.clipboard.read();
×
UNCOV
3138

×
UNCOV
3139
                    for (const item of clipboardContent) {
×
UNCOV
3140
                        if (item.types.includes(textHtml)) {
×
3141
                            const htmlBlob = await item.getType(textHtml);
×
3142
                            const html = await htmlBlob.text();
×
3143
                            const decoded = decodeHTML(html);
×
UNCOV
3144
                            if (decoded !== undefined) {
×
3145
                                data = decoded;
×
3146
                                break;
×
UNCOV
3147
                            }
×
UNCOV
3148
                        }
×
UNCOV
3149
                        if (item.types.includes(textPlain)) {
×
UNCOV
3150
                            // eslint-disable-next-line unicorn/no-await-expression-member
×
3151
                            text = await (await item.getType(textPlain)).text();
×
UNCOV
3152
                        }
×
UNCOV
3153
                    }
×
3154
                } else if (navigator.clipboard.readText !== undefined) {
5✔
3155
                    text = await navigator.clipboard.readText();
5✔
3156
                } else if (e !== undefined && e?.clipboardData !== null) {
5!
UNCOV
3157
                    if (e.clipboardData.types.includes(textHtml)) {
×
3158
                        const html = e.clipboardData.getData(textHtml);
×
3159
                        data = decodeHTML(html);
×
UNCOV
3160
                    }
×
UNCOV
3161
                    if (data === undefined && e.clipboardData.types.includes(textPlain)) {
×
3162
                        text = e.clipboardData.getData(textPlain);
×
UNCOV
3163
                    }
×
UNCOV
3164
                } else {
×
3165
                    return; // I didn't want to read that paste value anyway
×
UNCOV
3166
                }
×
3167

5✔
3168
                const [gridCol, gridRow] = target;
5✔
3169

5✔
3170
                const editList: EditListItem[] = [];
5✔
3171
                do {
5✔
3172
                    if (onPaste === undefined) {
5✔
3173
                        const cellData = getMangledCellContent(target);
2✔
3174
                        const rawValue = text ?? data?.map(r => r.map(cb => cb.rawValue).join("\t")).join("\t") ?? "";
2!
3175
                        const newVal = pasteToCell(cellData, target, rawValue, undefined);
2✔
3176
                        if (newVal !== undefined) {
2✔
3177
                            editList.push(newVal);
1✔
3178
                        }
1✔
3179
                        break;
2✔
3180
                    }
2✔
3181

3✔
3182
                    if (data === undefined) {
3✔
3183
                        if (text === undefined) return;
3!
3184
                        data = unquote(text);
3✔
3185
                    }
3✔
3186

3✔
3187
                    if (
3✔
3188
                        onPaste === false ||
3✔
3189
                        (typeof onPaste === "function" &&
3✔
3190
                            onPaste?.(
2✔
3191
                                [target[0] - rowMarkerOffset, target[1]],
2✔
3192
                                data.map(r => r.map(cb => cb.rawValue?.toString() ?? ""))
2!
3193
                            ) !== true)
2✔
3194
                    ) {
5!
3195
                        return;
×
UNCOV
3196
                    }
✔
3197

3✔
3198
                    for (const [row, dataRow] of data.entries()) {
5✔
3199
                        if (row + gridRow >= rows) break;
21!
3200
                        for (const [col, dataItem] of dataRow.entries()) {
21✔
3201
                            const index = [col + gridCol, row + gridRow] as const;
63✔
3202
                            const [writeCol, writeRow] = index;
63✔
3203
                            if (writeCol >= mangledCols.length) continue;
63✔
3204
                            if (writeRow >= mangledRows) continue;
49!
3205
                            const cellData = getMangledCellContent(index);
49✔
3206
                            const newVal = pasteToCell(cellData, index, dataItem.rawValue, dataItem.formatted);
49✔
3207
                            if (newVal !== undefined) {
63✔
3208
                                editList.push(newVal);
35✔
3209
                            }
35✔
3210
                        }
63✔
3211
                    }
21✔
3212
                    // eslint-disable-next-line no-constant-condition
3✔
3213
                } while (false);
5✔
3214

5✔
3215
                mangledOnCellsEdited(editList);
5✔
3216

5✔
3217
                gridRef.current?.damage(
5✔
3218
                    editList.map(c => ({
5✔
3219
                        cell: c.location,
36✔
3220
                    }))
5✔
3221
                );
5✔
3222
            }
5✔
3223
        },
6✔
3224
        [
649✔
3225
            coercePasteValue,
649✔
3226
            getCellRenderer,
649✔
3227
            getMangledCellContent,
649✔
3228
            gridSelection,
649✔
3229
            keybindings.paste,
649✔
3230
            mangledCols.length,
649✔
3231
            mangledOnCellsEdited,
649✔
3232
            mangledRows,
649✔
3233
            onPaste,
649✔
3234
            rowMarkerOffset,
649✔
3235
            rows,
649✔
3236
        ]
649✔
3237
    );
649✔
3238

649✔
3239
    useEventListener("paste", onPasteInternal, safeWindow, false, true);
649✔
3240

649✔
3241
    // While this function is async, we deeply prefer not to await if we don't have to. This will lead to unpacking
649✔
3242
    // promises in rather awkward ways when possible to avoid awaiting. We have to use fallback copy mechanisms when
649✔
3243
    // an await has happened.
649✔
3244
    const onCopy = React.useCallback(
649✔
3245
        async (e?: ClipboardEvent, ignoreFocus?: boolean) => {
649✔
3246
            if (!keybindings.copy) return;
6!
3247
            const focused =
6✔
3248
                ignoreFocus === true ||
6✔
3249
                scrollRef.current?.contains(document.activeElement) === true ||
5✔
3250
                canvasRef.current?.contains(document.activeElement) === true;
5✔
3251

6✔
3252
            const selectedColumns = gridSelection.columns;
6✔
3253
            const selectedRows = gridSelection.rows;
6✔
3254

6✔
3255
            const copyToClipboardWithHeaders = (
6✔
3256
                cells: readonly (readonly GridCell[])[],
5✔
3257
                columnIndexes: readonly number[]
5✔
3258
            ) => {
5✔
3259
                if (!copyHeaders) {
5✔
3260
                    copyToClipboard(cells, columnIndexes, e);
5✔
3261
                } else {
5!
3262
                    const headers = columnIndexes.map(index => ({
×
UNCOV
3263
                        kind: GridCellKind.Text,
×
UNCOV
3264
                        data: columnsIn[index].title,
×
UNCOV
3265
                        displayData: columnsIn[index].title,
×
UNCOV
3266
                        allowOverlay: false,
×
UNCOV
3267
                    })) as GridCell[];
×
3268
                    copyToClipboard([headers, ...cells], columnIndexes, e);
×
UNCOV
3269
                }
×
3270
            };
5✔
3271

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

649✔
3340
    useEventListener("copy", onCopy, safeWindow, false, false);
649✔
3341

649✔
3342
    const onCut = React.useCallback(
649✔
3343
        async (e?: ClipboardEvent) => {
649✔
3344
            if (!keybindings.cut) return;
1!
3345
            const focused =
1✔
3346
                scrollRef.current?.contains(document.activeElement) === true ||
1✔
3347
                canvasRef.current?.contains(document.activeElement) === true;
1✔
3348

1✔
3349
            if (!focused) return;
1!
3350
            await onCopy(e);
1✔
3351
            if (gridSelection.current !== undefined) {
1✔
3352
                deleteRange(gridSelection.current.range);
1✔
3353
            }
1✔
3354
        },
1✔
3355
        [deleteRange, gridSelection, keybindings.cut, onCopy]
649✔
3356
    );
649✔
3357

649✔
3358
    useEventListener("cut", onCut, safeWindow, false, false);
649✔
3359

649✔
3360
    const onSearchResultsChanged = React.useCallback(
649✔
3361
        (results: readonly Item[], navIndex: number) => {
649✔
3362
            if (onSearchResultsChangedIn !== undefined) {
7!
UNCOV
3363
                if (rowMarkerOffset !== 0) {
×
3364
                    results = results.map(item => [item[0] - rowMarkerOffset, item[1]]);
×
UNCOV
3365
                }
×
3366
                onSearchResultsChangedIn(results, navIndex);
×
3367
                return;
×
UNCOV
3368
            }
×
3369
            if (results.length === 0 || navIndex === -1) return;
7✔
3370

2✔
3371
            const [col, row] = results[navIndex];
2✔
3372
            if (lastSent.current !== undefined && lastSent.current[0] === col && lastSent.current[1] === row) {
7!
3373
                return;
×
UNCOV
3374
            }
✔
3375
            lastSent.current = [col, row];
2✔
3376
            updateSelectedCell(col, row, false, false);
2✔
3377
        },
7✔
3378
        [onSearchResultsChangedIn, rowMarkerOffset, updateSelectedCell]
649✔
3379
    );
649✔
3380

649✔
3381
    // this effects purpose in life is to scroll the newly selected cell into view when and ONLY when that cell
649✔
3382
    // is from an external gridSelection change. Also note we want the unmangled out selection because scrollTo
649✔
3383
    // expects unmangled indexes
649✔
3384
    const [outCol, outRow] = gridSelectionOuter?.current?.cell ?? [];
649✔
3385
    const scrollToRef = React.useRef(scrollTo);
649✔
3386
    scrollToRef.current = scrollTo;
649✔
3387
    React.useLayoutEffect(() => {
649✔
3388
        if (
205✔
3389
            !hasJustScrolled.current &&
205✔
3390
            outCol !== undefined &&
205✔
3391
            outRow !== undefined &&
76✔
3392
            (outCol !== expectedExternalGridSelection.current?.current?.cell[0] ||
76✔
3393
                outRow !== expectedExternalGridSelection.current?.current?.cell[1])
76✔
3394
        ) {
205!
3395
            scrollToRef.current(outCol, outRow);
×
UNCOV
3396
        }
×
3397
        hasJustScrolled.current = false; //only allow skipping a single scroll
205✔
3398
    }, [outCol, outRow]);
649✔
3399

649✔
3400
    const selectionOutOfBounds =
649✔
3401
        gridSelection.current !== undefined &&
649✔
3402
        (gridSelection.current.cell[0] >= mangledCols.length || gridSelection.current.cell[1] >= mangledRows);
301✔
3403
    React.useLayoutEffect(() => {
649✔
3404
        if (selectionOutOfBounds) {
135✔
3405
            setGridSelection(emptyGridSelection, false);
1✔
3406
        }
1✔
3407
    }, [selectionOutOfBounds, setGridSelection]);
649✔
3408

649✔
3409
    const disabledRows = React.useMemo(() => {
649✔
3410
        if (showTrailingBlankRow === true && trailingRowOptions?.tint === true) {
134✔
3411
            return CompactSelection.fromSingleSelection(mangledRows - 1);
131✔
3412
        }
131✔
3413
        return CompactSelection.empty();
3✔
3414
    }, [mangledRows, showTrailingBlankRow, trailingRowOptions?.tint]);
649✔
3415

649✔
3416
    const mangledVerticalBorder = React.useCallback(
649✔
3417
        (col: number) => {
649✔
3418
            return typeof verticalBorder === "boolean"
7,126!
UNCOV
3419
                ? verticalBorder
×
3420
                : verticalBorder?.(col - rowMarkerOffset) ?? true;
7,126!
3421
        },
7,126✔
3422
        [rowMarkerOffset, verticalBorder]
649✔
3423
    );
649✔
3424

649✔
3425
    const renameGroupNode = React.useMemo(() => {
649✔
3426
        if (renameGroup === undefined || canvasRef.current === null) return null;
135✔
3427
        const { bounds, group } = renameGroup;
2✔
3428
        const canvasBounds = canvasRef.current.getBoundingClientRect();
2✔
3429
        return (
2✔
3430
            <GroupRename
2✔
3431
                bounds={bounds}
2✔
3432
                group={group}
2✔
3433
                canvasBounds={canvasBounds}
2✔
3434
                onClose={() => setRenameGroup(undefined)}
2✔
3435
                onFinish={newVal => {
2✔
3436
                    setRenameGroup(undefined);
1✔
3437
                    onGroupHeaderRenamed?.(group, newVal);
1✔
3438
                }}
1✔
3439
            />
2✔
3440
        );
135✔
3441
    }, [onGroupHeaderRenamed, renameGroup]);
649✔
3442

649✔
3443
    const mangledFreezeColumns = Math.min(mangledCols.length, freezeColumns + (hasRowMarkers ? 1 : 0));
649✔
3444

649✔
3445
    React.useImperativeHandle(
649✔
3446
        forwardedRef,
649✔
3447
        () => ({
649✔
3448
            appendRow: (col: number, openOverlay?: boolean) => appendRow(col + rowMarkerOffset, openOverlay),
26✔
3449
            updateCells: damageList => {
26✔
3450
                if (rowMarkerOffset !== 0) {
2✔
3451
                    damageList = damageList.map(x => ({ cell: [x.cell[0] + rowMarkerOffset, x.cell[1]] }));
1✔
3452
                }
1✔
3453
                return gridRef.current?.damage(damageList);
2✔
3454
            },
2✔
3455
            getBounds: (col, row) => {
26✔
UNCOV
3456
                if (canvasRef?.current === null || scrollRef?.current === null) {
×
NEW
3457
                    return undefined;
×
UNCOV
3458
                }
×
UNCOV
3459

×
UNCOV
3460
                if (col === undefined && row === undefined) {
×
UNCOV
3461
                    // Return the bounds of the entire scroll area:
×
NEW
3462
                    const rect = canvasRef.current.getBoundingClientRect();
×
NEW
3463
                    const scale = rect.width / scrollRef.current.clientWidth;
×
NEW
3464
                    return {
×
NEW
3465
                        x: rect.x - scrollRef.current.scrollLeft * scale,
×
NEW
3466
                        y: rect.y - scrollRef.current.scrollTop * scale,
×
NEW
3467
                        width: scrollRef.current.scrollWidth * scale,
×
NEW
3468
                        height: scrollRef.current.scrollHeight * scale,
×
NEW
3469
                    };
×
3470
                }
×
NEW
3471
                return gridRef.current?.getBounds(col ?? 0 + rowMarkerOffset, row);
×
UNCOV
3472
            },
×
3473
            focus: () => gridRef.current?.focus(),
26✔
3474
            emit: async e => {
26✔
3475
                switch (e) {
5✔
3476
                    case "delete":
5✔
3477
                        onKeyDown({
1✔
3478
                            bounds: undefined,
1✔
3479
                            cancel: () => undefined,
1✔
3480
                            stopPropagation: () => undefined,
1✔
3481
                            preventDefault: () => undefined,
1✔
3482
                            ctrlKey: false,
1✔
3483
                            key: "Delete",
1✔
3484
                            keyCode: 46,
1✔
3485
                            metaKey: false,
1✔
3486
                            shiftKey: false,
1✔
3487
                            altKey: false,
1✔
3488
                            rawEvent: undefined,
1✔
3489
                            location: undefined,
1✔
3490
                        });
1✔
3491
                        break;
1✔
3492
                    case "fill-right":
5✔
3493
                        onKeyDown({
1✔
3494
                            bounds: undefined,
1✔
3495
                            cancel: () => undefined,
1✔
3496
                            stopPropagation: () => undefined,
1✔
3497
                            preventDefault: () => undefined,
1✔
3498
                            ctrlKey: true,
1✔
3499
                            key: "r",
1✔
3500
                            keyCode: 82,
1✔
3501
                            metaKey: false,
1✔
3502
                            shiftKey: false,
1✔
3503
                            altKey: false,
1✔
3504
                            rawEvent: undefined,
1✔
3505
                            location: undefined,
1✔
3506
                        });
1✔
3507
                        break;
1✔
3508
                    case "fill-down":
5✔
3509
                        onKeyDown({
1✔
3510
                            bounds: undefined,
1✔
3511
                            cancel: () => undefined,
1✔
3512
                            stopPropagation: () => undefined,
1✔
3513
                            preventDefault: () => undefined,
1✔
3514
                            ctrlKey: true,
1✔
3515
                            key: "d",
1✔
3516
                            keyCode: 68,
1✔
3517
                            metaKey: false,
1✔
3518
                            shiftKey: false,
1✔
3519
                            altKey: false,
1✔
3520
                            rawEvent: undefined,
1✔
3521
                            location: undefined,
1✔
3522
                        });
1✔
3523
                        break;
1✔
3524
                    case "copy":
5✔
3525
                        await onCopy(undefined, true);
1✔
3526
                        break;
1✔
3527
                    case "paste":
5✔
3528
                        await onPasteInternal();
1✔
3529
                        break;
1✔
3530
                }
5✔
3531
            },
5✔
3532
            scrollTo,
26✔
3533
            remeasureColumns: cols => {
26✔
3534
                for (const col of cols) {
1✔
3535
                    void normalSizeColumn(col + rowMarkerOffset, true);
1✔
3536
                }
1✔
3537
            },
1✔
3538
        }),
26✔
3539
        [appendRow, normalSizeColumn, onCopy, onKeyDown, onPasteInternal, rowMarkerOffset, scrollTo]
649✔
3540
    );
649✔
3541

649✔
3542
    const [selCol, selRow] = currentCell ?? [];
649✔
3543
    const onCellFocused = React.useCallback(
649✔
3544
        (cell: Item) => {
649✔
3545
            const [col, row] = cell;
24✔
3546

24✔
3547
            if (row === -1) {
24!
UNCOV
3548
                if (columnSelect !== "none") {
×
3549
                    setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, false);
×
3550
                    focus();
×
UNCOV
3551
                }
×
3552
                return;
×
UNCOV
3553
            }
×
3554

24✔
3555
            if (selCol === col && selRow === row) return;
24✔
3556
            setCurrent(
1✔
3557
                {
1✔
3558
                    cell,
1✔
3559
                    range: { x: col, y: row, width: 1, height: 1 },
1✔
3560
                },
1✔
3561
                true,
1✔
3562
                false,
1✔
3563
                "keyboard-nav"
1✔
3564
            );
1✔
3565
            scrollTo(col, row);
1✔
3566
        },
24✔
3567
        [columnSelect, focus, scrollTo, selCol, selRow, setCurrent, setSelectedColumns]
649✔
3568
    );
649✔
3569

649✔
3570
    const [isFocused, setIsFocused] = React.useState(false);
649✔
3571
    const setIsFocusedDebounced = React.useRef(
649✔
3572
        debounce((val: boolean) => {
649✔
3573
            setIsFocused(val);
46✔
3574
        }, 5)
649✔
3575
    );
649✔
3576

649✔
3577
    const onCanvasFocused = React.useCallback(() => {
649✔
3578
        setIsFocusedDebounced.current(true);
57✔
3579

57✔
3580
        // check for mouse state, don't do anything if the user is clicked to focus.
57✔
3581
        if (
57✔
3582
            gridSelection.current === undefined &&
57✔
3583
            gridSelection.columns.length === 0 &&
6✔
3584
            gridSelection.rows.length === 0 &&
5✔
3585
            mouseState === undefined
5✔
3586
        ) {
57✔
3587
            setCurrent(
5✔
3588
                {
5✔
3589
                    cell: [rowMarkerOffset, cellYOffset],
5✔
3590
                    range: {
5✔
3591
                        x: rowMarkerOffset,
5✔
3592
                        y: cellYOffset,
5✔
3593
                        width: 1,
5✔
3594
                        height: 1,
5✔
3595
                    },
5✔
3596
                },
5✔
3597
                true,
5✔
3598
                false,
5✔
3599
                "keyboard-select"
5✔
3600
            );
5✔
3601
        }
5✔
3602
    }, [cellYOffset, gridSelection, mouseState, rowMarkerOffset, setCurrent]);
649✔
3603

649✔
3604
    const onFocusOut = React.useCallback(() => {
649✔
3605
        setIsFocusedDebounced.current(false);
27✔
3606
    }, []);
649✔
3607

649✔
3608
    const [idealWidth, idealHeight] = React.useMemo(() => {
649✔
3609
        let h: number;
147✔
3610
        const scrollbarWidth = experimental?.scrollbarWidthOverride ?? getScrollBarWidth();
147✔
3611
        const rowsCountWithTrailingRow = rows + (showTrailingBlankRow ? 1 : 0);
147!
3612
        if (typeof rowHeight === "number") {
147✔
3613
            h = totalHeaderHeight + rowsCountWithTrailingRow * rowHeight;
146✔
3614
        } else {
147✔
3615
            let avg = 0;
1✔
3616
            const toAverage = Math.min(rowsCountWithTrailingRow, 10);
1✔
3617
            for (let i = 0; i < toAverage; i++) {
1✔
3618
                avg += rowHeight(i);
10✔
3619
            }
10✔
3620
            avg = Math.floor(avg / toAverage);
1✔
3621

1✔
3622
            h = totalHeaderHeight + rowsCountWithTrailingRow * avg;
1✔
3623
        }
1✔
3624
        h += scrollbarWidth;
147✔
3625

147✔
3626
        const w = mangledCols.reduce((acc, x) => x.width + acc, 0) + scrollbarWidth;
147✔
3627

147✔
3628
        // We need to set a reasonable cap here as some browsers will just ignore huge values
147✔
3629
        // rather than treat them as huge values.
147✔
3630
        return [`${Math.min(100_000, w)}px`, `${Math.min(100_000, h)}px`];
147✔
3631
    }, [mangledCols, experimental?.scrollbarWidthOverride, rowHeight, rows, showTrailingBlankRow, totalHeaderHeight]);
649✔
3632

649✔
3633
    return (
649✔
3634
        <ThemeContext.Provider value={mergedTheme}>
649✔
3635
            <DataEditorContainer
649✔
3636
                style={makeCSSStyle(mergedTheme)}
649✔
3637
                className={className}
649✔
3638
                inWidth={width ?? idealWidth}
649✔
3639
                inHeight={height ?? idealHeight}>
649✔
3640
                <DataGridSearch
649✔
3641
                    fillHandle={fillHandle}
649✔
3642
                    drawFocusRing={drawFocusRing}
649✔
3643
                    experimental={experimental}
649✔
3644
                    fixedShadowX={fixedShadowX}
649✔
3645
                    fixedShadowY={fixedShadowY}
649✔
3646
                    getRowThemeOverride={getRowThemeOverride}
649✔
3647
                    headerIcons={headerIcons}
649✔
3648
                    imageWindowLoader={imageWindowLoader}
649✔
3649
                    initialSize={initialSize}
649✔
3650
                    isDraggable={isDraggable}
649✔
3651
                    onDragLeave={onDragLeave}
649✔
3652
                    onRowMoved={onRowMoved}
649✔
3653
                    overscrollX={overscrollX}
649✔
3654
                    overscrollY={overscrollY}
649✔
3655
                    preventDiagonalScrolling={preventDiagonalScrolling}
649✔
3656
                    rightElement={rightElement}
649✔
3657
                    rightElementProps={rightElementProps}
649✔
3658
                    showMinimap={showMinimap}
649✔
3659
                    smoothScrollX={smoothScrollX}
649✔
3660
                    smoothScrollY={smoothScrollY}
649✔
3661
                    className={className}
649✔
3662
                    enableGroups={enableGroups}
649✔
3663
                    onCanvasFocused={onCanvasFocused}
649✔
3664
                    onCanvasBlur={onFocusOut}
649✔
3665
                    canvasRef={canvasRef}
649✔
3666
                    onContextMenu={onContextMenu}
649✔
3667
                    theme={mergedTheme}
649✔
3668
                    cellXOffset={cellXOffset}
649✔
3669
                    cellYOffset={cellYOffset}
649✔
3670
                    accessibilityHeight={visibleRegion.height}
649✔
3671
                    onDragEnd={onDragEnd}
649✔
3672
                    columns={mangledCols}
649✔
3673
                    drawCustomCell={drawCell}
649✔
3674
                    drawHeader={drawHeader}
649✔
3675
                    disabledRows={disabledRows}
649✔
3676
                    freezeColumns={mangledFreezeColumns}
649✔
3677
                    lockColumns={rowMarkerOffset}
649✔
3678
                    firstColAccessible={rowMarkerOffset === 0}
649✔
3679
                    getCellContent={getMangledCellContent}
649✔
3680
                    minColumnWidth={minColumnWidth}
649✔
3681
                    maxColumnWidth={maxColumnWidth}
649✔
3682
                    searchInputRef={searchInputRef}
649✔
3683
                    showSearch={showSearch}
649✔
3684
                    onSearchClose={onSearchClose}
649✔
3685
                    highlightRegions={highlightRegions}
649✔
3686
                    getCellsForSelection={getCellsForSelection}
649✔
3687
                    getGroupDetails={mangledGetGroupDetails}
649✔
3688
                    headerHeight={headerHeight}
649✔
3689
                    isFocused={isFocused}
649✔
3690
                    groupHeaderHeight={enableGroups ? groupHeaderHeight : 0}
649✔
3691
                    trailingRowType={
649✔
3692
                        !showTrailingBlankRow ? "none" : trailingRowOptions?.sticky === true ? "sticky" : "appended"
649!
3693
                    }
649✔
3694
                    onColumnResize={onColumnResize}
649✔
3695
                    onColumnResizeEnd={onColumnResizeEnd}
649✔
3696
                    onColumnResizeStart={onColumnResizeStart}
649✔
3697
                    onCellFocused={onCellFocused}
649✔
3698
                    onColumnMoved={onColumnMovedImpl}
649✔
3699
                    onDragStart={onDragStartImpl}
649✔
3700
                    onHeaderMenuClick={onHeaderMenuClickInner}
649✔
3701
                    onItemHovered={onItemHoveredImpl}
649✔
3702
                    isFilling={mouseState?.fillHandle === true}
649✔
3703
                    onMouseMove={onMouseMoveImpl}
649✔
3704
                    onKeyDown={onKeyDown}
649✔
3705
                    onKeyUp={onKeyUpIn}
649✔
3706
                    onMouseDown={onMouseDown}
649✔
3707
                    onMouseUp={onMouseUp}
649✔
3708
                    onDragOverCell={onDragOverCell}
649✔
3709
                    onDrop={onDrop}
649✔
3710
                    onSearchResultsChanged={onSearchResultsChanged}
649✔
3711
                    onVisibleRegionChanged={onVisibleRegionChangedImpl}
649✔
3712
                    clientSize={[clientSize[0], clientSize[1]]}
649✔
3713
                    rowHeight={rowHeight}
649✔
3714
                    searchResults={searchResults}
649✔
3715
                    searchValue={searchValue}
649✔
3716
                    onSearchValueChange={onSearchValueChange}
649✔
3717
                    rows={mangledRows}
649✔
3718
                    scrollRef={scrollRef}
649✔
3719
                    selection={gridSelection}
649✔
3720
                    translateX={visibleRegion.tx}
649✔
3721
                    translateY={visibleRegion.ty}
649✔
3722
                    verticalBorder={mangledVerticalBorder}
649✔
3723
                    gridRef={gridRef}
649✔
3724
                    getCellRenderer={getCellRenderer}
649✔
3725
                    scrollToEnd={scrollToEnd}
649✔
3726
                />
649✔
3727
                {renameGroupNode}
649✔
3728
                {overlay !== undefined && (
649✔
3729
                    <DataGridOverlayEditor
24✔
3730
                        {...overlay}
24✔
3731
                        validateCell={validateCell}
24✔
3732
                        id={overlayID}
24✔
3733
                        getCellRenderer={getCellRenderer}
24✔
3734
                        className={experimental?.isSubGrid === true ? "click-outside-ignore" : undefined}
24!
3735
                        provideEditor={provideEditor}
24✔
3736
                        imageEditorOverride={imageEditorOverride}
24✔
3737
                        onFinishEditing={onFinishEditing}
24✔
3738
                        markdownDivCreateNode={markdownDivCreateNode}
24✔
3739
                        isOutsideClick={isOutsideClick}
24✔
3740
                    />
24✔
3741
                )}
649✔
3742
            </DataEditorContainer>
649✔
3743
        </ThemeContext.Provider>
649✔
3744
    );
649✔
3745
};
649✔
3746

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

© 2026 Coveralls, Inc