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

glideapps / glide-data-grid / 17300808884

28 Aug 2025 03:39PM UTC coverage: 90.842% (-0.003%) from 90.845%
17300808884

Pull #1114

github

lukasmasuch
Fix issue with dragging on row will cause the cell rect to resize
Pull Request #1114: Optimize additive selection mode

2971 of 3682 branches covered (80.69%)

23 of 26 new or added lines in 2 files covered. (88.46%)

1 existing line in 1 file now uncovered.

18163 of 19994 relevant lines covered (90.84%)

3164.3 hits per line

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

89.76
/packages/core/src/data-editor/data-editor.tsx
1
/* eslint-disable sonarjs/no-duplicate-string */
1✔
2
import * as React from "react";
1✔
3
import { assert, assertNever, maybe } from "../common/support.js";
1✔
4
import clamp from "lodash/clamp.js";
1✔
5
import uniq from "lodash/uniq.js";
1✔
6
import flatten from "lodash/flatten.js";
1✔
7
import range from "lodash/range.js";
1✔
8
import debounce from "lodash/debounce.js";
1✔
9
import {
1✔
10
    type EditableGridCell,
1✔
11
    type GridCell,
1✔
12
    GridCellKind,
1✔
13
    type GridSelection,
1✔
14
    isEditableGridCell,
1✔
15
    type Rectangle,
1✔
16
    isReadWriteCell,
1✔
17
    type InnerGridCell,
1✔
18
    InnerGridCellKind,
1✔
19
    CompactSelection,
1✔
20
    type Slice,
1✔
21
    isInnerOnlyCell,
1✔
22
    type ProvideEditorCallback,
1✔
23
    type GridColumn,
1✔
24
    isObjectEditorCallbackResult,
1✔
25
    type Item,
1✔
26
    type MarkerCell,
1✔
27
    type ValidatedGridCell,
1✔
28
    type ImageEditorType,
1✔
29
    type CustomCell,
1✔
30
    BooleanEmpty,
1✔
31
    BooleanIndeterminate,
1✔
32
    type FillHandleDirection,
1✔
33
    type EditListItem,
1✔
34
    type CellActivationBehavior,
1✔
35
} from "../internal/data-grid/data-grid-types.js";
1✔
36
import DataGridSearch, { type DataGridSearchProps } from "../internal/data-grid-search/data-grid-search.js";
1✔
37
import { browserIsOSX } from "../common/browser-detect.js";
1✔
38
import {
1✔
39
    getDataEditorTheme,
1✔
40
    makeCSSStyle,
1✔
41
    type FullTheme,
1✔
42
    type Theme,
1✔
43
    ThemeContext,
1✔
44
    mergeAndRealizeTheme,
1✔
45
} from "../common/styles.js";
1✔
46
import type { DataGridRef } from "../internal/data-grid/data-grid.js";
1✔
47
import { getScrollBarWidth, useEventListener, whenDefined } from "../common/utils.js";
1✔
48
import {
1✔
49
    isGroupEqual,
1✔
50
    itemsAreEqual,
1✔
51
    itemIsInRect,
1✔
52
    gridSelectionHasItem,
1✔
53
    getFreezeTrailingHeight,
1✔
54
} from "../internal/data-grid/render/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, toggleBoolean } from "./data-editor-fns.js";
1✔
61
import { DataEditorContainer } from "../internal/data-editor-container/data-grid-container.js";
1✔
62
import { useAutoscroll } from "./use-autoscroll.js";
1✔
63
import type { CustomRenderer, CellRenderer, InternalCellRenderer } from "../cells/cell-types.js";
1✔
64
import { decodeHTML, type CopyBuffer } from "./copy-paste.js";
1✔
65
import { useRemAdjuster } from "./use-rem-adjuster.js";
1✔
66
import { withAlpha } from "../internal/data-grid/color-parser.js";
1✔
67
import { combineRects, getClosestRect, pointInRect } from "../common/math.js";
1✔
68
import {
1✔
69
    type HeaderClickedEventArgs,
1✔
70
    type GroupHeaderClickedEventArgs,
1✔
71
    type CellClickedEventArgs,
1✔
72
    type FillPatternEventArgs,
1✔
73
    type GridMouseEventArgs,
1✔
74
    groupHeaderKind,
1✔
75
    outOfBoundsKind,
1✔
76
    type GridMouseCellEventArgs,
1✔
77
    headerKind,
1✔
78
    type GridDragEventArgs,
1✔
79
    mouseEventArgsAreEqual,
1✔
80
    type GridKeyEventArgs,
1✔
81
    type CellActivatedEventArgs,
1✔
82
} from "../internal/data-grid/event-args.js";
1✔
83
import { type Keybinds, useKeybindingsWithDefaults } from "./data-editor-keybindings.js";
1✔
84
import type { Highlight } from "../internal/data-grid/render/data-grid-render.cells.js";
1✔
85
import { useRowGroupingInner, type RowGroupingOptions } from "./row-grouping.js";
1✔
86
import { useRowGrouping } from "./row-grouping-api.js";
1✔
87
import { useInitialScrollOffset } from "./use-initial-scroll-offset.js";
1✔
88
import type { VisibleRegion } from "./visible-region.js";
1✔
89

1✔
90
const DataGridOverlayEditor = React.lazy(
1✔
91
    async () => await import("../internal/data-grid-overlay-editor/data-grid-overlay-editor.js")
1✔
92
);
1✔
93

1✔
94
// There must be a better way
1✔
95
let idCounter = 0;
1✔
96

1✔
97
export interface RowMarkerOptions {
1✔
98
    kind: "checkbox" | "number" | "clickable-number" | "checkbox-visible" | "both" | "none";
1✔
99
    checkboxStyle?: "circle" | "square";
1✔
100
    startIndex?: number;
1✔
101
    width?: number;
1✔
102
    theme?: Partial<Theme>;
1✔
103
    headerTheme?: Partial<Theme>;
1✔
104
    headerAlwaysVisible?: boolean;
1✔
105
    headerDisabled?: boolean;
1✔
106
}
1✔
107

1✔
108
interface MouseState {
1✔
109
    readonly previousSelection?: GridSelection;
1✔
110
    readonly fillHandle?: boolean;
1✔
111
}
1✔
112

1✔
113
type Props = Partial<
1✔
114
    Omit<
1✔
115
        DataGridSearchProps,
1✔
116
        | "accessibilityHeight"
1✔
117
        | "canvasRef"
1✔
118
        | "cellXOffset"
1✔
119
        | "cellYOffset"
1✔
120
        | "className"
1✔
121
        | "clientSize"
1✔
122
        | "columns"
1✔
123
        | "disabledRows"
1✔
124
        | "drawFocusRing"
1✔
125
        | "enableGroups"
1✔
126
        | "firstColAccessible"
1✔
127
        | "firstColSticky"
1✔
128
        | "freezeColumns"
1✔
129
        | "hasAppendRow"
1✔
130
        | "getCellContent"
1✔
131
        | "getCellRenderer"
1✔
132
        | "getCellsForSelection"
1✔
133
        | "getRowThemeOverride"
1✔
134
        | "gridRef"
1✔
135
        | "groupHeaderHeight"
1✔
136
        | "headerHeight"
1✔
137
        | "isFilling"
1✔
138
        | "isFocused"
1✔
139
        | "imageWindowLoader"
1✔
140
        | "lockColumns"
1✔
141
        | "maxColumnWidth"
1✔
142
        | "minColumnWidth"
1✔
143
        | "nonGrowWidth"
1✔
144
        | "onCanvasBlur"
1✔
145
        | "onCanvasFocused"
1✔
146
        | "onCellFocused"
1✔
147
        | "onContextMenu"
1✔
148
        | "onDragEnd"
1✔
149
        | "onMouseDown"
1✔
150
        | "onMouseMove"
1✔
151
        | "onMouseUp"
1✔
152
        | "onVisibleRegionChanged"
1✔
153
        | "rowHeight"
1✔
154
        | "rows"
1✔
155
        | "scrollRef"
1✔
156
        | "searchInputRef"
1✔
157
        | "selectedColumns"
1✔
158
        | "selection"
1✔
159
        | "theme"
1✔
160
        | "translateX"
1✔
161
        | "translateY"
1✔
162
        | "verticalBorder"
1✔
163
    >
1✔
164
>;
1✔
165

1✔
166
type EmitEvents = "copy" | "paste" | "delete" | "fill-right" | "fill-down";
1✔
167

1✔
168
function getSpanStops(cells: readonly (readonly GridCell[])[]): number[] {
5✔
169
    return uniq(
5✔
170
        flatten(
5✔
171
            flatten(cells)
5✔
172
                .filter(c => c.span !== undefined)
5✔
173
                .map(c => range((c.span?.[0] ?? 0) + 1, (c.span?.[1] ?? 0) + 1))
5✔
174
        )
5✔
175
    );
5✔
176
}
5✔
177

1✔
178
function shiftSelection(input: GridSelection, offset: number): GridSelection {
331✔
179
    if (input === undefined || offset === 0 || (input.columns.length === 0 && input.current === undefined))
331✔
180
        return input;
331✔
181

48✔
182
    return {
48✔
183
        current:
48✔
184
            input.current === undefined
48✔
185
                ? undefined
1✔
186
                : {
47✔
187
                      cell: [input.current.cell[0] + offset, input.current.cell[1]],
47✔
188
                      range: {
47✔
189
                          ...input.current.range,
47✔
190
                          x: input.current.range.x + offset,
47✔
191
                      },
47✔
192
                      rangeStack: input.current.rangeStack.map(r => ({
47✔
193
                          ...r,
×
194
                          x: r.x + offset,
×
195
                      })),
47✔
196
                  },
47✔
197
        rows: input.rows,
331✔
198
        columns: input.columns.offset(offset),
331✔
199
    };
331✔
200
}
331✔
201

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

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

1✔
283
    /** The columns to display in the data grid.
1✔
284
     * @group Data
1✔
285
     */
1✔
286
    readonly columns: readonly GridColumn[];
1✔
287

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

1✔
314
    /**
1✔
315
     * The number of rows in the grid.
1✔
316
     * @group Data
1✔
317
     */
1✔
318
    readonly rows: number;
1✔
319

1✔
320
    /** Determines if row markers should be automatically added to the grid.
1✔
321
     * Interactive row markers allow the user to select a row.
1✔
322
     *
1✔
323
     * - "clickable-number" renders a number that can be clicked to
1✔
324
     *   select the row
1✔
325
     * - "both" causes the row marker to show up as a number but
1✔
326
     *   reveal a checkbox when the marker is hovered.
1✔
327
     *
1✔
328
     * @defaultValue `none`
1✔
329
     * @group Style
1✔
330
     */
1✔
331
    readonly rowMarkers?: RowMarkerOptions["kind"] | RowMarkerOptions;
1✔
332
    /**
1✔
333
     * Sets the width of row markers in pixels, if unset row markers will automatically size.
1✔
334
     * @group Style
1✔
335
     * @deprecated Use `rowMarkers` instead.
1✔
336
     */
1✔
337
    readonly rowMarkerWidth?: number;
1✔
338
    /** Changes the starting index for row markers.
1✔
339
     * @defaultValue 1
1✔
340
     * @group Style
1✔
341
     * @deprecated Use `rowMarkers` instead.
1✔
342
     */
1✔
343
    readonly rowMarkerStartIndex?: number;
1✔
344

1✔
345
    /** Changes the theme of the row marker column
1✔
346
     * @group Style
1✔
347
     * @deprecated Use `rowMarkers` instead.
1✔
348
     */
1✔
349
    readonly rowMarkerTheme?: Partial<Theme>;
1✔
350

1✔
351
    /** Sets the width of the data grid.
1✔
352
     * @group Style
1✔
353
     */
1✔
354
    readonly width?: number | string;
1✔
355
    /** Sets the height of the data grid.
1✔
356
     * @group Style
1✔
357
     */
1✔
358
    readonly height?: number | string;
1✔
359
    /** Custom classname for data grid wrapper.
1✔
360
     * @group Style
1✔
361
     */
1✔
362
    readonly className?: string;
1✔
363

1✔
364
    /** If set to `default`, `gridSelection` will be coerced to always include full spans.
1✔
365
     * @group Selection
1✔
366
     * @defaultValue `default`
1✔
367
     */
1✔
368
    readonly spanRangeBehavior?: "default" | "allowPartial";
1✔
369

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

1✔
405
    /** Controls if range selection is allowed to span columns.
1✔
406
     * @group Selection
1✔
407
     * @defaultValue `true`
1✔
408
     */
1✔
409
    readonly rangeSelectionColumnSpanning?: boolean;
1✔
410

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

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

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

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

1✔
466
    /**
1✔
467
     * Allows overriding the theme of any row
1✔
468
     * @param row represents the row index of the row, increasing by 1 for every represented row. Collapsed rows are not included.
1✔
469
     * @param groupRow represents the row index of the group row. Only distinct when row grouping enabled.
1✔
470
     * @param contentRow represents the index of the row excluding group headers. Only distinct when row grouping enabled.
1✔
471
     * @returns
1✔
472
     */
1✔
473
    readonly getRowThemeOverride?: (row: number, groupRow: number, contentRow: number) => Partial<Theme> | undefined;
1✔
474

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

1✔
488
    /**
1✔
489
     * Emitted when the grid selection is cleared.
1✔
490
     * @group Selection
1✔
491
     */
1✔
492
    readonly onSelectionCleared?: () => void;
1✔
493

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

1✔
533
            /**
1✔
534
             * All visible freeze regions
1✔
535
             */
1✔
536
            freezeRegions?: readonly Rectangle[];
1✔
537
        }
1✔
538
    ) => void;
1✔
539

1✔
540
    /**
1✔
541
     * The primary callback for getting cell data into the data grid.
1✔
542
     * @group Data
1✔
543
     * @param cell The location of the cell being requested.
1✔
544
     * @returns A valid GridCell to be rendered by the Grid.
1✔
545
     */
1✔
546
    readonly getCellContent: (cell: Item) => GridCell;
1✔
547

1✔
548
    /**
1✔
549
     * Determines if row selection requires a modifier key to enable multi-selection or not. In auto mode it adapts to
1✔
550
     * touch or mouse environments automatically, in multi-mode it always acts as if the multi key (Ctrl) is pressed.
1✔
551
     * @group Editing
1✔
552
     * @defaultValue `auto`
1✔
553
     */
1✔
554
    readonly rowSelectionMode?: "auto" | "multi";
1✔
555

1✔
556
    /**
1✔
557
     * Determines if column selection requires a modifier key to enable multi-selection or not. In auto mode it adapts to
1✔
558
     * touch or mouse environments automatically, in multi-mode it always acts as if the multi key (Ctrl) is pressed.
1✔
559
     * @group Editing
1✔
560
     * @defaultValue `auto`
1✔
561
     */
1✔
562
    readonly columnSelectionMode?: "auto" | "multi";
1✔
563

1✔
564
    /**
1✔
565
     * Add table headers to copied data.
1✔
566
     * @group Editing
1✔
567
     * @defaultValue `false`
1✔
568
     */
1✔
569
    readonly copyHeaders?: boolean;
1✔
570

1✔
571
    /**
1✔
572
     * Determins which keybindings are enabled.
1✔
573
     * @group Editing
1✔
574
     */
1✔
575
    readonly keybindings?: Partial<Keybinds>;
1✔
576

1✔
577
    /**
1✔
578
     * Determines if the data editor should immediately begin editing when the user types on a selected cell
1✔
579
     * @group Editing
1✔
580
     */
1✔
581
    readonly editOnType?: boolean;
1✔
582

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

1✔
606
    /** The number of columns which should remain in place when scrolling horizontally. The row marker column, if
1✔
607
     * enabled is always frozen and is not included in this count.
1✔
608
     * @defaultValue 0
1✔
609
     * @group Style
1✔
610
     */
1✔
611
    readonly freezeColumns?: DataGridSearchProps["freezeColumns"];
1✔
612

1✔
613
    /**
1✔
614
     * Controls the drawing of the left hand vertical border of a column. If set to a boolean value it controls all
1✔
615
     * borders.
1✔
616
     * @defaultValue `true`
1✔
617
     * @group Style
1✔
618
     */
1✔
619
    readonly verticalBorder?: DataGridSearchProps["verticalBorder"] | boolean;
1✔
620

1✔
621
    /**
1✔
622
     * Controls the grouping of rows to be drawn in the grid.
1✔
623
     */
1✔
624
    readonly rowGrouping?: RowGroupingOptions;
1✔
625

1✔
626
    /**
1✔
627
     * Called when data is pasted into the grid. If left undefined, the `DataEditor` will operate in a
1✔
628
     * fallback mode and attempt to paste the text buffer into the current cell assuming the current cell is not
1✔
629
     * readonly and can accept the data type. If `onPaste` is set to false or the function returns false, the grid will
1✔
630
     * simply ignore paste. If `onPaste` evaluates to true the grid will attempt to split the data by tabs and newlines
1✔
631
     * and paste into available cells.
1✔
632
     *
1✔
633
     * The grid will not attempt to add additional rows if more data is pasted then can fit. In that case it is
1✔
634
     * advisable to simply return false from onPaste and handle the paste manually.
1✔
635
     * @group Editing
1✔
636
     */
1✔
637
    readonly onPaste?: ((target: Item, values: readonly (readonly string[])[]) => boolean) | boolean;
1✔
638

1✔
639
    /**
1✔
640
     * The theme used by the data grid to get all color and font information
1✔
641
     * @group Style
1✔
642
     */
1✔
643
    readonly theme?: Partial<Theme>;
1✔
644

1✔
645
    readonly renderers?: readonly InternalCellRenderer<InnerGridCell>[];
1✔
646

1✔
647
    /**
1✔
648
     * An array of custom renderers which can be used to extend the data grid.
1✔
649
     * @group Advanced
1✔
650
     */
1✔
651
    readonly customRenderers?: readonly CustomRenderer<any>[];
1✔
652

1✔
653
    /**
1✔
654
     * Scales most elements in the theme to match rem scaling automatically
1✔
655
     * @defaultValue false
1✔
656
     */
1✔
657
    readonly scaleToRem?: boolean;
1✔
658

1✔
659
    /**
1✔
660
     * Custom predicate function to decide whether the click event occurred outside the grid
1✔
661
     * Especially used when custom editor is opened with the portal and is outside the grid, but there is no possibility
1✔
662
     * to add a class "click-outside-ignore"
1✔
663
     * If this function is supplied and returns false, the click event is ignored
1✔
664
     */
1✔
665
    readonly isOutsideClick?: (e: MouseEvent | TouchEvent) => boolean;
1✔
666

1✔
667
    /**
1✔
668
     * Controls which directions fill is allowed in.
1✔
669
     */
1✔
670
    readonly allowedFillDirections?: FillHandleDirection;
1✔
671

1✔
672
    /**
1✔
673
     * Determines when a cell is considered activated and will emit the `onCellActivated` event. Generally an activated
1✔
674
     * cell will open to edit mode.
1✔
675
     */
1✔
676
    readonly cellActivationBehavior?: CellActivationBehavior;
1✔
677

1✔
678
    /**
1✔
679
     * Controls if focus will trap inside the data grid when doing tab and caret navigation.
1✔
680
     */
1✔
681
    readonly trapFocus?: boolean;
1✔
682

1✔
683
    /**
1✔
684
     * Allows overriding the default amount of bloom (the size growth of the overlay editor)
1✔
685
     */
1✔
686
    readonly editorBloom?: readonly [number, number];
1✔
687

1✔
688
    /**
1✔
689
     * If set to true, the data grid will attempt to scroll to keep the selction in view
1✔
690
     */
1✔
691
    readonly scrollToActiveCell?: boolean;
1✔
692

1✔
693
    readonly drawFocusRing?: boolean | "no-editor";
1✔
694

1✔
695
    /**
1✔
696
     * Allows overriding the default portal element.
1✔
697
     */
1✔
698
    readonly portalElementRef?: React.RefObject<HTMLElement>;
1✔
699
}
1✔
700

1✔
701
type ScrollToFn = (
1✔
702
    col: number | { amount: number; unit: "cell" | "px" },
1✔
703
    row: number | { amount: number; unit: "cell" | "px" },
1✔
704
    dir?: "horizontal" | "vertical" | "both",
1✔
705
    paddingX?: number,
1✔
706
    paddingY?: number,
1✔
707
    options?: {
1✔
708
        hAlign?: "start" | "center" | "end";
1✔
709
        vAlign?: "start" | "center" | "end";
1✔
710
        behavior?: ScrollBehavior;
1✔
711
    }
1✔
712
) => void;
1✔
713

1✔
714
/** @category DataEditor */
1✔
715
export interface DataEditorRef {
1✔
716
    /**
1✔
717
     * Programatically appends a row.
1✔
718
     * @param col The column index to focus in the new row.
1✔
719
     * @returns A promise which waits for the append to complete.
1✔
720
     */
1✔
721
    appendRow: (col: number, openOverlay?: boolean, behavior?: ScrollBehavior) => Promise<void>;
1✔
722
    /**
1✔
723
     * Programatically appends a column.
1✔
724
     * @param row The row index to focus in the new column.
1✔
725
     * @returns A promise which waits for the append to complete.
1✔
726
     */
1✔
727
    appendColumn: (row: number, openOverlay?: boolean) => Promise<void>;
1✔
728
    /**
1✔
729
     * Triggers cells to redraw.
1✔
730
     */
1✔
731
    updateCells: DataGridRef["damage"];
1✔
732
    /**
1✔
733
     * Gets the screen space bounds of the requested item.
1✔
734
     */
1✔
735
    getBounds: DataGridRef["getBounds"];
1✔
736
    /**
1✔
737
     * Triggers the data grid to focus itself or the correct accessibility element.
1✔
738
     */
1✔
739
    focus: DataGridRef["focus"];
1✔
740
    /**
1✔
741
     * Generic API for emitting events as if they had been triggered via user interaction.
1✔
742
     */
1✔
743
    emit: (eventName: EmitEvents) => Promise<void>;
1✔
744
    /**
1✔
745
     * Scrolls to the desired cell or location in the grid.
1✔
746
     */
1✔
747
    scrollTo: ScrollToFn;
1✔
748
    /**
1✔
749
     * Causes the columns in the selection to have their natural size recomputed and re-emitted as a resize event.
1✔
750
     */
1✔
751
    remeasureColumns: (cols: CompactSelection) => void;
1✔
752
    /**
1✔
753
     * Gets the mouse args from pointer event position.
1✔
754
     */
1✔
755
    getMouseArgsForPosition: (
1✔
756
        posX: number,
1✔
757
        posY: number,
1✔
758
        ev?: MouseEvent | TouchEvent
1✔
759
    ) => GridMouseEventArgs | undefined;
1✔
760
}
1✔
761

1✔
762
const loadingCell: GridCell = {
1✔
763
    kind: GridCellKind.Loading,
1✔
764
    allowOverlay: false,
1✔
765
};
1✔
766

1✔
767
export const emptyGridSelection: GridSelection = {
1✔
768
    columns: CompactSelection.empty(),
1✔
769
    rows: CompactSelection.empty(),
1✔
770
    current: undefined,
1✔
771
};
1✔
772

1✔
773
const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorProps> = (p, forwardedRef) => {
1✔
774
    const [gridSelectionInner, setGridSelectionInner] = React.useState<GridSelection>(emptyGridSelection);
771✔
775

771✔
776
    const [overlay, setOverlay] = React.useState<{
771✔
777
        target: Rectangle;
771✔
778
        content: GridCell;
771✔
779
        theme: FullTheme;
771✔
780
        initialValue: string | undefined;
771✔
781
        cell: Item;
771✔
782
        highlight: boolean;
771✔
783
        forceEditMode: boolean;
771✔
784
        activation: CellActivatedEventArgs;
771✔
785
    }>();
771✔
786
    const searchInputRef = React.useRef<HTMLInputElement | null>(null);
771✔
787
    const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
771✔
788
    const [mouseState, setMouseState] = React.useState<MouseState>();
771✔
789
    const lastSent = React.useRef<[number, number]>();
771✔
790

771✔
791
    const safeWindow = typeof window === "undefined" ? null : window;
771!
792

771✔
793
    const {
771✔
794
        imageEditorOverride,
771✔
795
        getRowThemeOverride: getRowThemeOverrideIn,
771✔
796
        markdownDivCreateNode,
771✔
797
        width,
771✔
798
        height,
771✔
799
        columns: columnsIn,
771✔
800
        rows: rowsIn,
771✔
801
        getCellContent,
771✔
802
        onCellClicked,
771✔
803
        onCellActivated,
771✔
804
        onFillPattern,
771✔
805
        onFinishedEditing,
771✔
806
        coercePasteValue,
771✔
807
        drawHeader: drawHeaderIn,
771✔
808
        drawCell: drawCellIn,
771✔
809
        editorBloom,
771✔
810
        onHeaderClicked,
771✔
811
        onColumnProposeMove,
771✔
812
        rangeSelectionColumnSpanning = true,
771✔
813
        spanRangeBehavior = "default",
771✔
814
        onGroupHeaderClicked,
771✔
815
        onCellContextMenu,
771✔
816
        className,
771✔
817
        onHeaderContextMenu,
771✔
818
        getCellsForSelection: getCellsForSelectionIn,
771✔
819
        onGroupHeaderContextMenu,
771✔
820
        onGroupHeaderRenamed,
771✔
821
        onCellEdited,
771✔
822
        onCellsEdited,
771✔
823
        onSearchResultsChanged: onSearchResultsChangedIn,
771✔
824
        searchResults,
771✔
825
        onSearchValueChange,
771✔
826
        searchValue,
771✔
827
        onKeyDown: onKeyDownIn,
771✔
828
        onKeyUp: onKeyUpIn,
771✔
829
        keybindings: keybindingsIn,
771✔
830
        editOnType = true,
771✔
831
        onRowAppended,
771✔
832
        onColumnAppended,
771✔
833
        onColumnMoved,
771✔
834
        validateCell: validateCellIn,
771✔
835
        highlightRegions: highlightRegionsIn,
771✔
836
        rangeSelect = "rect",
771✔
837
        columnSelect = "multi",
771✔
838
        rowSelect = "multi",
771✔
839
        rangeSelectionBlending = "exclusive",
771✔
840
        columnSelectionBlending = "exclusive",
771✔
841
        rowSelectionBlending = "exclusive",
771✔
842
        onDelete: onDeleteIn,
771✔
843
        onDragStart,
771✔
844
        onMouseMove,
771✔
845
        onPaste,
771✔
846
        copyHeaders = false,
771✔
847
        freezeColumns = 0,
771✔
848
        cellActivationBehavior = "second-click",
771✔
849
        rowSelectionMode = "auto",
771✔
850
        columnSelectionMode = "auto",
771✔
851
        onHeaderMenuClick,
771✔
852
        onHeaderIndicatorClick,
771✔
853
        getGroupDetails,
771✔
854
        rowGrouping,
771✔
855
        onSearchClose: onSearchCloseIn,
771✔
856
        onItemHovered,
771✔
857
        onSelectionCleared,
771✔
858
        showSearch: showSearchIn,
771✔
859
        onVisibleRegionChanged,
771✔
860
        gridSelection: gridSelectionOuter,
771✔
861
        onGridSelectionChange,
771✔
862
        minColumnWidth: minColumnWidthIn = 50,
771✔
863
        maxColumnWidth: maxColumnWidthIn = 500,
771✔
864
        maxColumnAutoWidth: maxColumnAutoWidthIn,
771✔
865
        provideEditor,
771✔
866
        trailingRowOptions,
771✔
867
        freezeTrailingRows = 0,
771✔
868
        allowedFillDirections = "orthogonal",
771✔
869
        scrollOffsetX,
771✔
870
        scrollOffsetY,
771✔
871
        verticalBorder,
771✔
872
        onDragOverCell,
771✔
873
        onDrop,
771✔
874
        onColumnResize: onColumnResizeIn,
771✔
875
        onColumnResizeEnd: onColumnResizeEndIn,
771✔
876
        onColumnResizeStart: onColumnResizeStartIn,
771✔
877
        customRenderers: additionalRenderers,
771✔
878
        fillHandle,
771✔
879
        experimental,
771✔
880
        fixedShadowX,
771✔
881
        fixedShadowY,
771✔
882
        headerIcons,
771✔
883
        imageWindowLoader,
771✔
884
        initialSize,
771✔
885
        isDraggable,
771✔
886
        onDragLeave,
771✔
887
        onRowMoved,
771✔
888
        overscrollX: overscrollXIn,
771✔
889
        overscrollY: overscrollYIn,
771✔
890
        preventDiagonalScrolling,
771✔
891
        rightElement,
771✔
892
        rightElementProps,
771✔
893
        trapFocus = false,
771✔
894
        smoothScrollX,
771✔
895
        smoothScrollY,
771✔
896
        scaleToRem = false,
771✔
897
        rowHeight: rowHeightIn = 34,
771✔
898
        headerHeight: headerHeightIn = 36,
771✔
899
        groupHeaderHeight: groupHeaderHeightIn = headerHeightIn,
771✔
900
        theme: themeIn,
771✔
901
        isOutsideClick,
771✔
902
        renderers,
771✔
903
        resizeIndicator,
771✔
904
        scrollToActiveCell = true,
771✔
905
        drawFocusRing: drawFocusRingIn = true,
771✔
906
        portalElementRef,
771✔
907
    } = p;
771✔
908

771✔
909
    const drawFocusRing = drawFocusRingIn === "no-editor" ? overlay === undefined : drawFocusRingIn;
771!
910

771✔
911
    const rowMarkersObj = typeof p.rowMarkers === "string" ? undefined : p.rowMarkers;
771✔
912

771✔
913
    const rowMarkers = rowMarkersObj?.kind ?? (p.rowMarkers as RowMarkerOptions["kind"]) ?? "none";
771!
914
    const rowMarkerWidthRaw = rowMarkersObj?.width ?? p.rowMarkerWidth;
771!
915
    const rowMarkerStartIndex = rowMarkersObj?.startIndex ?? p.rowMarkerStartIndex ?? 1;
771!
916
    const rowMarkerTheme = rowMarkersObj?.theme ?? p.rowMarkerTheme;
771!
917
    const headerRowMarkerTheme = rowMarkersObj?.headerTheme;
771!
918
    const headerRowMarkerAlwaysVisible = rowMarkersObj?.headerAlwaysVisible;
771!
919
    const headerRowMarkerDisabled = rowSelect !== "multi" || rowMarkersObj?.headerDisabled === true;
771!
920
    const rowMarkerCheckboxStyle = rowMarkersObj?.checkboxStyle ?? "square";
771!
921

771✔
922
    const minColumnWidth = Math.max(minColumnWidthIn, 20);
771✔
923
    const maxColumnWidth = Math.max(maxColumnWidthIn, minColumnWidth);
771✔
924
    const maxColumnAutoWidth = Math.max(maxColumnAutoWidthIn ?? maxColumnWidth, minColumnWidth);
771✔
925

771✔
926
    const docStyle = React.useMemo(() => {
771✔
927
        if (typeof window === "undefined") return { fontSize: "16px" };
157!
928
        return window.getComputedStyle(document.documentElement);
157✔
929
    }, []);
771✔
930

771✔
931
    const {
771✔
932
        rows,
771✔
933
        rowNumberMapper,
771✔
934
        rowHeight: rowHeightPostGrouping,
771✔
935
        getRowThemeOverride,
771✔
936
    } = useRowGroupingInner(rowGrouping, rowsIn, rowHeightIn, getRowThemeOverrideIn);
771✔
937

771✔
938
    const remSize = React.useMemo(() => Number.parseFloat(docStyle.fontSize), [docStyle]);
771✔
939
    const { rowHeight, headerHeight, groupHeaderHeight, theme, overscrollX, overscrollY } = useRemAdjuster({
771✔
940
        groupHeaderHeight: groupHeaderHeightIn,
771✔
941
        headerHeight: headerHeightIn,
771✔
942
        overscrollX: overscrollXIn,
771✔
943
        overscrollY: overscrollYIn,
771✔
944
        remSize,
771✔
945
        rowHeight: rowHeightPostGrouping,
771✔
946
        scaleToRem,
771✔
947
        theme: themeIn,
771✔
948
    });
771✔
949

771✔
950
    const keybindings = useKeybindingsWithDefaults(keybindingsIn);
771✔
951

771✔
952
    const rowMarkerWidth = rowMarkerWidthRaw ?? (rowsIn > 10_000 ? 48 : rowsIn > 1000 ? 44 : rowsIn > 100 ? 36 : 32);
771!
953
    const hasRowMarkers = rowMarkers !== "none";
771✔
954
    const rowMarkerOffset = hasRowMarkers ? 1 : 0;
771✔
955
    const showTrailingBlankRow = trailingRowOptions !== undefined;
771✔
956
    const lastRowSticky = trailingRowOptions?.sticky === true;
771✔
957

771✔
958
    const [showSearchInner, setShowSearchInner] = React.useState(false);
771✔
959
    const showSearch = showSearchIn ?? showSearchInner;
771✔
960

771✔
961
    const onSearchClose = React.useCallback(() => {
771✔
962
        if (onSearchCloseIn !== undefined) {
2✔
963
            onSearchCloseIn();
2✔
964
        } else {
2!
965
            setShowSearchInner(false);
×
966
        }
×
967
    }, [onSearchCloseIn]);
771✔
968

771✔
969
    const gridSelectionOuterMangled: GridSelection | undefined = React.useMemo((): GridSelection | undefined => {
771✔
970
        return gridSelectionOuter === undefined ? undefined : shiftSelection(gridSelectionOuter, rowMarkerOffset);
304✔
971
    }, [gridSelectionOuter, rowMarkerOffset]);
771✔
972
    const gridSelection = gridSelectionOuterMangled ?? gridSelectionInner;
771✔
973

771✔
974
    const abortControllerRef = React.useRef() as React.MutableRefObject<AbortController>;
771✔
975
    if (abortControllerRef.current === undefined) abortControllerRef.current = new AbortController();
771✔
976

771✔
977
    React.useEffect(() => () => abortControllerRef?.current.abort(), []);
771✔
978

771✔
979
    const [getCellsForSelection, getCellsForSeletionDirect] = useCellsForSelection(
771✔
980
        getCellsForSelectionIn,
771✔
981
        getCellContent,
771✔
982
        rowMarkerOffset,
771✔
983
        abortControllerRef.current,
771✔
984
        rows
771✔
985
    );
771✔
986

771✔
987
    const validateCell = React.useCallback<NonNullable<typeof validateCellIn>>(
771✔
988
        (cell, newValue, prevValue) => {
771✔
989
            if (validateCellIn === undefined) return true;
18✔
990
            const item: Item = [cell[0] - rowMarkerOffset, cell[1]];
1✔
991
            return validateCellIn?.(item, newValue, prevValue);
1✔
992
        },
18✔
993
        [rowMarkerOffset, validateCellIn]
771✔
994
    );
771✔
995

771✔
996
    const expectedExternalGridSelection = React.useRef<GridSelection | undefined>(gridSelectionOuter);
771✔
997
    const setGridSelection = React.useCallback(
771✔
998
        (newVal: GridSelection, expand: boolean): void => {
771✔
999
            if (expand) {
194✔
1000
                newVal = expandSelection(
145✔
1001
                    newVal,
145✔
1002
                    getCellsForSelection,
145✔
1003
                    rowMarkerOffset,
145✔
1004
                    spanRangeBehavior,
145✔
1005
                    abortControllerRef.current
145✔
1006
                );
145✔
1007
            }
145✔
1008
            if (onGridSelectionChange !== undefined) {
194✔
1009
                expectedExternalGridSelection.current = shiftSelection(newVal, -rowMarkerOffset);
151✔
1010
                onGridSelectionChange(expectedExternalGridSelection.current);
151✔
1011
            } else {
194✔
1012
                setGridSelectionInner(newVal);
43✔
1013
            }
43✔
1014
        },
194✔
1015
        [onGridSelectionChange, getCellsForSelection, rowMarkerOffset, spanRangeBehavior]
771✔
1016
    );
771✔
1017

771✔
1018
    const onColumnResize = whenDefined(
771✔
1019
        onColumnResizeIn,
771✔
1020
        React.useCallback<NonNullable<typeof onColumnResizeIn>>(
771✔
1021
            (_, w, ind, wg) => {
771✔
1022
                onColumnResizeIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
11✔
1023
            },
11✔
1024
            [onColumnResizeIn, rowMarkerOffset, columnsIn]
771✔
1025
        )
771✔
1026
    );
771✔
1027

771✔
1028
    const onColumnResizeEnd = whenDefined(
771✔
1029
        onColumnResizeEndIn,
771✔
1030
        React.useCallback<NonNullable<typeof onColumnResizeEndIn>>(
771✔
1031
            (_, w, ind, wg) => {
771✔
1032
                onColumnResizeEndIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
2✔
1033
            },
2✔
1034
            [onColumnResizeEndIn, rowMarkerOffset, columnsIn]
771✔
1035
        )
771✔
1036
    );
771✔
1037

771✔
1038
    const onColumnResizeStart = whenDefined(
771✔
1039
        onColumnResizeStartIn,
771✔
1040
        React.useCallback<NonNullable<typeof onColumnResizeStartIn>>(
771✔
1041
            (_, w, ind, wg) => {
771✔
1042
                onColumnResizeStartIn?.(columnsIn[ind - rowMarkerOffset], w, ind - rowMarkerOffset, wg);
×
1043
            },
×
1044
            [onColumnResizeStartIn, rowMarkerOffset, columnsIn]
771✔
1045
        )
771✔
1046
    );
771✔
1047

771✔
1048
    const drawHeader = whenDefined(
771✔
1049
        drawHeaderIn,
771✔
1050
        React.useCallback<NonNullable<typeof drawHeaderIn>>(
771✔
1051
            (args, draw) => {
771✔
1052
                return drawHeaderIn?.({ ...args, columnIndex: args.columnIndex - rowMarkerOffset }, draw) ?? false;
×
1053
            },
×
1054
            [drawHeaderIn, rowMarkerOffset]
771✔
1055
        )
771✔
1056
    );
771✔
1057

771✔
1058
    const drawCell = whenDefined(
771✔
1059
        drawCellIn,
771✔
1060
        React.useCallback<NonNullable<typeof drawCellIn>>(
771✔
1061
            (args, draw) => {
771✔
1062
                return drawCellIn?.({ ...args, col: args.col - rowMarkerOffset }, draw) ?? false;
×
1063
            },
×
1064
            [drawCellIn, rowMarkerOffset]
771✔
1065
        )
771✔
1066
    );
771✔
1067

771✔
1068
    const onDelete = React.useCallback<NonNullable<DataEditorProps["onDelete"]>>(
771✔
1069
        sel => {
771✔
1070
            if (onDeleteIn !== undefined) {
9✔
1071
                const result = onDeleteIn(shiftSelection(sel, -rowMarkerOffset));
5✔
1072
                if (typeof result === "boolean") {
5!
1073
                    return result;
×
1074
                }
×
1075
                return shiftSelection(result, rowMarkerOffset);
5✔
1076
            }
5✔
1077
            return true;
4✔
1078
        },
9✔
1079
        [onDeleteIn, rowMarkerOffset]
771✔
1080
    );
771✔
1081

771✔
1082
    const [setCurrent, setSelectedRows, setSelectedColumns] = useSelectionBehavior(
771✔
1083
        gridSelection,
771✔
1084
        setGridSelection,
771✔
1085
        rangeSelectionBlending,
771✔
1086
        columnSelectionBlending,
771✔
1087
        rowSelectionBlending,
771✔
1088
        rangeSelect,
771✔
1089
        rangeSelectionColumnSpanning
771✔
1090
    );
771✔
1091

771✔
1092
    const mergedTheme = React.useMemo(() => {
771✔
1093
        return mergeAndRealizeTheme(getDataEditorTheme(), theme);
157✔
1094
    }, [theme]);
771✔
1095

771✔
1096
    const [clientSize, setClientSize] = React.useState<readonly [number, number, number]>([0, 0, 0]);
771✔
1097

771✔
1098
    const rendererMap = React.useMemo(() => {
771✔
1099
        if (renderers === undefined) return {};
157!
1100
        const result: Partial<Record<InnerGridCellKind | GridCellKind, InternalCellRenderer<InnerGridCell>>> = {};
157✔
1101
        for (const r of renderers) {
157✔
1102
            result[r.kind] = r;
2,042✔
1103
        }
2,042✔
1104
        return result;
157✔
1105
    }, [renderers]);
771✔
1106

771✔
1107
    const getCellRenderer: <T extends InnerGridCell>(cell: T) => CellRenderer<T> | undefined = React.useCallback(
771✔
1108
        <T extends InnerGridCell>(cell: T) => {
771✔
1109
            if (cell.kind !== GridCellKind.Custom) {
153,258✔
1110
                return rendererMap[cell.kind] as unknown as CellRenderer<T>;
149,528✔
1111
            }
149,528✔
1112
            return additionalRenderers?.find(x => x.isMatch(cell)) as CellRenderer<T>;
153,258✔
1113
        },
153,258✔
1114
        [additionalRenderers, rendererMap]
771✔
1115
    );
771✔
1116

771✔
1117
    // eslint-disable-next-line prefer-const
771✔
1118
    let { sizedColumns: columns, nonGrowWidth } = useColumnSizer(
771✔
1119
        columnsIn,
771✔
1120
        rows,
771✔
1121
        getCellsForSeletionDirect,
771✔
1122
        clientSize[0] - (rowMarkerOffset === 0 ? 0 : rowMarkerWidth) - clientSize[2],
771✔
1123
        minColumnWidth,
771✔
1124
        maxColumnAutoWidth,
771✔
1125
        mergedTheme,
771✔
1126
        getCellRenderer,
771✔
1127
        abortControllerRef.current
771✔
1128
    );
771✔
1129
    if (rowMarkers !== "none") nonGrowWidth += rowMarkerWidth;
771✔
1130

771✔
1131
    const enableGroups = React.useMemo(() => {
771✔
1132
        return columns.some(c => c.group !== undefined);
158✔
1133
    }, [columns]);
771✔
1134

771✔
1135
    const totalHeaderHeight = enableGroups ? headerHeight + groupHeaderHeight : headerHeight;
771✔
1136

771✔
1137
    const numSelectedRows = gridSelection.rows.length;
771✔
1138
    const rowMarkerChecked =
771✔
1139
        rowMarkers === "none" ? undefined : numSelectedRows === 0 ? false : numSelectedRows === rows ? true : undefined;
771✔
1140

771✔
1141
    const mangledCols = React.useMemo(() => {
771✔
1142
        if (rowMarkers === "none") return columns;
175✔
1143
        return [
53✔
1144
            {
53✔
1145
                title: "",
53✔
1146
                width: rowMarkerWidth,
53✔
1147
                icon: undefined,
53✔
1148
                hasMenu: false,
53✔
1149
                style: "normal" as const,
53✔
1150
                themeOverride: rowMarkerTheme,
53✔
1151
                rowMarker: rowMarkerCheckboxStyle,
53✔
1152
                rowMarkerChecked,
53✔
1153
                headerRowMarkerTheme,
53✔
1154
                headerRowMarkerAlwaysVisible,
53✔
1155
                headerRowMarkerDisabled,
53✔
1156
            },
53✔
1157
            ...columns,
53✔
1158
        ];
53✔
1159
    }, [
771✔
1160
        rowMarkers,
771✔
1161
        columns,
771✔
1162
        rowMarkerWidth,
771✔
1163
        rowMarkerTheme,
771✔
1164
        rowMarkerCheckboxStyle,
771✔
1165
        rowMarkerChecked,
771✔
1166
        headerRowMarkerTheme,
771✔
1167
        headerRowMarkerAlwaysVisible,
771✔
1168
        headerRowMarkerDisabled,
771✔
1169
    ]);
771✔
1170

771✔
1171
    const visibleRegionRef = React.useRef<VisibleRegion>({
771✔
1172
        height: 1,
771✔
1173
        width: 1,
771✔
1174
        x: 0,
771✔
1175
        y: 0,
771✔
1176
    });
771✔
1177

771✔
1178
    const hasJustScrolled = React.useRef(false);
771✔
1179

771✔
1180
    const { setVisibleRegion, visibleRegion, scrollRef } = useInitialScrollOffset(
771✔
1181
        scrollOffsetX,
771✔
1182
        scrollOffsetY,
771✔
1183
        rowHeight,
771✔
1184
        visibleRegionRef,
771✔
1185
        () => (hasJustScrolled.current = true)
771✔
1186
    );
771✔
1187

771✔
1188
    visibleRegionRef.current = visibleRegion;
771✔
1189

771✔
1190
    const cellXOffset = visibleRegion.x + rowMarkerOffset;
771✔
1191
    const cellYOffset = visibleRegion.y;
771✔
1192

771✔
1193
    const gridRef = React.useRef<DataGridRef | null>(null);
771✔
1194

771✔
1195
    const focus = React.useCallback((immediate?: boolean) => {
771✔
1196
        if (immediate === true) {
143✔
1197
            gridRef.current?.focus();
12✔
1198
        } else {
143✔
1199
            window.requestAnimationFrame(() => {
131✔
1200
                gridRef.current?.focus();
130✔
1201
            });
131✔
1202
        }
131✔
1203
    }, []);
771✔
1204

771✔
1205
    const mangledRows = showTrailingBlankRow ? rows + 1 : rows;
771✔
1206

771✔
1207
    const mangledOnCellsEdited = React.useCallback<NonNullable<typeof onCellsEdited>>(
771✔
1208
        (items: readonly EditListItem[]) => {
771✔
1209
            const mangledItems =
29✔
1210
                rowMarkerOffset === 0
29✔
1211
                    ? items
24✔
1212
                    : items.map(x => ({
5✔
1213
                          ...x,
29✔
1214
                          location: [x.location[0] - rowMarkerOffset, x.location[1]] as const,
29✔
1215
                      }));
5✔
1216
            const r = onCellsEdited?.(mangledItems);
29✔
1217

29✔
1218
            if (r !== true) {
29✔
1219
                for (const i of mangledItems) onCellEdited?.(i.location, i.value);
28✔
1220
            }
28✔
1221

29✔
1222
            return r;
29✔
1223
        },
29✔
1224
        [onCellEdited, onCellsEdited, rowMarkerOffset]
771✔
1225
    );
771✔
1226

771✔
1227
    const [fillHighlightRegion, setFillHighlightRegion] = React.useState<Rectangle | undefined>();
771✔
1228

771✔
1229
    // this will generally be undefined triggering the memo less often
771✔
1230
    const highlightRange =
771✔
1231
        gridSelection.current !== undefined &&
771✔
1232
        gridSelection.current.range.width * gridSelection.current.range.height > 1
392✔
1233
            ? gridSelection.current.range
46✔
1234
            : undefined;
725✔
1235

771✔
1236
    const highlightFocus = drawFocusRing ? gridSelection.current?.cell : undefined;
771!
1237
    const highlightFocusCol = highlightFocus?.[0];
771✔
1238
    const highlightFocusRow = highlightFocus?.[1];
771✔
1239

771✔
1240
    const highlightRegions = React.useMemo(() => {
771✔
1241
        if (
315✔
1242
            (highlightRegionsIn === undefined || highlightRegionsIn.length === 0) &&
315✔
1243
            (highlightRange ?? highlightFocusCol ?? highlightFocusRow ?? fillHighlightRegion) === undefined
314✔
1244
        )
315✔
1245
            return undefined;
315✔
1246

165✔
1247
        const regions: Highlight[] = [];
165✔
1248

165✔
1249
        if (highlightRegionsIn !== undefined) {
308✔
1250
            for (const r of highlightRegionsIn) {
1✔
1251
                const maxWidth = mangledCols.length - r.range.x - rowMarkerOffset;
1✔
1252
                if (maxWidth > 0) {
1✔
1253
                    regions.push({
1✔
1254
                        color: r.color,
1✔
1255
                        range: {
1✔
1256
                            ...r.range,
1✔
1257
                            x: r.range.x + rowMarkerOffset,
1✔
1258
                            width: Math.min(maxWidth, r.range.width),
1✔
1259
                        },
1✔
1260
                        style: r.style,
1✔
1261
                    });
1✔
1262
                }
1✔
1263
            }
1✔
1264
        }
1✔
1265

165✔
1266
        if (fillHighlightRegion !== undefined) {
308✔
1267
            regions.push({
6✔
1268
                color: withAlpha(mergedTheme.accentColor, 0),
6✔
1269
                range: fillHighlightRegion,
6✔
1270
                style: "dashed",
6✔
1271
            });
6✔
1272
        }
6✔
1273

165✔
1274
        if (highlightRange !== undefined) {
308✔
1275
            regions.push({
31✔
1276
                color: withAlpha(mergedTheme.accentColor, 0.5),
31✔
1277
                range: highlightRange,
31✔
1278
                style: "solid-outline",
31✔
1279
            });
31✔
1280
        }
31✔
1281

165✔
1282
        if (highlightFocusCol !== undefined && highlightFocusRow !== undefined) {
315✔
1283
            regions.push({
164✔
1284
                color: mergedTheme.accentColor,
164✔
1285
                range: {
164✔
1286
                    x: highlightFocusCol,
164✔
1287
                    y: highlightFocusRow,
164✔
1288
                    width: 1,
164✔
1289
                    height: 1,
164✔
1290
                },
164✔
1291
                style: "solid-outline",
164✔
1292
            });
164✔
1293
        }
164✔
1294

165✔
1295
        return regions.length > 0 ? regions : undefined;
315!
1296
    }, [
771✔
1297
        fillHighlightRegion,
771✔
1298
        highlightRange,
771✔
1299
        highlightFocusCol,
771✔
1300
        highlightFocusRow,
771✔
1301
        highlightRegionsIn,
771✔
1302
        mangledCols.length,
771✔
1303
        mergedTheme.accentColor,
771✔
1304
        rowMarkerOffset,
771✔
1305
    ]);
771✔
1306

771✔
1307
    const mangledColsRef = React.useRef(mangledCols);
771✔
1308
    mangledColsRef.current = mangledCols;
771✔
1309
    const getMangledCellContent = React.useCallback(
771✔
1310
        ([col, row]: Item, forceStrict: boolean = false): InnerGridCell => {
771✔
1311
            const isTrailing = showTrailingBlankRow && row === mangledRows - 1;
154,189✔
1312
            const isRowMarkerCol = col === 0 && hasRowMarkers;
154,189✔
1313
            if (isRowMarkerCol) {
154,189✔
1314
                if (isTrailing) {
2,487✔
1315
                    return loadingCell;
76✔
1316
                }
76✔
1317
                const mappedRow = rowNumberMapper(row);
2,411✔
1318
                if (mappedRow === undefined) return loadingCell;
2,487!
1319
                return {
2,411✔
1320
                    kind: InnerGridCellKind.Marker,
2,411✔
1321
                    allowOverlay: false,
2,411✔
1322
                    checkboxStyle: rowMarkerCheckboxStyle,
2,411✔
1323
                    checked: gridSelection?.rows.hasIndex(row) === true,
2,487✔
1324
                    markerKind: rowMarkers === "clickable-number" ? "number" : rowMarkers,
2,487!
1325
                    row: rowMarkerStartIndex + mappedRow,
2,487✔
1326
                    drawHandle: onRowMoved !== undefined,
2,487✔
1327
                    cursor: rowMarkers === "clickable-number" ? "pointer" : undefined,
2,487!
1328
                };
2,487✔
1329
            } else if (isTrailing) {
154,189✔
1330
                //If the grid is empty, we will return text
4,053✔
1331
                const isFirst = col === rowMarkerOffset;
4,053✔
1332

4,053✔
1333
                const maybeFirstColumnHint = isFirst ? (trailingRowOptions?.hint ?? "") : "";
4,053✔
1334
                const c = mangledColsRef.current[col];
4,053✔
1335

4,053✔
1336
                if (c?.trailingRowOptions?.disabled === true) {
4,053!
1337
                    return loadingCell;
×
1338
                } else {
4,053✔
1339
                    const hint = c?.trailingRowOptions?.hint ?? maybeFirstColumnHint;
4,053!
1340
                    const icon = c?.trailingRowOptions?.addIcon ?? trailingRowOptions?.addIcon;
4,053!
1341
                    return {
4,053✔
1342
                        kind: InnerGridCellKind.NewRow,
4,053✔
1343
                        hint,
4,053✔
1344
                        allowOverlay: false,
4,053✔
1345
                        icon,
4,053✔
1346
                    };
4,053✔
1347
                }
4,053✔
1348
            } else {
151,702✔
1349
                const outerCol = col - rowMarkerOffset;
147,649✔
1350
                if (forceStrict || experimental?.strict === true) {
147,649✔
1351
                    const vr = visibleRegionRef.current;
26,502✔
1352
                    const isOutsideMainArea =
26,502✔
1353
                        vr.x > outerCol ||
26,502✔
1354
                        outerCol > vr.x + vr.width ||
26,502✔
1355
                        vr.y > row ||
26,502✔
1356
                        row > vr.y + vr.height ||
26,502✔
1357
                        row >= rowsRef.current;
26,502✔
1358
                    const isSelected = outerCol === vr.extras?.selected?.[0] && row === vr.extras?.selected[1];
26,502✔
1359
                    let isInFreezeArea = false;
26,502✔
1360
                    if (vr.extras?.freezeRegions !== undefined) {
26,502✔
1361
                        for (const fr of vr.extras.freezeRegions) {
26,502!
1362
                            if (pointInRect(fr, outerCol, row)) {
×
1363
                                isInFreezeArea = true;
×
1364
                                break;
×
1365
                            }
×
1366
                        }
×
1367
                    }
26,502✔
1368

26,502✔
1369
                    if (isOutsideMainArea && !isSelected && !isInFreezeArea) {
26,502!
1370
                        return loadingCell;
×
1371
                    }
×
1372
                }
26,502✔
1373
                let result = getCellContent([outerCol, row]);
147,649✔
1374
                if (rowMarkerOffset !== 0 && result.span !== undefined) {
147,649!
1375
                    result = {
×
1376
                        ...result,
×
1377
                        span: [result.span[0] + rowMarkerOffset, result.span[1] + rowMarkerOffset],
×
1378
                    };
×
1379
                }
×
1380
                return result;
147,649✔
1381
            }
147,649✔
1382
        },
154,189✔
1383
        [
771✔
1384
            showTrailingBlankRow,
771✔
1385
            mangledRows,
771✔
1386
            hasRowMarkers,
771✔
1387
            rowNumberMapper,
771✔
1388
            rowMarkerCheckboxStyle,
771✔
1389
            gridSelection?.rows,
771✔
1390
            rowMarkers,
771✔
1391
            rowMarkerStartIndex,
771✔
1392
            onRowMoved,
771✔
1393
            rowMarkerOffset,
771✔
1394
            trailingRowOptions?.hint,
771✔
1395
            trailingRowOptions?.addIcon,
771✔
1396
            experimental?.strict,
771✔
1397
            getCellContent,
771✔
1398
        ]
771✔
1399
    );
771✔
1400

771✔
1401
    const mangledGetGroupDetails = React.useCallback<NonNullable<DataEditorProps["getGroupDetails"]>>(
771✔
1402
        group => {
771✔
1403
            let result = getGroupDetails?.(group) ?? { name: group };
8,814✔
1404
            if (onGroupHeaderRenamed !== undefined && group !== "") {
8,814✔
1405
                result = {
87✔
1406
                    icon: result.icon,
87✔
1407
                    name: result.name,
87✔
1408
                    overrideTheme: result.overrideTheme,
87✔
1409
                    actions: [
87✔
1410
                        ...(result.actions ?? []),
87✔
1411
                        {
87✔
1412
                            title: "Rename",
87✔
1413
                            icon: "renameIcon",
87✔
1414
                            onClick: e =>
87✔
1415
                                setRenameGroup({
1✔
1416
                                    group: result.name,
1✔
1417
                                    bounds: e.bounds,
1✔
1418
                                }),
1✔
1419
                        },
87✔
1420
                    ],
87✔
1421
                };
87✔
1422
            }
87✔
1423
            return result;
8,814✔
1424
        },
8,814✔
1425
        [getGroupDetails, onGroupHeaderRenamed]
771✔
1426
    );
771✔
1427

771✔
1428
    const setOverlaySimple = React.useCallback(
771✔
1429
        (val: Omit<NonNullable<typeof overlay>, "theme">) => {
771✔
1430
            const [col, row] = val.cell;
26✔
1431
            const column = mangledCols[col];
26✔
1432
            const groupTheme =
26✔
1433
                column?.group !== undefined ? mangledGetGroupDetails(column.group)?.overrideTheme : undefined;
26!
1434
            const colTheme = column?.themeOverride;
26✔
1435
            const rowTheme = getRowThemeOverride?.(row);
26!
1436

26✔
1437
            setOverlay({
26✔
1438
                ...val,
26✔
1439
                theme: mergeAndRealizeTheme(mergedTheme, groupTheme, colTheme, rowTheme, val.content.themeOverride),
26✔
1440
            });
26✔
1441
        },
26✔
1442
        [getRowThemeOverride, mangledCols, mangledGetGroupDetails, mergedTheme]
771✔
1443
    );
771✔
1444

771✔
1445
    const reselect = React.useCallback(
771✔
1446
        (bounds: Rectangle, activation: CellActivatedEventArgs, initialValue?: string) => {
771✔
1447
            if (gridSelection.current === undefined) return;
27!
1448

27✔
1449
            const [col, row] = gridSelection.current.cell;
27✔
1450
            const c = getMangledCellContent([col, row]);
27✔
1451
            if (c.kind !== GridCellKind.Boolean && c.allowOverlay) {
27✔
1452
                let content = c;
25✔
1453
                if (initialValue !== undefined) {
25✔
1454
                    switch (content.kind) {
13✔
1455
                        case GridCellKind.Number: {
13!
1456
                            const d = maybe(() => (initialValue === "-" ? -0 : Number.parseFloat(initialValue)), 0);
×
1457
                            content = {
×
1458
                                ...content,
×
1459
                                data: Number.isNaN(d) ? 0 : d,
×
1460
                            };
×
1461
                            break;
×
1462
                        }
×
1463
                        case GridCellKind.Text:
13✔
1464
                        case GridCellKind.Markdown:
13✔
1465
                        case GridCellKind.Uri:
13✔
1466
                            content = {
13✔
1467
                                ...content,
13✔
1468
                                data: initialValue,
13✔
1469
                            };
13✔
1470
                            break;
13✔
1471
                    }
13✔
1472
                }
13✔
1473

25✔
1474
                setOverlaySimple({
25✔
1475
                    target: bounds,
25✔
1476
                    content,
25✔
1477
                    initialValue,
25✔
1478
                    cell: [col, row],
25✔
1479
                    highlight: initialValue === undefined,
25✔
1480
                    forceEditMode: initialValue !== undefined,
25✔
1481
                    activation,
25✔
1482
                });
25✔
1483
            } else if (c.kind === GridCellKind.Boolean && activation.inputType === "keyboard" && c.readonly !== true) {
27✔
1484
                mangledOnCellsEdited([
1✔
1485
                    {
1✔
1486
                        location: gridSelection.current.cell,
1✔
1487
                        value: {
1✔
1488
                            ...c,
1✔
1489
                            data: toggleBoolean(c.data),
1✔
1490
                        },
1✔
1491
                    },
1✔
1492
                ]);
1✔
1493
                gridRef.current?.damage([{ cell: gridSelection.current.cell }]);
1✔
1494
            }
1✔
1495
        },
27✔
1496
        [getMangledCellContent, gridSelection, mangledOnCellsEdited, setOverlaySimple]
771✔
1497
    );
771✔
1498

771✔
1499
    const focusOnRowFromTrailingBlankRow = React.useCallback(
771✔
1500
        (col: number, row: number) => {
771✔
1501
            const bounds = gridRef.current?.getBounds(col, row);
1✔
1502
            if (bounds === undefined || scrollRef.current === null) {
1!
1503
                return;
×
1504
            }
×
1505

1✔
1506
            const content = getMangledCellContent([col, row]);
1✔
1507
            if (!content.allowOverlay) {
1!
1508
                return;
×
1509
            }
×
1510

1✔
1511
            setOverlaySimple({
1✔
1512
                target: bounds,
1✔
1513
                content,
1✔
1514
                initialValue: undefined,
1✔
1515
                highlight: true,
1✔
1516
                cell: [col, row],
1✔
1517
                forceEditMode: true,
1✔
1518
                activation: { inputType: "keyboard", key: "Enter" },
1✔
1519
            });
1✔
1520
        },
1✔
1521
        [getMangledCellContent, scrollRef, setOverlaySimple]
771✔
1522
    );
771✔
1523

771✔
1524
    const scrollTo = React.useCallback<ScrollToFn>(
771✔
1525
        (col, row, dir = "both", paddingX = 0, paddingY = 0, options = undefined): void => {
771✔
1526
            if (scrollRef.current !== null) {
53✔
1527
                const grid = gridRef.current;
53✔
1528
                const canvas = canvasRef.current;
53✔
1529

53✔
1530
                const trueCol = typeof col !== "number" ? (col.unit === "cell" ? col.amount : undefined) : col;
53!
1531
                const trueRow = typeof row !== "number" ? (row.unit === "cell" ? row.amount : undefined) : row;
53!
1532
                const desiredX = typeof col !== "number" && col.unit === "px" ? col.amount : undefined;
53!
1533
                const desiredY = typeof row !== "number" && row.unit === "px" ? row.amount : undefined;
53✔
1534
                if (grid !== null && canvas !== null) {
53✔
1535
                    let targetRect: Rectangle = {
53✔
1536
                        x: 0,
53✔
1537
                        y: 0,
53✔
1538
                        width: 0,
53✔
1539
                        height: 0,
53✔
1540
                    };
53✔
1541

53✔
1542
                    let scrollX = 0;
53✔
1543
                    let scrollY = 0;
53✔
1544

53✔
1545
                    if (trueCol !== undefined || trueRow !== undefined) {
53!
1546
                        targetRect = grid.getBounds((trueCol ?? 0) + rowMarkerOffset, trueRow ?? 0) ?? targetRect;
53!
1547
                        if (targetRect.width === 0 || targetRect.height === 0) return;
53!
1548
                    }
53✔
1549

53✔
1550
                    const scrollBounds = canvas.getBoundingClientRect();
53✔
1551
                    const scale = scrollBounds.width / canvas.offsetWidth;
53✔
1552

53✔
1553
                    if (desiredX !== undefined) {
53!
1554
                        targetRect = {
×
1555
                            ...targetRect,
×
1556
                            x: desiredX - scrollBounds.left - scrollRef.current.scrollLeft,
×
1557
                            width: 1,
×
1558
                        };
×
1559
                    }
×
1560
                    if (desiredY !== undefined) {
53✔
1561
                        targetRect = {
4✔
1562
                            ...targetRect,
4✔
1563
                            y: desiredY + scrollBounds.top - scrollRef.current.scrollTop,
4✔
1564
                            height: 1,
4✔
1565
                        };
4✔
1566
                    }
4✔
1567

53✔
1568
                    if (targetRect !== undefined) {
53✔
1569
                        const bounds = {
53✔
1570
                            x: targetRect.x - paddingX,
53✔
1571
                            y: targetRect.y - paddingY,
53✔
1572
                            width: targetRect.width + 2 * paddingX,
53✔
1573
                            height: targetRect.height + 2 * paddingY,
53✔
1574
                        };
53✔
1575

53✔
1576
                        let frozenWidth = 0;
53✔
1577
                        for (let i = 0; i < freezeColumns; i++) {
53!
1578
                            frozenWidth += columns[i].width;
×
1579
                        }
×
1580
                        let trailingRowHeight = 0;
53✔
1581
                        const freezeTrailingRowsEffective = freezeTrailingRows + (lastRowSticky ? 1 : 0);
53✔
1582
                        if (freezeTrailingRowsEffective > 0) {
53✔
1583
                            trailingRowHeight = getFreezeTrailingHeight(
51✔
1584
                                mangledRows,
51✔
1585
                                freezeTrailingRowsEffective,
51✔
1586
                                rowHeight
51✔
1587
                            );
51✔
1588
                        }
51✔
1589

53✔
1590
                        // scrollBounds is already scaled
53✔
1591
                        let sLeft = frozenWidth * scale + scrollBounds.left + rowMarkerOffset * rowMarkerWidth * scale;
53✔
1592
                        let sRight = scrollBounds.right;
53✔
1593
                        let sTop = scrollBounds.top + totalHeaderHeight * scale;
53✔
1594
                        let sBottom = scrollBounds.bottom - trailingRowHeight * scale;
53✔
1595

53✔
1596
                        const minx = targetRect.width + paddingX * 2;
53✔
1597
                        switch (options?.hAlign) {
53✔
1598
                            case "start":
53!
1599
                                sRight = sLeft + minx;
×
1600
                                break;
×
1601
                            case "end":
53!
1602
                                sLeft = sRight - minx;
×
1603
                                break;
×
1604
                            case "center":
53!
1605
                                sLeft = Math.floor((sLeft + sRight) / 2) - minx / 2;
×
1606
                                sRight = sLeft + minx;
×
1607
                                break;
×
1608
                        }
53✔
1609

53✔
1610
                        const miny = targetRect.height + paddingY * 2;
53✔
1611
                        switch (options?.vAlign) {
53✔
1612
                            case "start":
53✔
1613
                                sBottom = sTop + miny;
1✔
1614
                                break;
1✔
1615
                            case "end":
53✔
1616
                                sTop = sBottom - miny;
1✔
1617
                                break;
1✔
1618
                            case "center":
53✔
1619
                                sTop = Math.floor((sTop + sBottom) / 2) - miny / 2;
1✔
1620
                                sBottom = sTop + miny;
1✔
1621
                                break;
1✔
1622
                        }
53✔
1623

53✔
1624
                        if (sLeft > bounds.x) {
53!
1625
                            scrollX = bounds.x - sLeft;
×
1626
                        } else if (sRight < bounds.x + bounds.width) {
53✔
1627
                            scrollX = bounds.x + bounds.width - sRight;
7✔
1628
                        }
7✔
1629

53✔
1630
                        if (sTop > bounds.y) {
53!
1631
                            scrollY = bounds.y - sTop;
×
1632
                        } else if (sBottom < bounds.y + bounds.height) {
53✔
1633
                            scrollY = bounds.y + bounds.height - sBottom;
16✔
1634
                        }
16✔
1635

53✔
1636
                        if (dir === "vertical" || (typeof col === "number" && col < freezeColumns)) {
53✔
1637
                            scrollX = 0;
4✔
1638
                        } else if (
4✔
1639
                            dir === "horizontal" ||
49✔
1640
                            (typeof row === "number" && row >= mangledRows - freezeTrailingRowsEffective)
41✔
1641
                        ) {
49✔
1642
                            scrollY = 0;
10✔
1643
                        }
10✔
1644

53✔
1645
                        if (scrollX !== 0 || scrollY !== 0) {
53✔
1646
                            // Remove scaling as scrollTo method is unaffected by transform scale.
20✔
1647
                            if (scale !== 1) {
20!
1648
                                scrollX /= scale;
×
1649
                                scrollY /= scale;
×
1650
                            }
×
1651
                            scrollRef.current.scrollTo({
20✔
1652
                                left: scrollX + scrollRef.current.scrollLeft,
20✔
1653
                                top: scrollY + scrollRef.current.scrollTop,
20✔
1654
                                behavior: options?.behavior ?? "auto",
20✔
1655
                            });
20✔
1656
                        }
20✔
1657
                    }
53✔
1658
                }
53✔
1659
            }
53✔
1660
        },
53✔
1661
        [
771✔
1662
            rowMarkerOffset,
771✔
1663
            freezeTrailingRows,
771✔
1664
            rowMarkerWidth,
771✔
1665
            scrollRef,
771✔
1666
            totalHeaderHeight,
771✔
1667
            freezeColumns,
771✔
1668
            columns,
771✔
1669
            mangledRows,
771✔
1670
            lastRowSticky,
771✔
1671
            rowHeight,
771✔
1672
        ]
771✔
1673
    );
771✔
1674

771✔
1675
    const focusCallback = React.useRef(focusOnRowFromTrailingBlankRow);
771✔
1676
    const getCellContentRef = React.useRef(getCellContent);
771✔
1677

771✔
1678
    focusCallback.current = focusOnRowFromTrailingBlankRow;
771✔
1679
    getCellContentRef.current = getCellContent;
771✔
1680

771✔
1681
    const rowsRef = React.useRef(rows);
771✔
1682
    rowsRef.current = rows;
771✔
1683

771✔
1684
    const colsRef = React.useRef(mangledCols.length);
771✔
1685
    colsRef.current = mangledCols.length;
771✔
1686

771✔
1687
    const appendRow = React.useCallback(
771✔
1688
        async (col: number, openOverlay: boolean = true, behavior?: ScrollBehavior): Promise<void> => {
771✔
1689
            const c = mangledCols[col];
2✔
1690
            if (c?.trailingRowOptions?.disabled === true) {
2!
1691
                return;
×
1692
            }
×
1693
            const appendResult = onRowAppended?.();
2✔
1694

2✔
1695
            let r: "top" | "bottom" | number | undefined = undefined;
2✔
1696
            let bottom = true;
2✔
1697
            if (appendResult !== undefined) {
2!
1698
                r = await appendResult;
×
1699
                if (r === "top") bottom = false;
×
1700
                if (typeof r === "number") bottom = false;
×
1701
            }
×
1702

2✔
1703
            let backoff = 0;
2✔
1704
            const doFocus = () => {
2✔
1705
                if (rowsRef.current <= rows) {
4✔
1706
                    if (backoff < 500) {
2✔
1707
                        window.setTimeout(doFocus, backoff);
2✔
1708
                    }
2✔
1709
                    backoff = 50 + backoff * 2;
2✔
1710
                    return;
2✔
1711
                }
2✔
1712

2✔
1713
                const row = typeof r === "number" ? r : bottom ? rows : 0;
4!
1714
                scrollToRef.current(col - rowMarkerOffset, row, "both", 0, 0, behavior ? { behavior } : undefined);
4!
1715
                setCurrent(
4✔
1716
                    {
4✔
1717
                        cell: [col, row],
4✔
1718
                        range: {
4✔
1719
                            x: col,
4✔
1720
                            y: row,
4✔
1721
                            width: 1,
4✔
1722
                            height: 1,
4✔
1723
                        },
4✔
1724
                    },
4✔
1725
                    false,
4✔
1726
                    false,
4✔
1727
                    "edit"
4✔
1728
                );
4✔
1729

4✔
1730
                const cell = getCellContentRef.current([col - rowMarkerOffset, row]);
4✔
1731
                if (cell.allowOverlay && isReadWriteCell(cell) && cell.readonly !== true && openOverlay) {
4✔
1732
                    // wait for scroll to have a chance to process
1✔
1733
                    window.setTimeout(() => {
1✔
1734
                        focusCallback.current(col, row);
1✔
1735
                    }, 0);
1✔
1736
                }
1✔
1737
            };
4✔
1738
            // Queue up to allow the consumer to react to the event and let us check if they did
2✔
1739
            doFocus();
2✔
1740
        },
2✔
1741
        [mangledCols, onRowAppended, rowMarkerOffset, rows, setCurrent]
771✔
1742
    );
771✔
1743

771✔
1744
    const appendColumn = React.useCallback(
771✔
1745
        async (row: number, openOverlay: boolean = true): Promise<void> => {
771✔
1746
            const appendResult = onColumnAppended?.();
1✔
1747

1✔
1748
            let r: "left" | "right" | number | undefined = undefined;
1✔
1749
            let right = true;
1✔
1750
            if (appendResult !== undefined) {
1!
1751
                r = await appendResult;
×
1752
                if (r === "left") right = false;
×
1753
                if (typeof r === "number") right = false;
×
1754
            }
×
1755

1✔
1756
            let backoff = 0;
1✔
1757
            const doFocus = () => {
1✔
1758
                if (colsRef.current <= mangledCols.length) {
2✔
1759
                    if (backoff < 500) {
1✔
1760
                        window.setTimeout(doFocus, backoff);
1✔
1761
                    }
1✔
1762
                    backoff = 50 + backoff * 2;
1✔
1763
                    return;
1✔
1764
                }
1✔
1765

1✔
1766
                const col = typeof r === "number" ? r : right ? mangledCols.length : 0;
2!
1767
                scrollTo(col - rowMarkerOffset, row);
2✔
1768
                setCurrent(
2✔
1769
                    {
2✔
1770
                        cell: [col, row],
2✔
1771
                        range: {
2✔
1772
                            x: col,
2✔
1773
                            y: row,
2✔
1774
                            width: 1,
2✔
1775
                            height: 1,
2✔
1776
                        },
2✔
1777
                    },
2✔
1778
                    false,
2✔
1779
                    false,
2✔
1780
                    "edit"
2✔
1781
                );
2✔
1782

2✔
1783
                const cell = getCellContentRef.current([col - rowMarkerOffset, row]);
2✔
1784
                if (cell.allowOverlay && isReadWriteCell(cell) && cell.readonly !== true && openOverlay) {
2!
1785
                    window.setTimeout(() => {
×
1786
                        focusCallback.current(col, row);
×
1787
                    }, 0);
×
1788
                }
×
1789
            };
2✔
1790
            doFocus();
1✔
1791
        },
1✔
1792
        [mangledCols, onColumnAppended, rowMarkerOffset, scrollTo, setCurrent]
771✔
1793
    );
771✔
1794

771✔
1795
    const getCustomNewRowTargetColumn = React.useCallback(
771✔
1796
        (col: number): number | undefined => {
771✔
1797
            const customTargetColumn =
1✔
1798
                columns[col]?.trailingRowOptions?.targetColumn ?? trailingRowOptions?.targetColumn;
1!
1799

1✔
1800
            if (typeof customTargetColumn === "number") {
1!
1801
                const customTargetOffset = hasRowMarkers ? 1 : 0;
×
1802
                return customTargetColumn + customTargetOffset;
×
1803
            }
×
1804

1✔
1805
            if (typeof customTargetColumn === "object") {
1!
1806
                const maybeIndex = columnsIn.indexOf(customTargetColumn);
×
1807
                if (maybeIndex >= 0) {
×
1808
                    const customTargetOffset = hasRowMarkers ? 1 : 0;
×
1809
                    return maybeIndex + customTargetOffset;
×
1810
                }
×
1811
            }
×
1812

1✔
1813
            return undefined;
1✔
1814
        },
1✔
1815
        [columns, columnsIn, hasRowMarkers, trailingRowOptions?.targetColumn]
771✔
1816
    );
771✔
1817

771✔
1818
    const lastSelectedRowRef = React.useRef<number>();
771✔
1819
    const lastSelectedColRef = React.useRef<number>();
771✔
1820

771✔
1821
    const themeForCell = React.useCallback(
771✔
1822
        (cell: InnerGridCell, pos: Item): FullTheme => {
771✔
1823
            const [col, row] = pos;
30✔
1824
            return mergeAndRealizeTheme(
30✔
1825
                mergedTheme,
30✔
1826
                mangledCols[col]?.themeOverride,
30✔
1827
                getRowThemeOverride?.(row),
30!
1828
                cell.themeOverride
30✔
1829
            );
30✔
1830
        },
30✔
1831
        [getRowThemeOverride, mangledCols, mergedTheme]
771✔
1832
    );
771✔
1833

771✔
1834
    const { mapper } = useRowGrouping(rowGrouping, rowsIn);
771✔
1835

771✔
1836
    const rowGroupingNavBehavior = rowGrouping?.navigationBehavior;
771!
1837

771✔
1838
    const handleSelect = React.useCallback(
771✔
1839
        (args: GridMouseEventArgs) => {
771✔
1840
            const isMultiKey = browserIsOSX.value ? args.metaKey : args.ctrlKey;
136!
1841
            const isMultiRow = isMultiKey && rowSelect === "multi";
136✔
1842

136✔
1843
            const [col, row] = args.location;
136✔
1844
            const selectedColumns = gridSelection.columns;
136✔
1845
            const selectedRows = gridSelection.rows;
136✔
1846
            const [cellCol, cellRow] = gridSelection.current?.cell ?? [];
136✔
1847
            // eslint-disable-next-line unicorn/prefer-switch
136✔
1848
            if (args.kind === "cell") {
136✔
1849
                lastSelectedColRef.current = undefined;
118✔
1850

118✔
1851
                lastMouseSelectLocation.current = [col, row];
118✔
1852

118✔
1853
                if (col === 0 && hasRowMarkers) {
118✔
1854
                    if (
18✔
1855
                        (showTrailingBlankRow === true && row === rows) ||
18✔
1856
                        rowMarkers === "number" ||
18✔
1857
                        rowSelect === "none"
17✔
1858
                    )
18✔
1859
                        return;
18✔
1860

17✔
1861
                    const markerCell = getMangledCellContent(args.location);
17✔
1862
                    if (markerCell.kind !== InnerGridCellKind.Marker) {
18!
1863
                        return;
×
1864
                    }
✔
1865

17✔
1866
                    if (onRowMoved !== undefined) {
18!
1867
                        const renderer = getCellRenderer(markerCell);
×
1868
                        assert(renderer?.kind === InnerGridCellKind.Marker);
×
1869
                        const postClick = renderer?.onClick?.({
×
1870
                            ...args,
×
1871
                            cell: markerCell,
×
1872
                            posX: args.localEventX,
×
1873
                            posY: args.localEventY,
×
1874
                            bounds: args.bounds,
×
1875
                            theme: themeForCell(markerCell, args.location),
×
1876
                            preventDefault: () => undefined,
×
1877
                        }) as MarkerCell | undefined;
×
1878
                        if (postClick === undefined || postClick.checked === markerCell.checked) return;
×
1879
                    }
✔
1880

17✔
1881
                    setOverlay(undefined);
17✔
1882
                    focus();
17✔
1883
                    const isSelected = selectedRows.hasIndex(row);
17✔
1884

17✔
1885
                    const lastHighlighted = lastSelectedRowRef.current;
17✔
1886
                    if (
17✔
1887
                        rowSelect === "multi" &&
17✔
1888
                        (args.shiftKey || args.isLongTouch === true) &&
10✔
1889
                        lastHighlighted !== undefined &&
1✔
1890
                        selectedRows.hasIndex(lastHighlighted)
1✔
1891
                    ) {
18✔
1892
                        const newSlice: Slice = [Math.min(lastHighlighted, row), Math.max(lastHighlighted, row) + 1];
1✔
1893

1✔
1894
                        if (isMultiRow || rowSelectionMode === "multi") {
1!
1895
                            setSelectedRows(undefined, newSlice, true);
×
1896
                        } else {
1✔
1897
                            setSelectedRows(CompactSelection.fromSingleSelection(newSlice), undefined, isMultiRow);
1✔
1898
                        }
1✔
1899
                    } else if (rowSelect === "multi" && (isMultiRow || args.isTouch || rowSelectionMode === "multi")) {
18✔
1900
                        if (isSelected) {
3✔
1901
                            setSelectedRows(selectedRows.remove(row), undefined, true);
1✔
1902
                        } else {
2✔
1903
                            setSelectedRows(undefined, row, true);
2✔
1904
                            lastSelectedRowRef.current = row;
2✔
1905
                        }
2✔
1906
                    } else if (isSelected && selectedRows.length === 1) {
16✔
1907
                        setSelectedRows(CompactSelection.empty(), undefined, isMultiKey);
1✔
1908
                    } else {
13✔
1909
                        setSelectedRows(CompactSelection.fromSingleSelection(row), undefined, isMultiKey);
12✔
1910
                        lastSelectedRowRef.current = row;
12✔
1911
                    }
12✔
1912
                } else if (col >= rowMarkerOffset && showTrailingBlankRow && row === rows) {
118✔
1913
                    const customTargetColumn = getCustomNewRowTargetColumn(col);
1✔
1914
                    void appendRow(customTargetColumn ?? col);
1✔
1915
                } else {
100✔
1916
                    if (cellCol !== col || cellRow !== row) {
99✔
1917
                        const cell = getMangledCellContent(args.location);
92✔
1918
                        const renderer = getCellRenderer(cell);
92✔
1919

92✔
1920
                        if (renderer?.onSelect !== undefined) {
92✔
1921
                            let prevented = false;
7✔
1922
                            renderer.onSelect({
7✔
1923
                                ...args,
7✔
1924
                                cell,
7✔
1925
                                posX: args.localEventX,
7✔
1926
                                posY: args.localEventY,
7✔
1927
                                bounds: args.bounds,
7✔
1928
                                preventDefault: () => (prevented = true),
7✔
1929
                                theme: themeForCell(cell, args.location),
7✔
1930
                            });
7✔
1931
                            if (prevented) {
7✔
1932
                                return;
4✔
1933
                            }
4✔
1934
                        }
7✔
1935

88✔
1936
                        if (rowGroupingNavBehavior === "block" && mapper(row).isGroupHeader) {
92!
1937
                            return;
×
1938
                        }
✔
1939

88✔
1940
                        const isLastStickyRow = lastRowSticky && row === rows;
88✔
1941

92✔
1942
                        const startedFromLastSticky =
92✔
1943
                            lastRowSticky && gridSelection !== undefined && gridSelection.current?.cell[1] === rows;
92✔
1944

92✔
1945
                        if (
92✔
1946
                            (args.shiftKey || args.isLongTouch === true) &&
92✔
1947
                            cellCol !== undefined &&
7✔
1948
                            cellRow !== undefined &&
7✔
1949
                            gridSelection.current !== undefined &&
7✔
1950
                            !startedFromLastSticky
7✔
1951
                        ) {
92✔
1952
                            if (isLastStickyRow) {
7!
1953
                                // If we're making a selection and shift click in to the last sticky row,
×
1954
                                // just drop the event. Don't kill the selection.
×
1955
                                return;
×
1956
                            }
×
1957

7✔
1958
                            const left = Math.min(col, cellCol);
7✔
1959
                            const right = Math.max(col, cellCol);
7✔
1960
                            const top = Math.min(row, cellRow);
7✔
1961
                            const bottom = Math.max(row, cellRow);
7✔
1962
                            setCurrent(
7✔
1963
                                {
7✔
1964
                                    ...gridSelection.current,
7✔
1965
                                    range: {
7✔
1966
                                        x: left,
7✔
1967
                                        y: top,
7✔
1968
                                        width: right - left + 1,
7✔
1969
                                        height: bottom - top + 1,
7✔
1970
                                    },
7✔
1971
                                },
7✔
1972
                                true,
7✔
1973
                                isMultiKey,
7✔
1974
                                "click"
7✔
1975
                            );
7✔
1976
                            lastSelectedRowRef.current = undefined;
7✔
1977
                            focus();
7✔
1978
                        } else {
92✔
1979
                            setCurrent(
81✔
1980
                                {
81✔
1981
                                    cell: [col, row],
81✔
1982
                                    range: { x: col, y: row, width: 1, height: 1 },
81✔
1983
                                },
81✔
1984
                                true,
81✔
1985
                                isMultiKey,
81✔
1986
                                "click"
81✔
1987
                            );
81✔
1988
                            lastSelectedRowRef.current = undefined;
81✔
1989
                            setOverlay(undefined);
81✔
1990
                            focus();
81✔
1991
                        }
81✔
1992
                    }
92✔
1993
                }
99✔
1994
            } else if (args.kind === "header") {
136✔
1995
                lastMouseSelectLocation.current = [col, row];
13✔
1996
                setOverlay(undefined);
13✔
1997
                if (hasRowMarkers && col === 0) {
13✔
1998
                    lastSelectedRowRef.current = undefined;
3✔
1999
                    lastSelectedColRef.current = undefined;
3✔
2000
                    if (!headerRowMarkerDisabled && rowSelect === "multi") {
3✔
2001
                        if (selectedRows.length !== rows) {
3✔
2002
                            setSelectedRows(CompactSelection.fromSingleSelection([0, rows]), undefined, isMultiKey);
2✔
2003
                        } else {
3✔
2004
                            setSelectedRows(CompactSelection.empty(), undefined, isMultiKey);
1✔
2005
                        }
1✔
2006
                        focus();
3✔
2007
                    }
3✔
2008
                } else {
13✔
2009
                    const lastCol = lastSelectedColRef.current;
10✔
2010
                    if (
10✔
2011
                        columnSelect === "multi" &&
10✔
2012
                        (args.shiftKey || args.isLongTouch === true) &&
9!
2013
                        lastCol !== undefined &&
×
2014
                        selectedColumns.hasIndex(lastCol)
×
2015
                    ) {
10!
NEW
2016
                        // Support for selecting a slice of columns:
×
2017
                        const newSlice: Slice = [Math.min(lastCol, col), Math.max(lastCol, col) + 1];
×
2018

×
NEW
2019
                        if (isMultiKey || columnSelectionMode === "multi") {
×
2020
                            setSelectedColumns(undefined, newSlice, isMultiKey);
×
2021
                        } else {
×
2022
                            setSelectedColumns(CompactSelection.fromSingleSelection(newSlice), undefined, isMultiKey);
×
2023
                        }
×
2024
                    } else if (columnSelect === "multi" && (isMultiKey || columnSelectionMode === "multi")) {
10✔
2025
                        // Support for selecting a single columns additively:
1✔
2026
                        if (selectedColumns.hasIndex(col)) {
1!
2027
                            // If the column is already selected, deselect that column:
×
2028
                            setSelectedColumns(selectedColumns.remove(col), undefined, isMultiKey);
×
2029
                        } else {
1✔
2030
                            setSelectedColumns(undefined, col, isMultiKey);
1✔
2031
                        }
1✔
2032
                        lastSelectedColRef.current = col;
1✔
2033
                    } else if (columnSelect !== "none") {
10✔
2034
                        if (selectedColumns.hasIndex(col)) {
9!
NEW
2035
                            // If the column is already selected, deselect that column:
×
UNCOV
2036
                            setSelectedColumns(selectedColumns.remove(col), undefined, isMultiKey);
×
2037
                        } else {
9✔
2038
                            setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, isMultiKey);
9✔
2039
                        }
9✔
2040
                        lastSelectedColRef.current = col;
9✔
2041
                    }
9✔
2042
                    lastSelectedRowRef.current = undefined;
10✔
2043
                    focus();
10✔
2044
                }
10✔
2045
            } else if (args.kind === groupHeaderKind) {
18✔
2046
                lastMouseSelectLocation.current = [col, row];
4✔
2047
            } else if (args.kind === outOfBoundsKind && !args.isMaybeScrollbar) {
5✔
2048
                setGridSelection(emptyGridSelection, false);
1✔
2049
                setOverlay(undefined);
1✔
2050
                focus();
1✔
2051
                onSelectionCleared?.();
1!
2052
                lastSelectedRowRef.current = undefined;
1✔
2053
                lastSelectedColRef.current = undefined;
1✔
2054
            }
1✔
2055
        },
136✔
2056
        [
771✔
2057
            rowSelect,
771✔
2058
            columnSelect,
771✔
2059
            gridSelection,
771✔
2060
            hasRowMarkers,
771✔
2061
            rowMarkerOffset,
771✔
2062
            showTrailingBlankRow,
771✔
2063
            rows,
771✔
2064
            rowMarkers,
771✔
2065
            getMangledCellContent,
771✔
2066
            onRowMoved,
771✔
2067
            focus,
771✔
2068
            rowSelectionMode,
771✔
2069
            columnSelectionMode,
771✔
2070
            getCellRenderer,
771✔
2071
            themeForCell,
771✔
2072
            setSelectedRows,
771✔
2073
            getCustomNewRowTargetColumn,
771✔
2074
            appendRow,
771✔
2075
            rowGroupingNavBehavior,
771✔
2076
            mapper,
771✔
2077
            lastRowSticky,
771✔
2078
            setCurrent,
771✔
2079
            headerRowMarkerDisabled,
771✔
2080
            setSelectedColumns,
771✔
2081
            setGridSelection,
771✔
2082
            onSelectionCleared,
771✔
2083
        ]
771✔
2084
    );
771✔
2085
    const isActivelyDraggingHeader = React.useRef(false);
771✔
2086
    const lastMouseSelectLocation = React.useRef<readonly [number, number]>();
771✔
2087
    const touchDownArgs = React.useRef(visibleRegion);
771✔
2088
    const mouseDownData = React.useRef<{
771✔
2089
        time: number;
771✔
2090
        button: number;
771✔
2091
        location: Item;
771✔
2092
    }>();
771✔
2093
    const onMouseDown = React.useCallback(
771✔
2094
        (args: GridMouseEventArgs) => {
771✔
2095
            isPrevented.current = false;
153✔
2096
            touchDownArgs.current = visibleRegionRef.current;
153✔
2097
            if (args.button !== 0 && args.button !== 1) {
153✔
2098
                mouseDownData.current = undefined;
1✔
2099
                return;
1✔
2100
            }
1✔
2101

152✔
2102
            const time = performance.now();
152✔
2103
            mouseDownData.current = {
152✔
2104
                button: args.button,
152✔
2105
                time,
152✔
2106
                location: args.location,
152✔
2107
            };
152✔
2108

152✔
2109
            if (args?.kind === "header") {
153✔
2110
                isActivelyDraggingHeader.current = true;
19✔
2111
            }
19✔
2112

152✔
2113
            const fh = args.kind === "cell" && args.isFillHandle;
153✔
2114

153✔
2115
            if (!fh && args.kind !== "cell" && args.isEdge) return;
153✔
2116

145✔
2117
            setMouseState({
145✔
2118
                previousSelection: gridSelection,
145✔
2119
                fillHandle: fh,
145✔
2120
            });
145✔
2121
            lastMouseSelectLocation.current = undefined;
145✔
2122

145✔
2123
            if (!args.isTouch && args.button === 0 && !fh) {
153✔
2124
                handleSelect(args);
136✔
2125
            } else if (!args.isTouch && args.button === 1) {
146✔
2126
                lastMouseSelectLocation.current = args.location;
4✔
2127
            }
4✔
2128
        },
153✔
2129
        [gridSelection, handleSelect]
771✔
2130
    );
771✔
2131

771✔
2132
    const [renameGroup, setRenameGroup] = React.useState<{
771✔
2133
        group: string;
771✔
2134
        bounds: Rectangle;
771✔
2135
    }>();
771✔
2136

771✔
2137
    const handleGroupHeaderSelection = React.useCallback(
771✔
2138
        (args: GridMouseEventArgs) => {
771✔
2139
            if (args.kind !== groupHeaderKind || columnSelect !== "multi") {
4!
2140
                return;
×
2141
            }
×
2142
            const isMultiKey = browserIsOSX.value ? args.metaKey : args.ctrlKey;
4!
2143
            const [col] = args.location;
4✔
2144
            const selectedColumns = gridSelection.columns;
4✔
2145

4✔
2146
            if (col < rowMarkerOffset) return;
4!
2147

4✔
2148
            const needle = mangledCols[col];
4✔
2149
            let start = col;
4✔
2150
            let end = col;
4✔
2151
            for (let i = col - 1; i >= rowMarkerOffset; i--) {
4✔
2152
                if (!isGroupEqual(needle.group, mangledCols[i].group)) break;
4!
2153
                start--;
4✔
2154
            }
4✔
2155

4✔
2156
            for (let i = col + 1; i < mangledCols.length; i++) {
4✔
2157
                if (!isGroupEqual(needle.group, mangledCols[i].group)) break;
36!
2158
                end++;
36✔
2159
            }
36✔
2160

4✔
2161
            focus();
4✔
2162

4✔
2163
            if (isMultiKey) {
4✔
2164
                if (selectedColumns.hasAll([start, end + 1])) {
2✔
2165
                    let newVal = selectedColumns;
1✔
2166
                    for (let index = start; index <= end; index++) {
1✔
2167
                        newVal = newVal.remove(index);
11✔
2168
                    }
11✔
2169
                    setSelectedColumns(newVal, undefined, isMultiKey);
1✔
2170
                } else {
1✔
2171
                    setSelectedColumns(undefined, [start, end + 1], isMultiKey);
1✔
2172
                }
1✔
2173
            } else {
2✔
2174
                setSelectedColumns(CompactSelection.fromSingleSelection([start, end + 1]), undefined, isMultiKey);
2✔
2175
            }
2✔
2176
        },
4✔
2177
        [columnSelect, focus, gridSelection.columns, mangledCols, rowMarkerOffset, setSelectedColumns]
771✔
2178
    );
771✔
2179

771✔
2180
    const isPrevented = React.useRef(false);
771✔
2181

771✔
2182
    const normalSizeColumn = React.useCallback(
771✔
2183
        async (col: number): Promise<void> => {
771✔
2184
            if (getCellsForSelection !== undefined && onColumnResize !== undefined) {
2✔
2185
                const start = visibleRegionRef.current.y;
2✔
2186
                const end = visibleRegionRef.current.height;
2✔
2187
                let cells = getCellsForSelection(
2✔
2188
                    {
2✔
2189
                        x: col,
2✔
2190
                        y: start,
2✔
2191
                        width: 1,
2✔
2192
                        height: Math.min(end, rows - start),
2✔
2193
                    },
2✔
2194
                    abortControllerRef.current.signal
2✔
2195
                );
2✔
2196
                if (typeof cells !== "object") {
2!
2197
                    cells = await cells();
×
2198
                }
×
2199
                const inputCol = columns[col - rowMarkerOffset];
2✔
2200
                const offscreen = document.createElement("canvas");
2✔
2201
                const ctx = offscreen.getContext("2d", { alpha: false });
2✔
2202
                if (ctx !== null) {
2✔
2203
                    ctx.font = mergedTheme.baseFontFull;
2✔
2204
                    const newCol = measureColumn(
2✔
2205
                        ctx,
2✔
2206
                        mergedTheme,
2✔
2207
                        inputCol,
2✔
2208
                        0,
2✔
2209
                        cells,
2✔
2210
                        minColumnWidth,
2✔
2211
                        maxColumnWidth,
2✔
2212
                        false,
2✔
2213
                        getCellRenderer
2✔
2214
                    );
2✔
2215
                    onColumnResize?.(inputCol, newCol.width, col, newCol.width);
2✔
2216
                }
2✔
2217
            }
2✔
2218
        },
2✔
2219
        [
771✔
2220
            columns,
771✔
2221
            getCellsForSelection,
771✔
2222
            maxColumnWidth,
771✔
2223
            mergedTheme,
771✔
2224
            minColumnWidth,
771✔
2225
            onColumnResize,
771✔
2226
            rowMarkerOffset,
771✔
2227
            rows,
771✔
2228
            getCellRenderer,
771✔
2229
        ]
771✔
2230
    );
771✔
2231

771✔
2232
    const [scrollDir, setScrollDir] = React.useState<GridMouseEventArgs["scrollEdge"]>();
771✔
2233

771✔
2234
    const fillPattern = React.useCallback(
771✔
2235
        async (previousSelection: GridSelection, currentSelection: GridSelection) => {
771✔
2236
            const patternRange = previousSelection.current?.range;
7✔
2237

7✔
2238
            if (
7✔
2239
                patternRange === undefined ||
7✔
2240
                getCellsForSelection === undefined ||
7✔
2241
                currentSelection.current === undefined
7✔
2242
            ) {
7!
2243
                return;
×
2244
            }
×
2245
            const currentRange = currentSelection.current.range;
7✔
2246

7✔
2247
            if (onFillPattern !== undefined) {
7✔
2248
                let canceled = false;
1✔
2249
                onFillPattern({
1✔
2250
                    fillDestination: { ...currentRange, x: currentRange.x - rowMarkerOffset },
1✔
2251
                    patternSource: { ...patternRange, x: patternRange.x - rowMarkerOffset },
1✔
2252
                    preventDefault: () => (canceled = true),
1✔
2253
                });
1✔
2254
                if (canceled) return;
1!
2255
            }
1✔
2256

7✔
2257
            let cells = getCellsForSelection(patternRange, abortControllerRef.current.signal);
7✔
2258
            if (typeof cells !== "object") cells = await cells();
7!
2259

7✔
2260
            const pattern = cells;
7✔
2261

7✔
2262
            // loop through all cells in currentSelection.current.range
7✔
2263
            const editItemList: EditListItem[] = [];
7✔
2264
            for (let x = 0; x < currentRange.width; x++) {
7✔
2265
                for (let y = 0; y < currentRange.height; y++) {
9✔
2266
                    const cell: Item = [currentRange.x + x, currentRange.y + y];
41✔
2267
                    if (itemIsInRect(cell, patternRange)) continue;
41✔
2268
                    const patternCell = pattern[y % patternRange.height][x % patternRange.width];
29✔
2269
                    if (isInnerOnlyCell(patternCell) || !isReadWriteCell(patternCell)) continue;
41!
2270
                    editItemList.push({
29✔
2271
                        location: cell,
29✔
2272
                        value: { ...patternCell },
29✔
2273
                    });
29✔
2274
                }
29✔
2275
            }
9✔
2276
            mangledOnCellsEdited(editItemList);
7✔
2277

7✔
2278
            gridRef.current?.damage(
7✔
2279
                editItemList.map(c => ({
7✔
2280
                    cell: c.location,
29✔
2281
                }))
7✔
2282
            );
7✔
2283
        },
7✔
2284
        [getCellsForSelection, mangledOnCellsEdited, onFillPattern, rowMarkerOffset]
771✔
2285
    );
771✔
2286

771✔
2287
    const fillRight = React.useCallback(() => {
771✔
2288
        if (gridSelection.current === undefined || gridSelection.current.range.width <= 1) return;
1!
2289

1✔
2290
        const firstColSelection = {
1✔
2291
            ...gridSelection,
1✔
2292
            current: {
1✔
2293
                ...gridSelection.current,
1✔
2294
                range: {
1✔
2295
                    ...gridSelection.current.range,
1✔
2296
                    width: 1,
1✔
2297
                },
1✔
2298
            },
1✔
2299
        };
1✔
2300

1✔
2301
        void fillPattern(firstColSelection, gridSelection);
1✔
2302
    }, [fillPattern, gridSelection]);
771✔
2303

771✔
2304
    const fillDown = React.useCallback(() => {
771✔
2305
        if (gridSelection.current === undefined || gridSelection.current.range.height <= 1) return;
1!
2306

1✔
2307
        const firstRowSelection = {
1✔
2308
            ...gridSelection,
1✔
2309
            current: {
1✔
2310
                ...gridSelection.current,
1✔
2311
                range: {
1✔
2312
                    ...gridSelection.current.range,
1✔
2313
                    height: 1,
1✔
2314
                },
1✔
2315
            },
1✔
2316
        };
1✔
2317

1✔
2318
        void fillPattern(firstRowSelection, gridSelection);
1✔
2319
    }, [fillPattern, gridSelection]);
771✔
2320

771✔
2321
    const onMouseUp = React.useCallback(
771✔
2322
        (args: GridMouseEventArgs, isOutside: boolean) => {
771✔
2323
            const mouse = mouseState;
153✔
2324
            setMouseState(undefined);
153✔
2325
            setFillHighlightRegion(undefined);
153✔
2326
            setScrollDir(undefined);
153✔
2327
            isActivelyDraggingHeader.current = false;
153✔
2328

153✔
2329
            if (isOutside) return;
153✔
2330

152✔
2331
            if (
152✔
2332
                mouse?.fillHandle === true &&
153✔
2333
                gridSelection.current !== undefined &&
5✔
2334
                mouse.previousSelection?.current !== undefined
5✔
2335
            ) {
153✔
2336
                if (fillHighlightRegion === undefined) return;
5!
2337
                const newRange = {
5✔
2338
                    ...gridSelection,
5✔
2339
                    current: {
5✔
2340
                        ...gridSelection.current,
5✔
2341
                        range: combineRects(mouse.previousSelection.current.range, fillHighlightRegion),
5✔
2342
                    },
5✔
2343
                };
5✔
2344
                void fillPattern(mouse.previousSelection, newRange);
5✔
2345
                setGridSelection(newRange, true);
5✔
2346
                return;
5✔
2347
            }
5✔
2348

147✔
2349
            const [col, row] = args.location;
147✔
2350
            const [lastMouseDownCol, lastMouseDownRow] = lastMouseSelectLocation.current ?? [];
153✔
2351

153✔
2352
            const preventDefault = () => {
153✔
2353
                isPrevented.current = true;
×
2354
            };
×
2355

153✔
2356
            const handleMaybeClick = (a: GridMouseCellEventArgs): boolean => {
153✔
2357
                const isValidClick = a.isTouch || (lastMouseDownCol === col && lastMouseDownRow === row);
120✔
2358
                if (isValidClick) {
120✔
2359
                    onCellClicked?.([col - rowMarkerOffset, row], {
109✔
2360
                        ...a,
4✔
2361
                        preventDefault,
4✔
2362
                    });
4✔
2363
                }
109✔
2364
                if (a.button === 1) return !isPrevented.current;
120✔
2365
                if (!isPrevented.current) {
117✔
2366
                    const c = getMangledCellContent(args.location);
117✔
2367
                    const r = getCellRenderer(c);
117✔
2368
                    if (r !== undefined && r.onClick !== undefined && isValidClick) {
117✔
2369
                        const newVal = r.onClick({
23✔
2370
                            ...a,
23✔
2371
                            cell: c,
23✔
2372
                            posX: a.localEventX,
23✔
2373
                            posY: a.localEventY,
23✔
2374
                            bounds: a.bounds,
23✔
2375
                            theme: themeForCell(c, args.location),
23✔
2376
                            preventDefault,
23✔
2377
                        });
23✔
2378
                        if (newVal !== undefined && !isInnerOnlyCell(newVal) && isEditableGridCell(newVal)) {
23✔
2379
                            mangledOnCellsEdited([{ location: a.location, value: newVal }]);
4✔
2380
                            gridRef.current?.damage([
4✔
2381
                                {
4✔
2382
                                    cell: a.location,
4✔
2383
                                },
4✔
2384
                            ]);
4✔
2385
                        }
4✔
2386
                    }
23✔
2387
                    if (isPrevented.current || gridSelection.current === undefined) return false;
117✔
2388

102✔
2389
                    let shouldActivate = false;
102✔
2390
                    switch (c.activationBehaviorOverride ?? cellActivationBehavior) {
117✔
2391
                        case "double-click":
117✔
2392
                        case "second-click": {
117✔
2393
                            if (mouse?.previousSelection?.current?.cell === undefined) break;
101✔
2394
                            const [selectedCol, selectedRow] = gridSelection.current.cell;
24✔
2395
                            const [prevCol, prevRow] = mouse.previousSelection.current.cell;
24✔
2396
                            const isClickOnSelected =
24✔
2397
                                col === selectedCol && col === prevCol && row === selectedRow && row === prevRow;
101✔
2398
                            shouldActivate =
101✔
2399
                                isClickOnSelected &&
101✔
2400
                                (a.isDoubleClick === true || cellActivationBehavior === "second-click");
7✔
2401
                            break;
101✔
2402
                        }
101✔
2403
                        case "single-click": {
117✔
2404
                            shouldActivate = true;
1✔
2405
                            break;
1✔
2406
                        }
1✔
2407
                    }
117✔
2408
                    if (shouldActivate) {
117✔
2409
                        const act =
7✔
2410
                            a.isDoubleClick === true
7✔
2411
                                ? "double-click"
5✔
2412
                                : (c.activationBehaviorOverride ?? cellActivationBehavior);
2✔
2413
                        const activationEvent: CellActivatedEventArgs = {
7✔
2414
                            inputType: "pointer",
7✔
2415
                            pointerActivation: act,
7✔
2416
                            pointerType: a.isTouch ? "touch" : "mouse",
7!
2417
                        };
7✔
2418
                        onCellActivated?.([col - rowMarkerOffset, row], activationEvent);
7✔
2419
                        reselect(a.bounds, activationEvent);
7✔
2420
                        return true;
7✔
2421
                    }
7✔
2422
                }
117✔
2423
                return false;
95✔
2424
            };
120✔
2425

153✔
2426
            const clickLocation = args.location[0] - rowMarkerOffset;
153✔
2427
            if (args.isTouch) {
153!
2428
                const vr = visibleRegionRef.current;
×
2429
                const touchVr = touchDownArgs.current;
×
2430
                if (vr.x !== touchVr.x || vr.y !== touchVr.y) {
×
2431
                    // we scrolled, abort
×
2432
                    return;
×
2433
                }
×
2434
                // take care of context menus first if long pressed item is already selected
×
2435
                if (args.isLongTouch === true) {
×
2436
                    if (args.kind === "cell" && itemsAreEqual(gridSelection.current?.cell, args.location)) {
×
2437
                        onCellContextMenu?.([clickLocation, args.location[1]], {
×
2438
                            ...args,
×
2439
                            preventDefault,
×
2440
                        });
×
2441
                        return;
×
2442
                    } else if (args.kind === "header" && gridSelection.columns.hasIndex(col)) {
×
2443
                        onHeaderContextMenu?.(clickLocation, { ...args, preventDefault });
×
2444
                        return;
×
2445
                    } else if (args.kind === groupHeaderKind) {
×
2446
                        if (clickLocation < 0) {
×
2447
                            return;
×
2448
                        }
×
2449

×
2450
                        onGroupHeaderContextMenu?.(clickLocation, { ...args, preventDefault });
×
2451
                        return;
×
2452
                    }
×
2453
                }
×
2454
                if (args.kind === "cell") {
×
2455
                    // click that cell
×
2456
                    if (!handleMaybeClick(args)) {
×
2457
                        handleSelect(args);
×
2458
                    }
×
2459
                } else if (args.kind === groupHeaderKind) {
×
2460
                    onGroupHeaderClicked?.(clickLocation, { ...args, preventDefault });
×
2461
                } else {
×
2462
                    if (args.kind === headerKind) {
×
2463
                        onHeaderClicked?.(clickLocation, {
×
2464
                            ...args,
×
2465
                            preventDefault,
×
2466
                        });
×
2467
                    }
×
2468
                    handleSelect(args);
×
2469
                }
×
2470
                return;
×
2471
            }
✔
2472

147✔
2473
            if (args.kind === "header") {
153✔
2474
                if (clickLocation < 0) {
19✔
2475
                    return;
3✔
2476
                }
3✔
2477

16✔
2478
                if (args.isEdge) {
19✔
2479
                    if (args.isDoubleClick === true) {
2✔
2480
                        void normalSizeColumn(col);
1✔
2481
                    }
1✔
2482
                } else if (args.button === 0 && col === lastMouseDownCol && row === lastMouseDownRow) {
19✔
2483
                    onHeaderClicked?.(clickLocation, { ...args, preventDefault });
8✔
2484
                }
8✔
2485
            }
19✔
2486

144✔
2487
            if (args.kind === groupHeaderKind) {
153✔
2488
                if (clickLocation < 0) {
4!
2489
                    return;
×
2490
                }
×
2491

4✔
2492
                if (args.button === 0 && col === lastMouseDownCol && row === lastMouseDownRow) {
4✔
2493
                    onGroupHeaderClicked?.(clickLocation, { ...args, preventDefault });
4✔
2494
                    if (!isPrevented.current) {
4✔
2495
                        handleGroupHeaderSelection(args);
4✔
2496
                    }
4✔
2497
                }
4✔
2498
            }
4✔
2499

144✔
2500
            if (args.kind === "cell" && (args.button === 0 || args.button === 1)) {
153✔
2501
                handleMaybeClick(args);
120✔
2502
            }
120✔
2503

144✔
2504
            lastMouseSelectLocation.current = undefined;
144✔
2505
        },
153✔
2506
        [
771✔
2507
            mouseState,
771✔
2508
            gridSelection,
771✔
2509
            rowMarkerOffset,
771✔
2510
            fillHighlightRegion,
771✔
2511
            fillPattern,
771✔
2512
            setGridSelection,
771✔
2513
            onCellClicked,
771✔
2514
            getMangledCellContent,
771✔
2515
            getCellRenderer,
771✔
2516
            cellActivationBehavior,
771✔
2517
            themeForCell,
771✔
2518
            mangledOnCellsEdited,
771✔
2519
            onCellActivated,
771✔
2520
            reselect,
771✔
2521
            onCellContextMenu,
771✔
2522
            onHeaderContextMenu,
771✔
2523
            onGroupHeaderContextMenu,
771✔
2524
            handleSelect,
771✔
2525
            onGroupHeaderClicked,
771✔
2526
            onHeaderClicked,
771✔
2527
            normalSizeColumn,
771✔
2528
            handleGroupHeaderSelection,
771✔
2529
        ]
771✔
2530
    );
771✔
2531

771✔
2532
    const onMouseMoveImpl = React.useCallback(
771✔
2533
        (args: GridMouseEventArgs) => {
771✔
2534
            const a: GridMouseEventArgs = {
42✔
2535
                ...args,
42✔
2536
                location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
42✔
2537
            };
42✔
2538
            onMouseMove?.(a);
42✔
2539

42✔
2540
            if (mouseState !== undefined && args.buttons === 0) {
42✔
2541
                setMouseState(undefined);
8✔
2542
                setFillHighlightRegion(undefined);
8✔
2543
                setScrollDir(undefined);
8✔
2544
                isActivelyDraggingHeader.current = false;
8✔
2545
            }
8✔
2546

42✔
2547
            setScrollDir(cv => {
42✔
2548
                if (isActivelyDraggingHeader.current) return [args.scrollEdge[0], 0];
42✔
2549
                if (args.scrollEdge[0] === cv?.[0] && args.scrollEdge[1] === cv[1]) return cv;
42✔
2550
                return mouseState === undefined || (mouseDownData.current?.location[0] ?? 0) < rowMarkerOffset
42!
2551
                    ? undefined
15✔
2552
                    : args.scrollEdge;
17✔
2553
            });
42✔
2554
        },
42✔
2555
        [mouseState, onMouseMove, rowMarkerOffset]
771✔
2556
    );
771✔
2557

771✔
2558
    const onHeaderMenuClickInner = React.useCallback(
771✔
2559
        (col: number, screenPosition: Rectangle) => {
771✔
2560
            onHeaderMenuClick?.(col - rowMarkerOffset, screenPosition);
1✔
2561
        },
1✔
2562
        [onHeaderMenuClick, rowMarkerOffset]
771✔
2563
    );
771✔
2564

771✔
2565
    const onHeaderIndicatorClickInner = React.useCallback(
771✔
2566
        (col: number, screenPosition: Rectangle) => {
771✔
2567
            onHeaderIndicatorClick?.(col - rowMarkerOffset, screenPosition);
×
2568
        },
×
2569
        [onHeaderIndicatorClick, rowMarkerOffset]
771✔
2570
    );
771✔
2571

771✔
2572
    const currentCell = gridSelection?.current?.cell;
771✔
2573
    const onVisibleRegionChangedImpl = React.useCallback(
771✔
2574
        (
771✔
2575
            region: Rectangle,
162✔
2576
            clientWidth: number,
162✔
2577
            clientHeight: number,
162✔
2578
            rightElWidth: number,
162✔
2579
            tx: number,
162✔
2580
            ty: number
162✔
2581
        ) => {
162✔
2582
            hasJustScrolled.current = false;
162✔
2583
            let selected = currentCell;
162✔
2584
            if (selected !== undefined) {
162✔
2585
                selected = [selected[0] - rowMarkerOffset, selected[1]];
15✔
2586
            }
15✔
2587

162✔
2588
            const freezeRegion =
162✔
2589
                freezeColumns === 0
162✔
2590
                    ? undefined
161✔
2591
                    : {
1✔
2592
                          x: 0,
1✔
2593
                          y: region.y,
1✔
2594
                          width: freezeColumns,
1✔
2595
                          height: region.height,
1✔
2596
                      };
1✔
2597

162✔
2598
            const freezeRegions: Rectangle[] = [];
162✔
2599
            if (freezeRegion !== undefined) freezeRegions.push(freezeRegion);
162✔
2600
            if (freezeTrailingRows > 0) {
162✔
2601
                freezeRegions.push({
1✔
2602
                    x: region.x - rowMarkerOffset,
1✔
2603
                    y: rows - freezeTrailingRows,
1✔
2604
                    width: region.width,
1✔
2605
                    height: freezeTrailingRows,
1✔
2606
                });
1✔
2607

1✔
2608
                if (freezeColumns > 0) {
1✔
2609
                    freezeRegions.push({
1✔
2610
                        x: 0,
1✔
2611
                        y: rows - freezeTrailingRows,
1✔
2612
                        width: freezeColumns,
1✔
2613
                        height: freezeTrailingRows,
1✔
2614
                    });
1✔
2615
                }
1✔
2616
            }
1✔
2617

162✔
2618
            const newRegion = {
162✔
2619
                x: region.x - rowMarkerOffset,
162✔
2620
                y: region.y,
162✔
2621
                width: region.width,
162✔
2622
                height: showTrailingBlankRow && region.y + region.height >= rows ? region.height - 1 : region.height,
162✔
2623
                tx,
162✔
2624
                ty,
162✔
2625
                extras: {
162✔
2626
                    selected,
162✔
2627
                    freezeRegion,
162✔
2628
                    freezeRegions,
162✔
2629
                },
162✔
2630
            };
162✔
2631
            visibleRegionRef.current = newRegion;
162✔
2632
            setVisibleRegion(newRegion);
162✔
2633
            setClientSize([clientWidth, clientHeight, rightElWidth]);
162✔
2634
            onVisibleRegionChanged?.(newRegion, newRegion.tx, newRegion.ty, newRegion.extras);
162✔
2635
        },
162✔
2636
        [
771✔
2637
            currentCell,
771✔
2638
            rowMarkerOffset,
771✔
2639
            showTrailingBlankRow,
771✔
2640
            rows,
771✔
2641
            freezeColumns,
771✔
2642
            freezeTrailingRows,
771✔
2643
            setVisibleRegion,
771✔
2644
            onVisibleRegionChanged,
771✔
2645
        ]
771✔
2646
    );
771✔
2647

771✔
2648
    const onColumnProposeMoveImpl = whenDefined(
771✔
2649
        onColumnProposeMove,
771✔
2650
        React.useCallback(
771✔
2651
            (startIndex: number, endIndex: number) => {
771✔
2652
                return onColumnProposeMove?.(startIndex - rowMarkerOffset, endIndex - rowMarkerOffset) !== false;
×
2653
            },
×
2654
            [onColumnProposeMove, rowMarkerOffset]
771✔
2655
        )
771✔
2656
    );
771✔
2657

771✔
2658
    const onColumnMovedImpl = whenDefined(
771✔
2659
        onColumnMoved,
771✔
2660
        React.useCallback(
771✔
2661
            (startIndex: number, endIndex: number) => {
771✔
2662
                onColumnMoved?.(startIndex - rowMarkerOffset, endIndex - rowMarkerOffset);
1✔
2663
                if (columnSelect !== "none") {
1✔
2664
                    setSelectedColumns(CompactSelection.fromSingleSelection(endIndex), undefined, true);
1✔
2665
                }
1✔
2666
            },
1✔
2667
            [columnSelect, onColumnMoved, rowMarkerOffset, setSelectedColumns]
771✔
2668
        )
771✔
2669
    );
771✔
2670

771✔
2671
    const isActivelyDragging = React.useRef(false);
771✔
2672
    const onDragStartImpl = React.useCallback(
771✔
2673
        (args: GridDragEventArgs) => {
771✔
2674
            if (args.location[0] === 0 && rowMarkerOffset > 0) {
1!
2675
                args.preventDefault();
×
2676
                return;
×
2677
            }
×
2678
            onDragStart?.({
1✔
2679
                ...args,
1✔
2680
                location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
1✔
2681
            });
1✔
2682

1✔
2683
            if (!args.defaultPrevented()) {
1✔
2684
                isActivelyDragging.current = true;
1✔
2685
            }
1✔
2686
            setMouseState(undefined);
1✔
2687
        },
1✔
2688
        [onDragStart, rowMarkerOffset]
771✔
2689
    );
771✔
2690

771✔
2691
    const onDragEnd = React.useCallback(() => {
771✔
2692
        isActivelyDragging.current = false;
×
2693
    }, []);
771✔
2694

771✔
2695
    const rowGroupingSelectionBehavior = rowGrouping?.selectionBehavior;
771!
2696

771✔
2697
    const getSelectionRowLimits = React.useCallback(
771✔
2698
        (selectedRow: number): readonly [number, number] | undefined => {
771✔
2699
            if (rowGroupingSelectionBehavior !== "block-spanning") return undefined;
16!
2700

×
2701
            const { isGroupHeader, path, groupRows } = mapper(selectedRow);
×
2702

×
2703
            if (isGroupHeader) {
×
2704
                return [selectedRow, selectedRow];
×
2705
            }
×
2706

×
2707
            const groupRowIndex = path[path.length - 1];
×
2708
            const lowerBounds = selectedRow - groupRowIndex;
×
2709
            const upperBounds = selectedRow + groupRows - groupRowIndex - 1;
×
2710

×
2711
            return [lowerBounds, upperBounds];
×
2712
        },
16✔
2713
        [mapper, rowGroupingSelectionBehavior]
771✔
2714
    );
771✔
2715

771✔
2716
    const hoveredRef = React.useRef<GridMouseEventArgs>();
771✔
2717
    const onItemHoveredImpl = React.useCallback(
771✔
2718
        (args: GridMouseEventArgs) => {
771✔
2719
            // make sure we still have a button down
32✔
2720
            if (mouseEventArgsAreEqual(args, hoveredRef.current)) return;
32!
2721
            hoveredRef.current = args;
32✔
2722
            if (mouseDownData?.current?.button !== undefined && mouseDownData.current.button >= 1) return;
32✔
2723
            if (
31✔
2724
                args.buttons !== 0 &&
31✔
2725
                mouseState !== undefined &&
15✔
2726
                mouseDownData.current?.location[0] === 0 &&
15✔
2727
                rowMarkerOffset === 1 &&
2✔
2728
                rowSelect === "multi" &&
2✔
2729
                mouseState.previousSelection &&
2✔
2730
                !mouseState.previousSelection.rows.hasIndex(mouseDownData.current.location[1]) &&
2✔
2731
                gridSelection.rows.hasIndex(mouseDownData.current.location[1])
2✔
2732
            ) {
32✔
2733
                const start = Math.min(mouseDownData.current.location[1], args.location[1]);
2✔
2734
                const end = Math.max(mouseDownData.current.location[1], args.location[1]) + 1;
2✔
2735
                setSelectedRows(CompactSelection.fromSingleSelection([start, end]), undefined, false);
2✔
2736
            }
2✔
2737
            // Only handle rect selection if not already processed by row selection:
29✔
2738
            else if (
29✔
2739
                args.buttons !== 0 &&
29✔
2740
                mouseState !== undefined &&
13✔
2741
                gridSelection.current !== undefined &&
13✔
2742
                !isActivelyDragging.current &&
12✔
2743
                !isActivelyDraggingHeader.current &&
12✔
2744
                (rangeSelect === "rect" || rangeSelect === "multi-rect")
12!
2745
            ) {
29✔
2746
                const [selectedCol, selectedRow] = gridSelection.current.cell;
12✔
2747
                // eslint-disable-next-line prefer-const
12✔
2748
                let [col, row] = args.location;
12✔
2749

12✔
2750
                if (row < 0) {
12✔
2751
                    row = visibleRegionRef.current.y;
1✔
2752
                }
1✔
2753

12✔
2754
                if (mouseState.fillHandle === true && mouseState.previousSelection?.current !== undefined) {
12✔
2755
                    const prevRange = mouseState.previousSelection.current.range;
6✔
2756
                    row = Math.min(row, showTrailingBlankRow ? rows - 1 : rows);
6!
2757
                    const rect = getClosestRect(prevRange, col, row, allowedFillDirections);
6✔
2758
                    setFillHighlightRegion(rect);
6✔
2759
                } else {
6✔
2760
                    const startedFromLastStickyRow = showTrailingBlankRow && selectedRow === rows;
6✔
2761
                    if (startedFromLastStickyRow) return;
6!
2762

6✔
2763
                    const landedOnLastStickyRow = showTrailingBlankRow && row === rows;
6✔
2764
                    if (landedOnLastStickyRow) {
6!
2765
                        if (args.kind === outOfBoundsKind) row--;
×
2766
                        else return;
×
2767
                    }
×
2768

6✔
2769
                    col = Math.max(col, rowMarkerOffset);
6✔
2770
                    const clampLimits = getSelectionRowLimits(selectedRow);
6✔
2771
                    row = clampLimits === undefined ? row : clamp(row, clampLimits[0], clampLimits[1]);
6!
2772

6✔
2773
                    // FIXME: Restrict row based on rowGrouping.selectionBehavior here
6✔
2774

6✔
2775
                    const deltaX = col - selectedCol;
6✔
2776
                    const deltaY = row - selectedRow;
6✔
2777

6✔
2778
                    const newRange: Rectangle = {
6✔
2779
                        x: deltaX >= 0 ? selectedCol : col,
6!
2780
                        y: deltaY >= 0 ? selectedRow : row,
6✔
2781
                        width: Math.abs(deltaX) + 1,
6✔
2782
                        height: Math.abs(deltaY) + 1,
6✔
2783
                    };
6✔
2784

6✔
2785
                    setCurrent(
6✔
2786
                        {
6✔
2787
                            ...gridSelection.current,
6✔
2788
                            range: newRange,
6✔
2789
                        },
6✔
2790
                        true,
6✔
2791
                        false,
6✔
2792
                        "drag"
6✔
2793
                    );
6✔
2794
                }
6✔
2795
            }
12✔
2796

31✔
2797
            onItemHovered?.({ ...args, location: [args.location[0] - rowMarkerOffset, args.location[1]] as any });
32✔
2798
        },
32✔
2799
        [
771✔
2800
            mouseState,
771✔
2801
            rowMarkerOffset,
771✔
2802
            rowSelect,
771✔
2803
            gridSelection,
771✔
2804
            rangeSelect,
771✔
2805
            onItemHovered,
771✔
2806
            setSelectedRows,
771✔
2807
            showTrailingBlankRow,
771✔
2808
            rows,
771✔
2809
            allowedFillDirections,
771✔
2810
            getSelectionRowLimits,
771✔
2811
            setCurrent,
771✔
2812
        ]
771✔
2813
    );
771✔
2814

771✔
2815
    const adjustSelectionOnScroll = React.useCallback(() => {
771✔
2816
        const args = hoveredRef.current;
×
2817
        if (args === undefined) return;
×
2818
        const [xDir, yDir] = args.scrollEdge;
×
2819
        let [col, row] = args.location;
×
2820
        const visible = visibleRegionRef.current;
×
2821
        if (xDir === -1) {
×
2822
            col = visible.extras?.freezeRegion?.x ?? visible.x;
×
2823
        } else if (xDir === 1) {
×
2824
            col = visible.x + visible.width;
×
2825
        }
×
2826
        if (yDir === -1) {
×
2827
            row = Math.max(0, visible.y);
×
2828
        } else if (yDir === 1) {
×
2829
            row = Math.min(rows - 1, visible.y + visible.height);
×
2830
        }
×
2831
        col = clamp(col, 0, mangledCols.length - 1);
×
2832
        row = clamp(row, 0, rows - 1);
×
2833
        onItemHoveredImpl({
×
2834
            ...args,
×
2835
            location: [col, row] as any,
×
2836
        });
×
2837
    }, [mangledCols.length, onItemHoveredImpl, rows]);
771✔
2838

771✔
2839
    useAutoscroll(scrollDir, scrollRef, adjustSelectionOnScroll);
771✔
2840

771✔
2841
    // 1 === move one
771✔
2842
    // 2 === move to end
771✔
2843
    const adjustSelection = React.useCallback(
771✔
2844
        (direction: [0 | 1 | -1 | 2 | -2, 0 | 1 | -1 | 2 | -2]) => {
771✔
2845
            if (gridSelection.current === undefined) return;
10!
2846

10✔
2847
            const [x, y] = direction;
10✔
2848
            const [col, row] = gridSelection.current.cell;
10✔
2849
            const old = gridSelection.current.range;
10✔
2850
            let left = old.x;
10✔
2851
            let right = old.x + old.width;
10✔
2852
            let top = old.y;
10✔
2853
            let bottom = old.y + old.height;
10✔
2854

10✔
2855
            const [minRow, maxRowRaw] = getSelectionRowLimits(row) ?? [0, rows - 1];
10✔
2856
            const maxRow = maxRowRaw + 1; // we need an inclusive value
10✔
2857

10✔
2858
            // take care of vertical first in case new spans come in
10✔
2859
            if (y !== 0) {
10✔
2860
                switch (y) {
4✔
2861
                    case 2: {
4✔
2862
                        // go to end
2✔
2863
                        bottom = maxRow;
2✔
2864
                        top = row;
2✔
2865
                        scrollTo(0, bottom, "vertical");
2✔
2866

2✔
2867
                        break;
2✔
2868
                    }
2✔
2869
                    case -2: {
4✔
2870
                        // go to start
1✔
2871
                        top = minRow;
1✔
2872
                        bottom = row + 1;
1✔
2873
                        scrollTo(0, top, "vertical");
1✔
2874

1✔
2875
                        break;
1✔
2876
                    }
1✔
2877
                    case 1: {
4✔
2878
                        // motion down
1✔
2879
                        if (top < row) {
1!
2880
                            top++;
×
2881
                            scrollTo(0, top, "vertical");
×
2882
                        } else {
1✔
2883
                            bottom = Math.min(maxRow, bottom + 1);
1✔
2884
                            scrollTo(0, bottom, "vertical");
1✔
2885
                        }
1✔
2886

1✔
2887
                        break;
1✔
2888
                    }
1✔
2889
                    case -1: {
4!
2890
                        // motion up
×
2891
                        if (bottom > row + 1) {
×
2892
                            bottom--;
×
2893
                            scrollTo(0, bottom, "vertical");
×
2894
                        } else {
×
2895
                            top = Math.max(minRow, top - 1);
×
2896
                            scrollTo(0, top, "vertical");
×
2897
                        }
×
2898

×
2899
                        break;
×
2900
                    }
×
2901
                    default: {
4!
2902
                        assertNever(y);
×
2903
                    }
×
2904
                }
4✔
2905
            }
4✔
2906

10✔
2907
            if (x !== 0) {
10✔
2908
                if (x === 2) {
8✔
2909
                    right = mangledCols.length;
2✔
2910
                    left = col;
2✔
2911
                    scrollTo(right - 1 - rowMarkerOffset, 0, "horizontal");
2✔
2912
                } else if (x === -2) {
8✔
2913
                    left = rowMarkerOffset;
1✔
2914
                    right = col + 1;
1✔
2915
                    scrollTo(left - rowMarkerOffset, 0, "horizontal");
1✔
2916
                } else {
6✔
2917
                    let disallowed: number[] = [];
5✔
2918
                    if (getCellsForSelection !== undefined) {
5✔
2919
                        const cells = getCellsForSelection(
5✔
2920
                            {
5✔
2921
                                x: left,
5✔
2922
                                y: top,
5✔
2923
                                width: right - left - rowMarkerOffset,
5✔
2924
                                height: bottom - top,
5✔
2925
                            },
5✔
2926
                            abortControllerRef.current.signal
5✔
2927
                        );
5✔
2928

5✔
2929
                        if (typeof cells === "object") {
5✔
2930
                            disallowed = getSpanStops(cells);
5✔
2931
                        }
5✔
2932
                    }
5✔
2933
                    if (x === 1) {
5✔
2934
                        // motion right
4✔
2935
                        let done = false;
4✔
2936
                        if (left < col) {
4!
2937
                            if (disallowed.length > 0) {
×
2938
                                const target = range(left + 1, col + 1).find(
×
2939
                                    n => !disallowed.includes(n - rowMarkerOffset)
×
2940
                                );
×
2941
                                if (target !== undefined) {
×
2942
                                    left = target;
×
2943
                                    done = true;
×
2944
                                }
×
2945
                            } else {
×
2946
                                left++;
×
2947
                                done = true;
×
2948
                            }
×
2949
                            if (done) scrollTo(left, 0, "horizontal");
×
2950
                        }
×
2951
                        if (!done) {
4✔
2952
                            right = Math.min(mangledCols.length, right + 1);
4✔
2953
                            scrollTo(right - 1 - rowMarkerOffset, 0, "horizontal");
4✔
2954
                        }
4✔
2955
                    } else if (x === -1) {
5✔
2956
                        // motion left
1✔
2957
                        let done = false;
1✔
2958
                        if (right > col + 1) {
1!
2959
                            if (disallowed.length > 0) {
×
2960
                                const target = range(right - 1, col, -1).find(
×
2961
                                    n => !disallowed.includes(n - rowMarkerOffset)
×
2962
                                );
×
2963
                                if (target !== undefined) {
×
2964
                                    right = target;
×
2965
                                    done = true;
×
2966
                                }
×
2967
                            } else {
×
2968
                                right--;
×
2969
                                done = true;
×
2970
                            }
×
2971
                            if (done) scrollTo(right - rowMarkerOffset, 0, "horizontal");
×
2972
                        }
×
2973
                        if (!done) {
1✔
2974
                            left = Math.max(rowMarkerOffset, left - 1);
1✔
2975
                            scrollTo(left - rowMarkerOffset, 0, "horizontal");
1✔
2976
                        }
1✔
2977
                    } else {
1!
2978
                        assertNever(x);
×
2979
                    }
×
2980
                }
5✔
2981
            }
8✔
2982

10✔
2983
            setCurrent(
10✔
2984
                {
10✔
2985
                    cell: gridSelection.current.cell,
10✔
2986
                    range: {
10✔
2987
                        x: left,
10✔
2988
                        y: top,
10✔
2989
                        width: right - left,
10✔
2990
                        height: bottom - top,
10✔
2991
                    },
10✔
2992
                },
10✔
2993
                true,
10✔
2994
                false,
10✔
2995
                "keyboard-select"
10✔
2996
            );
10✔
2997
        },
10✔
2998
        [
771✔
2999
            getCellsForSelection,
771✔
3000
            getSelectionRowLimits,
771✔
3001
            gridSelection,
771✔
3002
            mangledCols.length,
771✔
3003
            rowMarkerOffset,
771✔
3004
            rows,
771✔
3005
            scrollTo,
771✔
3006
            setCurrent,
771✔
3007
        ]
771✔
3008
    );
771✔
3009

771✔
3010
    const scrollToActiveCellRef = React.useRef(scrollToActiveCell);
771✔
3011
    scrollToActiveCellRef.current = scrollToActiveCell;
771✔
3012

771✔
3013
    const updateSelectedCell = React.useCallback(
771✔
3014
        (col: number, row: number, fromEditingTrailingRow: boolean, freeMove: boolean): boolean => {
771✔
3015
            const rowMax = mangledRows - (fromEditingTrailingRow ? 0 : 1);
68!
3016
            col = clamp(col, rowMarkerOffset, columns.length - 1 + rowMarkerOffset);
68✔
3017
            row = clamp(row, 0, rowMax);
68✔
3018

68✔
3019
            const curCol = currentCell?.[0];
68✔
3020
            const curRow = currentCell?.[1];
68✔
3021

68✔
3022
            if (col === curCol && row === curRow) return false;
68✔
3023
            if (freeMove && gridSelection.current !== undefined) {
68✔
3024
                const newStack = [...gridSelection.current.rangeStack];
1✔
3025
                if (gridSelection.current.range.width > 1 || gridSelection.current.range.height > 1) {
1!
3026
                    newStack.push(gridSelection.current.range);
1✔
3027
                }
1✔
3028
                setGridSelection(
1✔
3029
                    {
1✔
3030
                        ...gridSelection,
1✔
3031
                        current: {
1✔
3032
                            cell: [col, row],
1✔
3033
                            range: { x: col, y: row, width: 1, height: 1 },
1✔
3034
                            rangeStack: newStack,
1✔
3035
                        },
1✔
3036
                    },
1✔
3037
                    true
1✔
3038
                );
1✔
3039
            } else {
68✔
3040
                setCurrent(
29✔
3041
                    {
29✔
3042
                        cell: [col, row],
29✔
3043
                        range: { x: col, y: row, width: 1, height: 1 },
29✔
3044
                    },
29✔
3045
                    true,
29✔
3046
                    false,
29✔
3047
                    "keyboard-nav"
29✔
3048
                );
29✔
3049
            }
29✔
3050

30✔
3051
            if (lastSent.current !== undefined && lastSent.current[0] === col && lastSent.current[1] === row) {
68✔
3052
                lastSent.current = undefined;
2✔
3053
            }
2✔
3054

30✔
3055
            if (scrollToActiveCellRef.current) {
30✔
3056
                scrollTo(col - rowMarkerOffset, row);
30✔
3057
            }
30✔
3058

30✔
3059
            return true;
30✔
3060
        },
68✔
3061
        [
771✔
3062
            mangledRows,
771✔
3063
            rowMarkerOffset,
771✔
3064
            columns.length,
771✔
3065
            currentCell,
771✔
3066
            gridSelection,
771✔
3067
            scrollTo,
771✔
3068
            setGridSelection,
771✔
3069
            setCurrent,
771✔
3070
        ]
771✔
3071
    );
771✔
3072

771✔
3073
    const onFinishEditing = React.useCallback(
771✔
3074
        (newValue: GridCell | undefined, movement: readonly [-1 | 0 | 1, -1 | 0 | 1]) => {
771✔
3075
            if (overlay?.cell !== undefined && newValue !== undefined && isEditableGridCell(newValue)) {
12✔
3076
                mangledOnCellsEdited([{ location: overlay.cell, value: newValue }]);
4✔
3077
                window.requestAnimationFrame(() => {
4✔
3078
                    gridRef.current?.damage([
4✔
3079
                        {
4✔
3080
                            cell: overlay.cell,
4✔
3081
                        },
4✔
3082
                    ]);
4✔
3083
                });
4✔
3084
            }
4✔
3085
            focus(true);
12✔
3086
            setOverlay(undefined);
12✔
3087

12✔
3088
            const [movX, movY] = movement;
12✔
3089
            if (gridSelection.current !== undefined && (movX !== 0 || movY !== 0)) {
12✔
3090
                const isEditingLastRow = gridSelection.current.cell[1] === mangledRows - 1 && newValue !== undefined;
3!
3091
                const isEditingLastCol =
3✔
3092
                    gridSelection.current.cell[0] === mangledCols.length - 1 && newValue !== undefined;
3!
3093
                let updateSelected = true;
3✔
3094
                if (isEditingLastRow && movY === 1 && onRowAppended !== undefined) {
3!
3095
                    updateSelected = false;
×
3096
                    const col = gridSelection.current.cell[0] + movX;
×
3097
                    const customTargetColumn = getCustomNewRowTargetColumn(col);
×
3098
                    void appendRow(customTargetColumn ?? col, false);
×
3099
                }
×
3100
                if (isEditingLastCol && movX === 1 && onColumnAppended !== undefined) {
3!
3101
                    updateSelected = false;
×
3102
                    const row = gridSelection.current.cell[1] + movY;
×
3103
                    void appendColumn(row, false);
×
3104
                }
×
3105
                if (updateSelected) {
3✔
3106
                    updateSelectedCell(
3✔
3107
                        clamp(gridSelection.current.cell[0] + movX, 0, mangledCols.length - 1),
3✔
3108
                        clamp(gridSelection.current.cell[1] + movY, 0, mangledRows - 1),
3✔
3109
                        isEditingLastRow,
3✔
3110
                        false
3✔
3111
                    );
3✔
3112
                }
3✔
3113
            }
3✔
3114
            onFinishedEditing?.(newValue, movement);
12✔
3115
        },
12✔
3116
        [
771✔
3117
            overlay?.cell,
771✔
3118
            focus,
771✔
3119
            gridSelection,
771✔
3120
            onFinishedEditing,
771✔
3121
            mangledOnCellsEdited,
771✔
3122
            mangledRows,
771✔
3123
            updateSelectedCell,
771✔
3124
            mangledCols.length,
771✔
3125
            appendRow,
771✔
3126
            appendColumn,
771✔
3127
            onRowAppended,
771✔
3128
            onColumnAppended,
771✔
3129
            getCustomNewRowTargetColumn,
771✔
3130
        ]
771✔
3131
    );
771✔
3132

771✔
3133
    const overlayID = React.useMemo(() => {
771✔
3134
        return `gdg-overlay-${idCounter++}`;
157✔
3135
    }, []);
771✔
3136

771✔
3137
    const deleteRange = React.useCallback(
771✔
3138
        (r: Rectangle) => {
771✔
3139
            focus();
8✔
3140
            const editList: EditListItem[] = [];
8✔
3141
            for (let x = r.x; x < r.x + r.width; x++) {
8✔
3142
                for (let y = r.y; y < r.y + r.height; y++) {
23✔
3143
                    const cellValue = getCellContent([x - rowMarkerOffset, y]);
1,066✔
3144
                    if (!cellValue.allowOverlay && cellValue.kind !== GridCellKind.Boolean) continue;
1,066✔
3145
                    let newVal: InnerGridCell | undefined = undefined;
1,042✔
3146
                    if (cellValue.kind === GridCellKind.Custom) {
1,066✔
3147
                        const toDelete = getCellRenderer(cellValue);
1✔
3148
                        const editor = toDelete?.provideEditor?.({
1!
3149
                            ...cellValue,
×
3150
                            location: [x - rowMarkerOffset, y],
×
3151
                        });
×
3152
                        if (toDelete?.onDelete !== undefined) {
1✔
3153
                            newVal = toDelete.onDelete(cellValue);
1✔
3154
                        } else if (isObjectEditorCallbackResult(editor)) {
1!
3155
                            newVal = editor?.deletedValue?.(cellValue);
×
3156
                        }
×
3157
                    } else if (
1✔
3158
                        (isEditableGridCell(cellValue) && cellValue.allowOverlay) ||
1,041✔
3159
                        cellValue.kind === GridCellKind.Boolean
1✔
3160
                    ) {
1,041✔
3161
                        const toDelete = getCellRenderer(cellValue);
1,041✔
3162
                        newVal = toDelete?.onDelete?.(cellValue);
1,041✔
3163
                    }
1,041✔
3164
                    if (newVal !== undefined && !isInnerOnlyCell(newVal) && isEditableGridCell(newVal)) {
1,066✔
3165
                        editList.push({ location: [x, y], value: newVal });
1,041✔
3166
                    }
1,041✔
3167
                }
1,066✔
3168
            }
23✔
3169
            mangledOnCellsEdited(editList);
8✔
3170
            gridRef.current?.damage(editList.map(x => ({ cell: x.location })));
8✔
3171
        },
8✔
3172
        [focus, getCellContent, getCellRenderer, mangledOnCellsEdited, rowMarkerOffset]
771✔
3173
    );
771✔
3174

771✔
3175
    const overlayOpen = overlay !== undefined;
771✔
3176

771✔
3177
    const handleFixedKeybindings = React.useCallback(
771✔
3178
        (event: GridKeyEventArgs): boolean => {
771✔
3179
            const cancel = () => {
74✔
3180
                event.stopPropagation();
50✔
3181
                event.preventDefault();
50✔
3182
            };
50✔
3183

74✔
3184
            const details = {
74✔
3185
                didMatch: false,
74✔
3186
            };
74✔
3187

74✔
3188
            const { bounds } = event;
74✔
3189
            const selectedColumns = gridSelection.columns;
74✔
3190
            const selectedRows = gridSelection.rows;
74✔
3191

74✔
3192
            const keys = keybindings;
74✔
3193

74✔
3194
            if (!overlayOpen && isHotkey(keys.clear, event, details)) {
74✔
3195
                setGridSelection(emptyGridSelection, false);
2✔
3196
                onSelectionCleared?.();
2!
3197
            } else if (!overlayOpen && isHotkey(keys.selectAll, event, details)) {
74✔
3198
                setGridSelection(
1✔
3199
                    {
1✔
3200
                        columns: CompactSelection.empty(),
1✔
3201
                        rows: CompactSelection.empty(),
1✔
3202
                        current: {
1✔
3203
                            cell: gridSelection.current?.cell ?? [rowMarkerOffset, 0],
1!
3204
                            range: {
1✔
3205
                                x: rowMarkerOffset,
1✔
3206
                                y: 0,
1✔
3207
                                width: columnsIn.length,
1✔
3208
                                height: rows,
1✔
3209
                            },
1✔
3210
                            rangeStack: [],
1✔
3211
                        },
1✔
3212
                    },
1✔
3213
                    false
1✔
3214
                );
1✔
3215
            } else if (isHotkey(keys.search, event, details)) {
72!
3216
                searchInputRef?.current?.focus({ preventScroll: true });
×
3217
                setShowSearchInner(true);
×
3218
            } else if (isHotkey(keys.delete, event, details)) {
71✔
3219
                const callbackResult = onDelete?.(gridSelection) ?? true;
8✔
3220
                if (callbackResult !== false) {
8✔
3221
                    const toDelete = callbackResult === true ? gridSelection : callbackResult;
8✔
3222

8✔
3223
                    // delete order:
8✔
3224
                    // 1) primary range
8✔
3225
                    // 2) secondary ranges
8✔
3226
                    // 3) columns
8✔
3227
                    // 4) rows
8✔
3228

8✔
3229
                    if (toDelete.current !== undefined) {
8✔
3230
                        deleteRange(toDelete.current.range);
5✔
3231
                        for (const r of toDelete.current.rangeStack) {
5!
3232
                            deleteRange(r);
×
3233
                        }
×
3234
                    }
5✔
3235

8✔
3236
                    for (const r of toDelete.rows) {
8✔
3237
                        deleteRange({
1✔
3238
                            x: rowMarkerOffset,
1✔
3239
                            y: r,
1✔
3240
                            width: columnsIn.length,
1✔
3241
                            height: 1,
1✔
3242
                        });
1✔
3243
                    }
1✔
3244

8✔
3245
                    for (const col of toDelete.columns) {
8✔
3246
                        deleteRange({
1✔
3247
                            x: col,
1✔
3248
                            y: 0,
1✔
3249
                            width: 1,
1✔
3250
                            height: rows,
1✔
3251
                        });
1✔
3252
                    }
1✔
3253
                }
8✔
3254
            }
8✔
3255

74✔
3256
            if (details.didMatch) {
74✔
3257
                cancel();
11✔
3258
                return true;
11✔
3259
            }
11✔
3260

63✔
3261
            if (gridSelection.current === undefined) return false;
74✔
3262
            let [col, row] = gridSelection.current.cell;
60✔
3263
            const [, startRow] = gridSelection.current.cell;
60✔
3264
            let freeMove = false;
60✔
3265
            let cancelOnlyOnMove = false;
60✔
3266

60✔
3267
            if (isHotkey(keys.scrollToSelectedCell, event, details)) {
74!
3268
                scrollToRef.current(col - rowMarkerOffset, row);
×
3269
            } else if (columnSelect !== "none" && isHotkey(keys.selectColumn, event, details)) {
74✔
3270
                if (selectedColumns.hasIndex(col)) {
2!
3271
                    setSelectedColumns(selectedColumns.remove(col), undefined, true);
×
3272
                } else {
2✔
3273
                    if (columnSelect === "single") {
2!
3274
                        setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, true);
×
3275
                    } else {
2✔
3276
                        setSelectedColumns(undefined, col, true);
2✔
3277
                    }
2✔
3278
                }
2✔
3279
            } else if (rowSelect !== "none" && isHotkey(keys.selectRow, event, details)) {
60✔
3280
                if (selectedRows.hasIndex(row)) {
2!
3281
                    setSelectedRows(selectedRows.remove(row), undefined, true);
×
3282
                } else {
2✔
3283
                    if (rowSelect === "single") {
2!
3284
                        setSelectedRows(CompactSelection.fromSingleSelection(row), undefined, true);
×
3285
                    } else {
2✔
3286
                        setSelectedRows(undefined, row, true);
2✔
3287
                    }
2✔
3288
                }
2✔
3289
            } else if (!overlayOpen && bounds !== undefined && isHotkey(keys.activateCell, event, details)) {
58✔
3290
                if (row === rows && showTrailingBlankRow) {
7!
3291
                    window.setTimeout(() => {
×
3292
                        const customTargetColumn = getCustomNewRowTargetColumn(col);
×
3293
                        void appendRow(customTargetColumn ?? col);
×
3294
                    }, 0);
×
3295
                } else {
7✔
3296
                    const activationEvent: CellActivatedEventArgs = {
7✔
3297
                        inputType: "keyboard",
7✔
3298
                        key: event.key,
7✔
3299
                    };
7✔
3300
                    onCellActivated?.([col - rowMarkerOffset, row], activationEvent);
7✔
3301
                    reselect(bounds, activationEvent);
7✔
3302
                }
7✔
3303
            } else if (gridSelection.current.range.height > 1 && isHotkey(keys.downFill, event, details)) {
56✔
3304
                fillDown();
1✔
3305
            } else if (gridSelection.current.range.width > 1 && isHotkey(keys.rightFill, event, details)) {
49✔
3306
                fillRight();
1✔
3307
            } else if (isHotkey(keys.goToNextPage, event, details)) {
48✔
3308
                row += Math.max(1, visibleRegionRef.current.height - 4); // partial cell accounting
1✔
3309
            } else if (isHotkey(keys.goToPreviousPage, event, details)) {
47✔
3310
                row -= Math.max(1, visibleRegionRef.current.height - 4); // partial cell accounting
1✔
3311
            } else if (isHotkey(keys.goToFirstCell, event, details)) {
46✔
3312
                setOverlay(undefined);
1✔
3313
                row = 0;
1✔
3314
                col = 0;
1✔
3315
            } else if (isHotkey(keys.goToLastCell, event, details)) {
45✔
3316
                setOverlay(undefined);
1✔
3317
                row = Number.MAX_SAFE_INTEGER;
1✔
3318
                col = Number.MAX_SAFE_INTEGER;
1✔
3319
            } else if (isHotkey(keys.selectToFirstCell, event, details)) {
44✔
3320
                setOverlay(undefined);
1✔
3321
                adjustSelection([-2, -2]);
1✔
3322
            } else if (isHotkey(keys.selectToLastCell, event, details)) {
43✔
3323
                setOverlay(undefined);
1✔
3324
                adjustSelection([2, 2]);
1✔
3325
            } else if (!overlayOpen) {
42✔
3326
                if (isHotkey(keys.goDownCell, event, details)) {
37✔
3327
                    row += 1;
5✔
3328
                } else if (isHotkey(keys.goUpCell, event, details)) {
37✔
3329
                    row -= 1;
1✔
3330
                } else if (isHotkey(keys.goRightCell, event, details)) {
32✔
3331
                    col += 1;
2✔
3332
                } else if (isHotkey(keys.goLeftCell, event, details)) {
31✔
3333
                    col -= 1;
2✔
3334
                } else if (isHotkey(keys.goDownCellRetainSelection, event, details)) {
29!
3335
                    row += 1;
×
3336
                    freeMove = true;
×
3337
                } else if (isHotkey(keys.goUpCellRetainSelection, event, details)) {
27!
3338
                    row -= 1;
×
3339
                    freeMove = true;
×
3340
                } else if (isHotkey(keys.goRightCellRetainSelection, event, details)) {
27!
3341
                    col += 1;
×
3342
                    freeMove = true;
×
3343
                } else if (isHotkey(keys.goLeftCellRetainSelection, event, details)) {
27✔
3344
                    col -= 1;
1✔
3345
                    freeMove = true;
1✔
3346
                } else if (isHotkey(keys.goToLastRow, event, details)) {
27✔
3347
                    row = rows - 1;
1✔
3348
                } else if (isHotkey(keys.goToFirstRow, event, details)) {
26✔
3349
                    row = Number.MIN_SAFE_INTEGER;
1✔
3350
                } else if (isHotkey(keys.goToLastColumn, event, details)) {
25✔
3351
                    col = Number.MAX_SAFE_INTEGER;
2✔
3352
                } else if (isHotkey(keys.goToFirstColumn, event, details)) {
24✔
3353
                    col = Number.MIN_SAFE_INTEGER;
1✔
3354
                } else if (rangeSelect === "rect" || rangeSelect === "multi-rect") {
22!
3355
                    if (isHotkey(keys.selectGrowDown, event, details)) {
21✔
3356
                        adjustSelection([0, 1]);
1✔
3357
                    } else if (isHotkey(keys.selectGrowUp, event, details)) {
21!
3358
                        adjustSelection([0, -1]);
×
3359
                    } else if (isHotkey(keys.selectGrowRight, event, details)) {
20✔
3360
                        adjustSelection([1, 0]);
4✔
3361
                    } else if (isHotkey(keys.selectGrowLeft, event, details)) {
20✔
3362
                        adjustSelection([-1, 0]);
1✔
3363
                    } else if (isHotkey(keys.selectToLastRow, event, details)) {
16✔
3364
                        adjustSelection([0, 2]);
1✔
3365
                    } else if (isHotkey(keys.selectToFirstRow, event, details)) {
15!
3366
                        adjustSelection([0, -2]);
×
3367
                    } else if (isHotkey(keys.selectToLastColumn, event, details)) {
14✔
3368
                        adjustSelection([2, 0]);
1✔
3369
                    } else if (isHotkey(keys.selectToFirstColumn, event, details)) {
14!
3370
                        adjustSelection([-2, 0]);
×
3371
                    }
×
3372
                }
21✔
3373
                cancelOnlyOnMove = details.didMatch;
37✔
3374
            } else {
41✔
3375
                if (isHotkey(keys.closeOverlay, event, details)) {
4✔
3376
                    setOverlay(undefined);
2✔
3377
                }
2✔
3378

4✔
3379
                if (isHotkey(keys.acceptOverlayDown, event, details)) {
4✔
3380
                    setOverlay(undefined);
2✔
3381
                    row++;
2✔
3382
                }
2✔
3383

4✔
3384
                if (isHotkey(keys.acceptOverlayUp, event, details)) {
4!
3385
                    setOverlay(undefined);
×
3386
                    row--;
×
3387
                }
×
3388

4✔
3389
                if (isHotkey(keys.acceptOverlayLeft, event, details)) {
4!
3390
                    setOverlay(undefined);
×
3391
                    col--;
×
3392
                }
×
3393

4✔
3394
                if (isHotkey(keys.acceptOverlayRight, event, details)) {
4!
3395
                    setOverlay(undefined);
×
3396
                    col++;
×
3397
                }
×
3398
            }
4✔
3399
            // #endregion
60✔
3400

60✔
3401
            const mustRestrictRow = rowGroupingNavBehavior !== undefined && rowGroupingNavBehavior !== "normal";
74!
3402

74✔
3403
            if (mustRestrictRow && row !== startRow) {
74!
3404
                const skipUp =
×
3405
                    rowGroupingNavBehavior === "skip-up" ||
×
3406
                    rowGroupingNavBehavior === "skip" ||
×
3407
                    rowGroupingNavBehavior === "block";
×
3408
                const skipDown =
×
3409
                    rowGroupingNavBehavior === "skip-down" ||
×
3410
                    rowGroupingNavBehavior === "skip" ||
×
3411
                    rowGroupingNavBehavior === "block";
×
3412
                const didMoveUp = row < startRow;
×
3413
                if (didMoveUp && skipUp) {
×
3414
                    while (row >= 0 && mapper(row).isGroupHeader) {
×
3415
                        row--;
×
3416
                    }
×
3417

×
3418
                    if (row < 0) {
×
3419
                        row = startRow;
×
3420
                    }
×
3421
                } else if (!didMoveUp && skipDown) {
×
3422
                    while (row < rows && mapper(row).isGroupHeader) {
×
3423
                        row++;
×
3424
                    }
×
3425

×
3426
                    if (row >= rows) {
×
3427
                        row = startRow;
×
3428
                    }
×
3429
                }
×
3430
            }
✔
3431

60✔
3432
            const moved = updateSelectedCell(col, row, false, freeMove);
60✔
3433

60✔
3434
            const didMatch = details.didMatch;
60✔
3435

60✔
3436
            if (didMatch && (moved || !cancelOnlyOnMove || trapFocus)) {
74✔
3437
                cancel();
39✔
3438
            }
39✔
3439

60✔
3440
            return didMatch;
60✔
3441
        },
74✔
3442
        [
771✔
3443
            rowGroupingNavBehavior,
771✔
3444
            overlayOpen,
771✔
3445
            gridSelection,
771✔
3446
            keybindings,
771✔
3447
            columnSelect,
771✔
3448
            rowSelect,
771✔
3449
            rangeSelect,
771✔
3450
            rowMarkerOffset,
771✔
3451
            mapper,
771✔
3452
            rows,
771✔
3453
            updateSelectedCell,
771✔
3454
            setGridSelection,
771✔
3455
            onSelectionCleared,
771✔
3456
            columnsIn.length,
771✔
3457
            onDelete,
771✔
3458
            trapFocus,
771✔
3459
            deleteRange,
771✔
3460
            setSelectedColumns,
771✔
3461
            setSelectedRows,
771✔
3462
            showTrailingBlankRow,
771✔
3463
            getCustomNewRowTargetColumn,
771✔
3464
            appendRow,
771✔
3465
            onCellActivated,
771✔
3466
            reselect,
771✔
3467
            fillDown,
771✔
3468
            fillRight,
771✔
3469
            adjustSelection,
771✔
3470
        ]
771✔
3471
    );
771✔
3472

771✔
3473
    const onKeyDown = React.useCallback(
771✔
3474
        (event: GridKeyEventArgs) => {
771✔
3475
            let cancelled = false;
74✔
3476
            if (onKeyDownIn !== undefined) {
74✔
3477
                onKeyDownIn({
1✔
3478
                    ...event,
1✔
3479
                    ...(event.location && {
1✔
3480
                        location: [event.location[0] - rowMarkerOffset, event.location[1]] as any,
1✔
3481
                    }),
1✔
3482
                    cancel: () => {
1✔
3483
                        cancelled = true;
×
3484
                    },
×
3485
                });
1✔
3486
            }
1✔
3487

74✔
3488
            if (cancelled) return;
74!
3489

74✔
3490
            if (handleFixedKeybindings(event)) return;
74✔
3491

16✔
3492
            if (gridSelection.current === undefined) return;
16✔
3493
            const [col, row] = gridSelection.current.cell;
13✔
3494
            const vr = visibleRegionRef.current;
13✔
3495

13✔
3496
            if (
13✔
3497
                editOnType &&
13✔
3498
                !event.metaKey &&
13✔
3499
                !event.ctrlKey &&
13✔
3500
                gridSelection.current !== undefined &&
13✔
3501
                event.key.length === 1 &&
13✔
3502
                /[\p{L}\p{M}\p{N}\p{S}\p{P}]/u.test(event.key) &&
13✔
3503
                event.bounds !== undefined &&
13✔
3504
                isReadWriteCell(getCellContent([col - rowMarkerOffset, Math.max(0, Math.min(row, rows - 1))]))
13✔
3505
            ) {
74✔
3506
                if (
13✔
3507
                    (!showTrailingBlankRow || row !== rows) &&
13✔
3508
                    (vr.y > row || row > vr.y + vr.height || vr.x > col || col > vr.x + vr.width)
13✔
3509
                ) {
13!
3510
                    return;
×
3511
                }
×
3512
                const activationEvent: CellActivatedEventArgs = {
13✔
3513
                    inputType: "keyboard",
13✔
3514
                    key: event.key,
13✔
3515
                };
13✔
3516
                onCellActivated?.([col - rowMarkerOffset, row], activationEvent);
13✔
3517
                reselect(event.bounds, activationEvent, event.key);
13✔
3518
                event.stopPropagation();
13✔
3519
                event.preventDefault();
13✔
3520
            }
13✔
3521
        },
74✔
3522
        [
771✔
3523
            editOnType,
771✔
3524
            onKeyDownIn,
771✔
3525
            handleFixedKeybindings,
771✔
3526
            gridSelection,
771✔
3527
            getCellContent,
771✔
3528
            rowMarkerOffset,
771✔
3529
            rows,
771✔
3530
            showTrailingBlankRow,
771✔
3531
            onCellActivated,
771✔
3532
            reselect,
771✔
3533
        ]
771✔
3534
    );
771✔
3535

771✔
3536
    const onContextMenu = React.useCallback(
771✔
3537
        (args: GridMouseEventArgs, preventDefault: () => void) => {
771✔
3538
            const adjustedCol = args.location[0] - rowMarkerOffset;
7✔
3539
            if (args.kind === "header") {
7!
3540
                onHeaderContextMenu?.(adjustedCol, { ...args, preventDefault });
×
3541
            }
×
3542

7✔
3543
            if (args.kind === groupHeaderKind) {
7!
3544
                if (adjustedCol < 0) {
×
3545
                    return;
×
3546
                }
×
3547
                onGroupHeaderContextMenu?.(adjustedCol, { ...args, preventDefault });
×
3548
            }
×
3549

7✔
3550
            if (args.kind === "cell") {
7✔
3551
                const [col, row] = args.location;
7✔
3552
                onCellContextMenu?.([adjustedCol, row], {
7✔
3553
                    ...args,
7✔
3554
                    preventDefault,
7✔
3555
                });
7✔
3556

7✔
3557
                if (!gridSelectionHasItem(gridSelection, args.location)) {
7✔
3558
                    updateSelectedCell(col, row, false, false);
3✔
3559
                }
3✔
3560
            }
7✔
3561
        },
7✔
3562
        [
771✔
3563
            gridSelection,
771✔
3564
            onCellContextMenu,
771✔
3565
            onGroupHeaderContextMenu,
771✔
3566
            onHeaderContextMenu,
771✔
3567
            rowMarkerOffset,
771✔
3568
            updateSelectedCell,
771✔
3569
        ]
771✔
3570
    );
771✔
3571

771✔
3572
    const onPasteInternal = React.useCallback(
771✔
3573
        async (e?: ClipboardEvent) => {
771✔
3574
            if (!keybindings.paste) return;
6!
3575
            function pasteToCell(
6✔
3576
                inner: InnerGridCell,
51✔
3577
                target: Item,
51✔
3578
                rawValue: string | boolean | string[] | number | boolean | BooleanEmpty | BooleanIndeterminate,
51✔
3579
                formatted?: string | string[]
51✔
3580
            ): EditListItem | undefined {
51✔
3581
                const stringifiedRawValue =
51✔
3582
                    typeof rawValue === "object" ? (rawValue?.join("\n") ?? "") : (rawValue?.toString() ?? "");
51!
3583

51✔
3584
                if (!isInnerOnlyCell(inner) && isReadWriteCell(inner) && inner.readonly !== true) {
51✔
3585
                    const coerced = coercePasteValue?.(stringifiedRawValue, inner);
51!
3586
                    if (coerced !== undefined && isEditableGridCell(coerced)) {
51!
3587
                        if (process.env.NODE_ENV !== "production" && coerced.kind !== inner.kind) {
×
3588
                            // eslint-disable-next-line no-console
×
3589
                            console.warn("Coercion should not change cell kind.");
×
3590
                        }
×
3591
                        return {
×
3592
                            location: target,
×
3593
                            value: coerced,
×
3594
                        };
×
3595
                    }
×
3596
                    const r = getCellRenderer(inner);
51✔
3597
                    if (r === undefined) return undefined;
51!
3598
                    if (r.kind === GridCellKind.Custom) {
51✔
3599
                        assert(inner.kind === GridCellKind.Custom);
1✔
3600
                        const newVal = (r as unknown as CustomRenderer<CustomCell<any>>).onPaste?.(
1✔
3601
                            stringifiedRawValue,
1✔
3602
                            inner.data
1✔
3603
                        );
1✔
3604
                        if (newVal === undefined) return undefined;
1!
3605
                        return {
×
3606
                            location: target,
×
3607
                            value: {
×
3608
                                ...inner,
×
3609
                                data: newVal,
×
3610
                            },
×
3611
                        };
×
3612
                    } else {
51✔
3613
                        const newVal = r.onPaste?.(stringifiedRawValue, inner, {
50✔
3614
                            formatted,
50✔
3615
                            formattedString: typeof formatted === "string" ? formatted : formatted?.join("\n"),
50!
3616
                            rawValue,
50✔
3617
                        });
50✔
3618
                        if (newVal === undefined) return undefined;
50✔
3619
                        assert(newVal.kind === inner.kind);
36✔
3620
                        return {
36✔
3621
                            location: target,
36✔
3622
                            value: newVal,
36✔
3623
                        };
36✔
3624
                    }
36✔
3625
                }
51!
3626
                return undefined;
×
3627
            }
51✔
3628

6✔
3629
            const selectedColumns = gridSelection.columns;
6✔
3630
            const selectedRows = gridSelection.rows;
6✔
3631
            const focused =
6✔
3632
                scrollRef.current?.contains(document.activeElement) === true ||
6✔
3633
                canvasRef.current?.contains(document.activeElement) === true;
6✔
3634

6✔
3635
            let target: Item | undefined;
6✔
3636

6✔
3637
            if (gridSelection.current !== undefined) {
6✔
3638
                target = [gridSelection.current.range.x, gridSelection.current.range.y];
5✔
3639
            } else if (selectedColumns.length === 1) {
6!
3640
                target = [selectedColumns.first() ?? 0, 0];
×
3641
            } else if (selectedRows.length === 1) {
1!
3642
                target = [rowMarkerOffset, selectedRows.first() ?? 0];
×
3643
            }
×
3644

6✔
3645
            if (focused && target !== undefined) {
6✔
3646
                let data: CopyBuffer | undefined;
5✔
3647
                let text: string | undefined;
5✔
3648

5✔
3649
                const textPlain = "text/plain";
5✔
3650
                const textHtml = "text/html";
5✔
3651

5✔
3652
                if (navigator.clipboard.read !== undefined) {
5!
3653
                    const clipboardContent = await navigator.clipboard.read();
×
3654

×
3655
                    for (const item of clipboardContent) {
×
3656
                        if (item.types.includes(textHtml)) {
×
3657
                            const htmlBlob = await item.getType(textHtml);
×
3658
                            const html = await htmlBlob.text();
×
3659
                            const decoded = decodeHTML(html);
×
3660
                            if (decoded !== undefined) {
×
3661
                                data = decoded;
×
3662
                                break;
×
3663
                            }
×
3664
                        }
×
3665
                        if (item.types.includes(textPlain)) {
×
3666
                            // eslint-disable-next-line unicorn/no-await-expression-member
×
3667
                            text = await (await item.getType(textPlain)).text();
×
3668
                        }
×
3669
                    }
×
3670
                } else if (navigator.clipboard.readText !== undefined) {
5✔
3671
                    text = await navigator.clipboard.readText();
5✔
3672
                } else if (e !== undefined && e?.clipboardData !== null) {
5!
3673
                    if (e.clipboardData.types.includes(textHtml)) {
×
3674
                        const html = e.clipboardData.getData(textHtml);
×
3675
                        data = decodeHTML(html);
×
3676
                    }
×
3677
                    if (data === undefined && e.clipboardData.types.includes(textPlain)) {
×
3678
                        text = e.clipboardData.getData(textPlain);
×
3679
                    }
×
3680
                } else {
×
3681
                    return; // I didn't want to read that paste value anyway
×
3682
                }
×
3683

5✔
3684
                const [targetCol, targetRow] = target;
5✔
3685

5✔
3686
                const editList: EditListItem[] = [];
5✔
3687
                do {
5✔
3688
                    if (onPaste === undefined) {
5✔
3689
                        const cellData = getMangledCellContent(target);
2✔
3690
                        const rawValue = text ?? data?.map(r => r.map(cb => cb.rawValue).join("\t")).join("\t") ?? "";
2!
3691
                        const newVal = pasteToCell(cellData, target, rawValue, undefined);
2✔
3692
                        if (newVal !== undefined) {
2✔
3693
                            editList.push(newVal);
1✔
3694
                        }
1✔
3695
                        break;
2✔
3696
                    }
2✔
3697

3✔
3698
                    if (data === undefined) {
3✔
3699
                        if (text === undefined) return;
3!
3700
                        data = unquote(text);
3✔
3701
                    }
3✔
3702

3✔
3703
                    if (
3✔
3704
                        onPaste === false ||
3✔
3705
                        (typeof onPaste === "function" &&
3✔
3706
                            onPaste?.(
2✔
3707
                                [target[0] - rowMarkerOffset, target[1]],
2✔
3708
                                data.map(r => r.map(cb => cb.rawValue?.toString() ?? ""))
2!
3709
                            ) !== true)
2✔
3710
                    ) {
5!
3711
                        return;
×
3712
                    }
✔
3713

3✔
3714
                    for (const [row, dataRow] of data.entries()) {
5✔
3715
                        if (row + targetRow >= rows) break;
21!
3716
                        for (const [col, dataItem] of dataRow.entries()) {
21✔
3717
                            const index = [col + targetCol, row + targetRow] as const;
63✔
3718
                            const [writeCol, writeRow] = index;
63✔
3719
                            if (writeCol >= mangledCols.length) continue;
63✔
3720
                            if (writeRow >= mangledRows) continue;
49!
3721
                            const cellData = getMangledCellContent(index);
49✔
3722
                            const newVal = pasteToCell(cellData, index, dataItem.rawValue, dataItem.formatted);
49✔
3723
                            if (newVal !== undefined) {
63✔
3724
                                editList.push(newVal);
35✔
3725
                            }
35✔
3726
                        }
63✔
3727
                    }
21✔
3728
                    // eslint-disable-next-line no-constant-condition
3✔
3729
                } while (false);
5✔
3730

5✔
3731
                mangledOnCellsEdited(editList);
5✔
3732

5✔
3733
                gridRef.current?.damage(
5✔
3734
                    editList.map(c => ({
5✔
3735
                        cell: c.location,
36✔
3736
                    }))
5✔
3737
                );
5✔
3738
            }
5✔
3739
        },
6✔
3740
        [
771✔
3741
            coercePasteValue,
771✔
3742
            getCellRenderer,
771✔
3743
            getMangledCellContent,
771✔
3744
            gridSelection,
771✔
3745
            keybindings.paste,
771✔
3746
            scrollRef,
771✔
3747
            mangledCols.length,
771✔
3748
            mangledOnCellsEdited,
771✔
3749
            mangledRows,
771✔
3750
            onPaste,
771✔
3751
            rowMarkerOffset,
771✔
3752
            rows,
771✔
3753
        ]
771✔
3754
    );
771✔
3755

771✔
3756
    useEventListener("paste", onPasteInternal, safeWindow, false, true);
771✔
3757

771✔
3758
    // While this function is async, we deeply prefer not to await if we don't have to. This will lead to unpacking
771✔
3759
    // promises in rather awkward ways when possible to avoid awaiting. We have to use fallback copy mechanisms when
771✔
3760
    // an await has happened.
771✔
3761
    const onCopy = React.useCallback(
771✔
3762
        async (e?: ClipboardEvent, ignoreFocus?: boolean) => {
771✔
3763
            if (!keybindings.copy) return;
6!
3764
            const focused =
6✔
3765
                ignoreFocus === true ||
6✔
3766
                scrollRef.current?.contains(document.activeElement) === true ||
5✔
3767
                canvasRef.current?.contains(document.activeElement) === true;
5✔
3768

6✔
3769
            const selectedColumns = gridSelection.columns;
6✔
3770
            const selectedRows = gridSelection.rows;
6✔
3771

6✔
3772
            const copyToClipboardWithHeaders = (
6✔
3773
                cells: readonly (readonly GridCell[])[],
5✔
3774
                columnIndexes: readonly number[]
5✔
3775
            ) => {
5✔
3776
                if (!copyHeaders) {
5✔
3777
                    copyToClipboard(cells, columnIndexes, e);
5✔
3778
                } else {
5!
3779
                    const headers = columnIndexes.map(index => ({
×
3780
                        kind: GridCellKind.Text,
×
3781
                        data: columnsIn[index].title,
×
3782
                        displayData: columnsIn[index].title,
×
3783
                        allowOverlay: false,
×
3784
                    })) as GridCell[];
×
3785
                    copyToClipboard([headers, ...cells], columnIndexes, e);
×
3786
                }
×
3787
            };
5✔
3788

6✔
3789
            if (focused && getCellsForSelection !== undefined) {
6✔
3790
                if (gridSelection.current !== undefined) {
6✔
3791
                    let thunk = getCellsForSelection(gridSelection.current.range, abortControllerRef.current.signal);
3✔
3792
                    if (typeof thunk !== "object") {
3!
3793
                        thunk = await thunk();
×
3794
                    }
×
3795
                    copyToClipboardWithHeaders(
3✔
3796
                        thunk,
3✔
3797
                        range(
3✔
3798
                            gridSelection.current.range.x - rowMarkerOffset,
3✔
3799
                            gridSelection.current.range.x + gridSelection.current.range.width - rowMarkerOffset
3✔
3800
                        )
3✔
3801
                    );
3✔
3802
                } else if (selectedRows !== undefined && selectedRows.length > 0) {
3✔
3803
                    const toCopy = [...selectedRows];
1✔
3804
                    const cells = toCopy.map(rowIndex => {
1✔
3805
                        const thunk = getCellsForSelection(
1✔
3806
                            {
1✔
3807
                                x: rowMarkerOffset,
1✔
3808
                                y: rowIndex,
1✔
3809
                                width: columnsIn.length,
1✔
3810
                                height: 1,
1✔
3811
                            },
1✔
3812
                            abortControllerRef.current.signal
1✔
3813
                        );
1✔
3814
                        if (typeof thunk === "object") {
1✔
3815
                            return thunk[0];
1✔
3816
                        }
1!
3817
                        return thunk().then(v => v[0]);
×
3818
                    });
1✔
3819
                    if (cells.some(x => x instanceof Promise)) {
1!
3820
                        const settled = await Promise.all(cells);
×
3821
                        copyToClipboardWithHeaders(settled, range(columnsIn.length));
×
3822
                    } else {
1✔
3823
                        copyToClipboardWithHeaders(cells as (readonly GridCell[])[], range(columnsIn.length));
1✔
3824
                    }
1✔
3825
                } else if (selectedColumns.length > 0) {
3✔
3826
                    const results: (readonly (readonly GridCell[])[])[] = [];
1✔
3827
                    const cols: number[] = [];
1✔
3828
                    for (const col of selectedColumns) {
1✔
3829
                        let thunk = getCellsForSelection(
3✔
3830
                            {
3✔
3831
                                x: col,
3✔
3832
                                y: 0,
3✔
3833
                                width: 1,
3✔
3834
                                height: rows,
3✔
3835
                            },
3✔
3836
                            abortControllerRef.current.signal
3✔
3837
                        );
3✔
3838
                        if (typeof thunk !== "object") {
3!
3839
                            thunk = await thunk();
×
3840
                        }
×
3841
                        results.push(thunk);
3✔
3842
                        cols.push(col - rowMarkerOffset);
3✔
3843
                    }
3✔
3844
                    if (results.length === 1) {
1!
3845
                        copyToClipboardWithHeaders(results[0], cols);
×
3846
                    } else {
1✔
3847
                        // FIXME: this is dumb
1✔
3848
                        const toCopy = results.reduce((pv, cv) => pv.map((row, index) => [...row, ...cv[index]]));
1✔
3849
                        copyToClipboardWithHeaders(toCopy, cols);
1✔
3850
                    }
1✔
3851
                }
1✔
3852
            }
6✔
3853
        },
6✔
3854
        [
771✔
3855
            columnsIn,
771✔
3856
            getCellsForSelection,
771✔
3857
            gridSelection,
771✔
3858
            keybindings.copy,
771✔
3859
            rowMarkerOffset,
771✔
3860
            scrollRef,
771✔
3861
            rows,
771✔
3862
            copyHeaders,
771✔
3863
        ]
771✔
3864
    );
771✔
3865

771✔
3866
    useEventListener("copy", onCopy, safeWindow, false, false);
771✔
3867

771✔
3868
    const onCut = React.useCallback(
771✔
3869
        async (e?: ClipboardEvent) => {
771✔
3870
            if (!keybindings.cut) return;
1!
3871
            const focused =
1✔
3872
                scrollRef.current?.contains(document.activeElement) === true ||
1✔
3873
                canvasRef.current?.contains(document.activeElement) === true;
1✔
3874

1✔
3875
            if (!focused) return;
1!
3876
            await onCopy(e);
1✔
3877
            if (gridSelection.current !== undefined) {
1✔
3878
                let effectiveSelection: GridSelection = {
1✔
3879
                    current: {
1✔
3880
                        cell: gridSelection.current.cell,
1✔
3881
                        range: gridSelection.current.range,
1✔
3882
                        rangeStack: [],
1✔
3883
                    },
1✔
3884
                    rows: CompactSelection.empty(),
1✔
3885
                    columns: CompactSelection.empty(),
1✔
3886
                };
1✔
3887
                const onDeleteResult = onDelete?.(effectiveSelection);
1✔
3888
                if (onDeleteResult === false) return;
1!
3889
                effectiveSelection = onDeleteResult === true ? effectiveSelection : onDeleteResult;
1!
3890
                if (effectiveSelection.current === undefined) return;
1!
3891
                deleteRange(effectiveSelection.current.range);
1✔
3892
            }
1✔
3893
        },
1✔
3894
        [deleteRange, gridSelection, keybindings.cut, onCopy, scrollRef, onDelete]
771✔
3895
    );
771✔
3896

771✔
3897
    useEventListener("cut", onCut, safeWindow, false, false);
771✔
3898

771✔
3899
    const onSearchResultsChanged = React.useCallback(
771✔
3900
        (results: readonly Item[], navIndex: number) => {
771✔
3901
            if (onSearchResultsChangedIn !== undefined) {
7!
3902
                if (rowMarkerOffset !== 0) {
×
3903
                    results = results.map(item => [item[0] - rowMarkerOffset, item[1]]);
×
3904
                }
×
3905
                onSearchResultsChangedIn(results, navIndex);
×
3906
                return;
×
3907
            }
×
3908
            if (results.length === 0 || navIndex === -1) return;
7✔
3909

2✔
3910
            const [col, row] = results[navIndex];
2✔
3911
            if (lastSent.current !== undefined && lastSent.current[0] === col && lastSent.current[1] === row) {
7!
3912
                return;
×
3913
            }
✔
3914
            lastSent.current = [col, row];
2✔
3915
            updateSelectedCell(col, row, false, false);
2✔
3916
        },
7✔
3917
        [onSearchResultsChangedIn, rowMarkerOffset, updateSelectedCell]
771✔
3918
    );
771✔
3919

771✔
3920
    // this effects purpose in life is to scroll the newly selected cell into view when and ONLY when that cell
771✔
3921
    // is from an external gridSelection change. Also note we want the unmangled out selection because scrollTo
771✔
3922
    // expects unmangled indexes
771✔
3923
    const [outCol, outRow] = gridSelectionOuter?.current?.cell ?? [];
771✔
3924
    const scrollToRef = React.useRef(scrollTo);
771✔
3925
    scrollToRef.current = scrollTo;
771✔
3926
    React.useLayoutEffect(() => {
771✔
3927
        if (
245✔
3928
            scrollToActiveCellRef.current &&
245✔
3929
            !hasJustScrolled.current &&
245✔
3930
            outCol !== undefined &&
245✔
3931
            outRow !== undefined &&
95✔
3932
            (outCol !== expectedExternalGridSelection.current?.current?.cell[0] ||
95✔
3933
                outRow !== expectedExternalGridSelection.current?.current?.cell[1])
95✔
3934
        ) {
245!
3935
            scrollToRef.current(outCol, outRow);
×
3936
        }
×
3937
        hasJustScrolled.current = false; //only allow skipping a single scroll
245✔
3938
    }, [outCol, outRow]);
771✔
3939

771✔
3940
    const selectionOutOfBounds =
771✔
3941
        gridSelection.current !== undefined &&
771✔
3942
        (gridSelection.current.cell[0] >= mangledCols.length || gridSelection.current.cell[1] >= mangledRows);
392✔
3943
    React.useLayoutEffect(() => {
771✔
3944
        if (selectionOutOfBounds) {
167✔
3945
            setGridSelection(emptyGridSelection, false);
1✔
3946
        }
1✔
3947
    }, [selectionOutOfBounds, setGridSelection]);
771✔
3948

771✔
3949
    const disabledRows = React.useMemo(() => {
771✔
3950
        if (showTrailingBlankRow === true && trailingRowOptions?.tint === true) {
160✔
3951
            return CompactSelection.fromSingleSelection(mangledRows - 1);
154✔
3952
        }
154✔
3953
        return CompactSelection.empty();
6✔
3954
    }, [mangledRows, showTrailingBlankRow, trailingRowOptions?.tint]);
771✔
3955

771✔
3956
    const mangledVerticalBorder = React.useCallback(
771✔
3957
        (col: number) => {
771✔
3958
            return typeof verticalBorder === "boolean"
8,236!
3959
                ? verticalBorder
×
3960
                : (verticalBorder?.(col - rowMarkerOffset) ?? true);
8,236!
3961
        },
8,236✔
3962
        [rowMarkerOffset, verticalBorder]
771✔
3963
    );
771✔
3964

771✔
3965
    const renameGroupNode = React.useMemo(() => {
771✔
3966
        if (renameGroup === undefined || canvasRef.current === null) return null;
159✔
3967
        const { bounds, group } = renameGroup;
1✔
3968
        const canvasBounds = canvasRef.current.getBoundingClientRect();
1✔
3969
        return (
1✔
3970
            <GroupRename
1✔
3971
                bounds={bounds}
1✔
3972
                group={group}
1✔
3973
                canvasBounds={canvasBounds}
1✔
3974
                onClose={() => setRenameGroup(undefined)}
1✔
3975
                onFinish={newVal => {
1✔
3976
                    setRenameGroup(undefined);
1✔
3977
                    onGroupHeaderRenamed?.(group, newVal);
1✔
3978
                }}
1✔
3979
            />
1✔
3980
        );
159✔
3981
    }, [onGroupHeaderRenamed, renameGroup]);
771✔
3982

771✔
3983
    const mangledFreezeColumns = Math.min(mangledCols.length, freezeColumns + (hasRowMarkers ? 1 : 0));
771✔
3984

771✔
3985
    React.useImperativeHandle(
771✔
3986
        forwardedRef,
771✔
3987
        () => ({
771✔
3988
            appendRow: (col: number, openOverlay?: boolean) => appendRow(col + rowMarkerOffset, openOverlay),
30✔
3989
            appendColumn: (row: number, openOverlay?: boolean) => appendColumn(row, openOverlay),
30✔
3990
            updateCells: damageList => {
30✔
3991
                if (rowMarkerOffset !== 0) {
2✔
3992
                    damageList = damageList.map(x => ({ cell: [x.cell[0] + rowMarkerOffset, x.cell[1]] }));
1✔
3993
                }
1✔
3994
                return gridRef.current?.damage(damageList);
2✔
3995
            },
2✔
3996
            getBounds: (col, row) => {
30✔
3997
                if (canvasRef?.current === null || scrollRef?.current === null) {
2!
3998
                    return undefined;
×
3999
                }
×
4000

2✔
4001
                if (col === undefined && row === undefined) {
2✔
4002
                    // Return the bounds of the entire scroll area:
1✔
4003
                    const rect = canvasRef.current.getBoundingClientRect();
1✔
4004
                    const scale = rect.width / scrollRef.current.clientWidth;
1✔
4005
                    return {
1✔
4006
                        x: rect.x - scrollRef.current.scrollLeft * scale,
1✔
4007
                        y: rect.y - scrollRef.current.scrollTop * scale,
1✔
4008
                        width: scrollRef.current.scrollWidth * scale,
1✔
4009
                        height: scrollRef.current.scrollHeight * scale,
1✔
4010
                    };
1✔
4011
                }
1✔
4012
                return gridRef.current?.getBounds((col ?? 0) + rowMarkerOffset, row);
2!
4013
            },
2✔
4014
            focus: () => gridRef.current?.focus(),
30✔
4015
            emit: async e => {
30✔
4016
                switch (e) {
5✔
4017
                    case "delete":
5✔
4018
                        onKeyDown({
1✔
4019
                            bounds: undefined,
1✔
4020
                            cancel: () => undefined,
1✔
4021
                            stopPropagation: () => undefined,
1✔
4022
                            preventDefault: () => undefined,
1✔
4023
                            ctrlKey: false,
1✔
4024
                            key: "Delete",
1✔
4025
                            keyCode: 46,
1✔
4026
                            metaKey: false,
1✔
4027
                            shiftKey: false,
1✔
4028
                            altKey: false,
1✔
4029
                            rawEvent: undefined,
1✔
4030
                            location: undefined,
1✔
4031
                        });
1✔
4032
                        break;
1✔
4033
                    case "fill-right":
5✔
4034
                        onKeyDown({
1✔
4035
                            bounds: undefined,
1✔
4036
                            cancel: () => undefined,
1✔
4037
                            stopPropagation: () => undefined,
1✔
4038
                            preventDefault: () => undefined,
1✔
4039
                            ctrlKey: true,
1✔
4040
                            key: "r",
1✔
4041
                            keyCode: 82,
1✔
4042
                            metaKey: false,
1✔
4043
                            shiftKey: false,
1✔
4044
                            altKey: false,
1✔
4045
                            rawEvent: undefined,
1✔
4046
                            location: undefined,
1✔
4047
                        });
1✔
4048
                        break;
1✔
4049
                    case "fill-down":
5✔
4050
                        onKeyDown({
1✔
4051
                            bounds: undefined,
1✔
4052
                            cancel: () => undefined,
1✔
4053
                            stopPropagation: () => undefined,
1✔
4054
                            preventDefault: () => undefined,
1✔
4055
                            ctrlKey: true,
1✔
4056
                            key: "d",
1✔
4057
                            keyCode: 68,
1✔
4058
                            metaKey: false,
1✔
4059
                            shiftKey: false,
1✔
4060
                            altKey: false,
1✔
4061
                            rawEvent: undefined,
1✔
4062
                            location: undefined,
1✔
4063
                        });
1✔
4064
                        break;
1✔
4065
                    case "copy":
5✔
4066
                        await onCopy(undefined, true);
1✔
4067
                        break;
1✔
4068
                    case "paste":
5✔
4069
                        await onPasteInternal();
1✔
4070
                        break;
1✔
4071
                }
5✔
4072
            },
5✔
4073
            scrollTo,
30✔
4074
            remeasureColumns: cols => {
30✔
4075
                for (const col of cols) {
1✔
4076
                    void normalSizeColumn(col + rowMarkerOffset);
1✔
4077
                }
1✔
4078
            },
1✔
4079
            getMouseArgsForPosition: (
30✔
4080
                posX: number,
×
4081
                posY: number,
×
4082
                ev?: MouseEvent | TouchEvent
×
4083
            ): GridMouseEventArgs | undefined => {
×
4084
                if (gridRef?.current === null) {
×
4085
                    return undefined;
×
4086
                }
×
4087

×
4088
                const args = gridRef.current.getMouseArgsForPosition(posX, posY, ev);
×
4089
                if (args === undefined) {
×
4090
                    return undefined;
×
4091
                }
×
4092

×
4093
                return {
×
4094
                    ...args,
×
4095
                    location: [args.location[0] - rowMarkerOffset, args.location[1]] as any,
×
4096
                };
×
4097
            },
×
4098
        }),
30✔
4099
        [
771✔
4100
            appendRow,
771✔
4101
            appendColumn,
771✔
4102
            normalSizeColumn,
771✔
4103
            scrollRef,
771✔
4104
            onCopy,
771✔
4105
            onKeyDown,
771✔
4106
            onPasteInternal,
771✔
4107
            rowMarkerOffset,
771✔
4108
            scrollTo,
771✔
4109
        ]
771✔
4110
    );
771✔
4111

771✔
4112
    const [selCol, selRow] = currentCell ?? [];
771✔
4113
    const onCellFocused = React.useCallback(
771✔
4114
        (cell: Item) => {
771✔
4115
            const [col, row] = cell;
30✔
4116

30✔
4117
            if (row === -1) {
30!
4118
                if (columnSelect !== "none") {
×
4119
                    setSelectedColumns(CompactSelection.fromSingleSelection(col), undefined, false);
×
4120
                    focus();
×
4121
                }
×
4122
                return;
×
4123
            }
×
4124

30✔
4125
            if (selCol === col && selRow === row) return;
30✔
4126
            setCurrent(
1✔
4127
                {
1✔
4128
                    cell,
1✔
4129
                    range: { x: col, y: row, width: 1, height: 1 },
1✔
4130
                },
1✔
4131
                true,
1✔
4132
                false,
1✔
4133
                "keyboard-nav"
1✔
4134
            );
1✔
4135
            scrollTo(col, row);
1✔
4136
        },
30✔
4137
        [columnSelect, focus, scrollTo, selCol, selRow, setCurrent, setSelectedColumns]
771✔
4138
    );
771✔
4139

771✔
4140
    const [isFocused, setIsFocused] = React.useState(false);
771✔
4141
    const setIsFocusedDebounced = React.useRef(
771✔
4142
        debounce((val: boolean) => {
771✔
4143
            setIsFocused(val);
57✔
4144
        }, 5)
771✔
4145
    );
771✔
4146

771✔
4147
    const onCanvasFocused = React.useCallback(() => {
771✔
4148
        setIsFocusedDebounced.current(true);
76✔
4149

76✔
4150
        // check for mouse state, don't do anything if the user is clicked to focus.
76✔
4151
        if (
76✔
4152
            gridSelection.current === undefined &&
76✔
4153
            gridSelection.columns.length === 0 &&
6✔
4154
            gridSelection.rows.length === 0 &&
5✔
4155
            mouseState === undefined
5✔
4156
        ) {
76✔
4157
            setCurrent(
5✔
4158
                {
5✔
4159
                    cell: [rowMarkerOffset, cellYOffset],
5✔
4160
                    range: {
5✔
4161
                        x: rowMarkerOffset,
5✔
4162
                        y: cellYOffset,
5✔
4163
                        width: 1,
5✔
4164
                        height: 1,
5✔
4165
                    },
5✔
4166
                },
5✔
4167
                true,
5✔
4168
                false,
5✔
4169
                "keyboard-select"
5✔
4170
            );
5✔
4171
        }
5✔
4172
    }, [cellYOffset, gridSelection, mouseState, rowMarkerOffset, setCurrent]);
771✔
4173

771✔
4174
    const onFocusOut = React.useCallback(() => {
771✔
4175
        setIsFocusedDebounced.current(false);
39✔
4176
    }, []);
771✔
4177

771✔
4178
    const [idealWidth, idealHeight] = React.useMemo(() => {
771✔
4179
        let h: number;
176✔
4180
        const scrollbarWidth = experimental?.scrollbarWidthOverride ?? getScrollBarWidth();
176✔
4181
        const rowsCountWithTrailingRow = rows + (showTrailingBlankRow ? 1 : 0);
176✔
4182
        if (typeof rowHeight === "number") {
176✔
4183
            h = totalHeaderHeight + rowsCountWithTrailingRow * rowHeight;
175✔
4184
        } else {
176✔
4185
            let avg = 0;
1✔
4186
            const toAverage = Math.min(rowsCountWithTrailingRow, 10);
1✔
4187
            for (let i = 0; i < toAverage; i++) {
1✔
4188
                avg += rowHeight(i);
10✔
4189
            }
10✔
4190
            avg = Math.floor(avg / toAverage);
1✔
4191

1✔
4192
            h = totalHeaderHeight + rowsCountWithTrailingRow * avg;
1✔
4193
        }
1✔
4194
        h += scrollbarWidth;
176✔
4195

176✔
4196
        const w = mangledCols.reduce((acc, x) => x.width + acc, 0) + scrollbarWidth;
176✔
4197

176✔
4198
        // We need to set a reasonable cap here as some browsers will just ignore huge values
176✔
4199
        // rather than treat them as huge values.
176✔
4200
        return [`${Math.min(100_000, w)}px`, `${Math.min(100_000, h)}px`];
176✔
4201
    }, [mangledCols, experimental?.scrollbarWidthOverride, rowHeight, rows, showTrailingBlankRow, totalHeaderHeight]);
771✔
4202

771✔
4203
    const cssStyle = React.useMemo(() => {
771✔
4204
        return makeCSSStyle(mergedTheme);
157✔
4205
    }, [mergedTheme]);
771✔
4206

771✔
4207
    return (
771✔
4208
        <ThemeContext.Provider value={mergedTheme}>
771✔
4209
            <DataEditorContainer
771✔
4210
                style={cssStyle}
771✔
4211
                className={className}
771✔
4212
                inWidth={width ?? idealWidth}
771✔
4213
                inHeight={height ?? idealHeight}>
771✔
4214
                <DataGridSearch
771✔
4215
                    fillHandle={fillHandle}
771✔
4216
                    drawFocusRing={drawFocusRing}
771✔
4217
                    experimental={experimental}
771✔
4218
                    fixedShadowX={fixedShadowX}
771✔
4219
                    fixedShadowY={fixedShadowY}
771✔
4220
                    getRowThemeOverride={getRowThemeOverride}
771✔
4221
                    headerIcons={headerIcons}
771✔
4222
                    imageWindowLoader={imageWindowLoader}
771✔
4223
                    initialSize={initialSize}
771✔
4224
                    isDraggable={isDraggable}
771✔
4225
                    onDragLeave={onDragLeave}
771✔
4226
                    onRowMoved={onRowMoved}
771✔
4227
                    overscrollX={overscrollX}
771✔
4228
                    overscrollY={overscrollY}
771✔
4229
                    preventDiagonalScrolling={preventDiagonalScrolling}
771✔
4230
                    rightElement={rightElement}
771✔
4231
                    rightElementProps={rightElementProps}
771✔
4232
                    smoothScrollX={smoothScrollX}
771✔
4233
                    smoothScrollY={smoothScrollY}
771✔
4234
                    className={className}
771✔
4235
                    enableGroups={enableGroups}
771✔
4236
                    onCanvasFocused={onCanvasFocused}
771✔
4237
                    onCanvasBlur={onFocusOut}
771✔
4238
                    canvasRef={canvasRef}
771✔
4239
                    onContextMenu={onContextMenu}
771✔
4240
                    theme={mergedTheme}
771✔
4241
                    cellXOffset={cellXOffset}
771✔
4242
                    cellYOffset={cellYOffset}
771✔
4243
                    accessibilityHeight={visibleRegion.height}
771✔
4244
                    onDragEnd={onDragEnd}
771✔
4245
                    columns={mangledCols}
771✔
4246
                    nonGrowWidth={nonGrowWidth}
771✔
4247
                    drawHeader={drawHeader}
771✔
4248
                    onColumnProposeMove={onColumnProposeMoveImpl}
771✔
4249
                    drawCell={drawCell}
771✔
4250
                    disabledRows={disabledRows}
771✔
4251
                    freezeColumns={mangledFreezeColumns}
771✔
4252
                    lockColumns={rowMarkerOffset}
771✔
4253
                    firstColAccessible={rowMarkerOffset === 0}
771✔
4254
                    getCellContent={getMangledCellContent}
771✔
4255
                    minColumnWidth={minColumnWidth}
771✔
4256
                    maxColumnWidth={maxColumnWidth}
771✔
4257
                    searchInputRef={searchInputRef}
771✔
4258
                    showSearch={showSearch}
771✔
4259
                    onSearchClose={onSearchClose}
771✔
4260
                    highlightRegions={highlightRegions}
771✔
4261
                    getCellsForSelection={getCellsForSelection}
771✔
4262
                    getGroupDetails={mangledGetGroupDetails}
771✔
4263
                    headerHeight={headerHeight}
771✔
4264
                    isFocused={isFocused}
771✔
4265
                    groupHeaderHeight={enableGroups ? groupHeaderHeight : 0}
771✔
4266
                    freezeTrailingRows={
771✔
4267
                        freezeTrailingRows + (showTrailingBlankRow && trailingRowOptions?.sticky === true ? 1 : 0)
771✔
4268
                    }
771✔
4269
                    hasAppendRow={showTrailingBlankRow}
771✔
4270
                    onColumnResize={onColumnResize}
771✔
4271
                    onColumnResizeEnd={onColumnResizeEnd}
771✔
4272
                    onColumnResizeStart={onColumnResizeStart}
771✔
4273
                    onCellFocused={onCellFocused}
771✔
4274
                    onColumnMoved={onColumnMovedImpl}
771✔
4275
                    onDragStart={onDragStartImpl}
771✔
4276
                    onHeaderMenuClick={onHeaderMenuClickInner}
771✔
4277
                    onHeaderIndicatorClick={onHeaderIndicatorClickInner}
771✔
4278
                    onItemHovered={onItemHoveredImpl}
771✔
4279
                    isFilling={mouseState?.fillHandle === true}
771✔
4280
                    onMouseMove={onMouseMoveImpl}
771✔
4281
                    onKeyDown={onKeyDown}
771✔
4282
                    onKeyUp={onKeyUpIn}
771✔
4283
                    onMouseDown={onMouseDown}
771✔
4284
                    onMouseUp={onMouseUp}
771✔
4285
                    onDragOverCell={onDragOverCell}
771✔
4286
                    onDrop={onDrop}
771✔
4287
                    onSearchResultsChanged={onSearchResultsChanged}
771✔
4288
                    onVisibleRegionChanged={onVisibleRegionChangedImpl}
771✔
4289
                    clientSize={clientSize}
771✔
4290
                    rowHeight={rowHeight}
771✔
4291
                    searchResults={searchResults}
771✔
4292
                    searchValue={searchValue}
771✔
4293
                    onSearchValueChange={onSearchValueChange}
771✔
4294
                    rows={mangledRows}
771✔
4295
                    scrollRef={scrollRef}
771✔
4296
                    selection={gridSelection}
771✔
4297
                    translateX={visibleRegion.tx}
771✔
4298
                    translateY={visibleRegion.ty}
771✔
4299
                    verticalBorder={mangledVerticalBorder}
771✔
4300
                    gridRef={gridRef}
771✔
4301
                    getCellRenderer={getCellRenderer}
771✔
4302
                    resizeIndicator={resizeIndicator}
771✔
4303
                />
771✔
4304
                {renameGroupNode}
771✔
4305
                {overlay !== undefined && (
771✔
4306
                    <React.Suspense fallback={null}>
37✔
4307
                        <DataGridOverlayEditor
37✔
4308
                            {...overlay}
37✔
4309
                            validateCell={validateCell}
37✔
4310
                            bloom={editorBloom}
37✔
4311
                            id={overlayID}
37✔
4312
                            getCellRenderer={getCellRenderer}
37✔
4313
                            className={experimental?.isSubGrid === true ? "click-outside-ignore" : undefined}
37!
4314
                            provideEditor={provideEditor}
37✔
4315
                            imageEditorOverride={imageEditorOverride}
37✔
4316
                            portalElementRef={portalElementRef}
37✔
4317
                            onFinishEditing={onFinishEditing}
37✔
4318
                            markdownDivCreateNode={markdownDivCreateNode}
37✔
4319
                            isOutsideClick={isOutsideClick}
37✔
4320
                            customEventTarget={experimental?.eventTarget}
37✔
4321
                        />
37✔
4322
                    </React.Suspense>
37✔
4323
                )}
771✔
4324
            </DataEditorContainer>
771✔
4325
        </ThemeContext.Provider>
771✔
4326
    );
771✔
4327
};
771✔
4328

1✔
4329
/**
1✔
4330
 * The primary component of Glide Data Grid.
1✔
4331
 * @category DataEditor
1✔
4332
 * @param {DataEditorProps} props
1✔
4333
 */
1✔
4334
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