@gridkitjs/dom 0.0.0-dev-20261106222533

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,901 @@
1
+ import { ColumnResizeSession, ColumnOrderState, GroupByState, GridFocus, NavigationModifiers, VisibleRange, ResolvedToolbarItem, PrintPage } from '@gridkitjs/core';
2
+
3
+ /** A live region to report changes through. */
4
+ interface LiveAnnouncer {
5
+ /**
6
+ * Has assistive technology read `text` out, politely — after whatever it is
7
+ * already saying. The same text twice in a row is read twice.
8
+ */
9
+ readonly announce: (text: string) => void;
10
+ /** Removes the live region. */
11
+ readonly destroy: () => void;
12
+ }
13
+ /**
14
+ * Creates the one live region a grid reports through — a reorder, a resize, a
15
+ * bulk selection, anything no visual cue can carry to a screen reader — as
16
+ * the last child of `container`.
17
+ *
18
+ * Polite rather than assertive, so an announcement waits for a pause instead
19
+ * of cutting across what the user is already hearing. The element carries
20
+ * `gridkit-sr-only`, which the theme clips to nothing while leaving it in the
21
+ * accessibility tree.
22
+ *
23
+ * A live region speaks only when its text changes, so a message identical to
24
+ * the last would go unheard. A no-break space, added and dropped in turn,
25
+ * makes the repeat a change without changing what is read.
26
+ */
27
+ declare function createLiveAnnouncer(container?: HTMLElement): LiveAnnouncer;
28
+
29
+ /** The data cell an event happened in. */
30
+ interface BodyCellTarget {
31
+ /** The row's place among every body row, from its `data-gridkit-row-index`. */
32
+ readonly rowIndex: number;
33
+ /** The cell's place in its row, off the table's own indices. */
34
+ readonly columnIndex: number;
35
+ readonly columnId: string;
36
+ }
37
+ /** The group-header row an event happened in. */
38
+ interface GroupRowTarget {
39
+ readonly rowIndex: number;
40
+ readonly groupId: string;
41
+ }
42
+ /** The body row an event happened in — a data row, or a group's header or summary. */
43
+ interface BodyRowTarget {
44
+ readonly rowIndex: number;
45
+ /** The column of the cell it happened in, `null` when it was in none. */
46
+ readonly columnId: string | null;
47
+ }
48
+ /**
49
+ * The data cell `target` is in — what a renderer delegating its body's
50
+ * events to one listener asks of each event. `closest` is what makes the
51
+ * whole cell the target: a click on a template's own markup resolves to the
52
+ * cell holding it.
53
+ *
54
+ * Never matches inside a group-header row: that row's single `<td>` carries
55
+ * no `data-gridkit-column` (it spans every column at once, so no one column
56
+ * id applies), which keeps this to data rows without a check of its own.
57
+ */
58
+ declare function bodyCellFrom(target: EventTarget | null): BodyCellTarget | null;
59
+ /** The group-header row (`tr[data-gridkit-group]`) `target` is in. */
60
+ declare function groupRowFrom(target: EventTarget | null): GroupRowTarget | null;
61
+ /**
62
+ * The row `target` is in, cell or not — all a row click needs. The column is
63
+ * read only off a cell of that very row, so a table inside a cell template
64
+ * cannot lend it one of its own.
65
+ */
66
+ declare function bodyRowFrom(target: EventTarget | null): BodyRowTarget | null;
67
+
68
+ interface ChecklistOptions {
69
+ /**
70
+ * The item at `index` — its place in the whole list — asks to be checked
71
+ * or unchecked: Space on it, or a click. The caller flips it in its own
72
+ * state and draws the new `aria-checked`.
73
+ */
74
+ readonly onToggle: (index: number) => void;
75
+ /** Focus moved to the item at `index`, by a key, a click or a script. */
76
+ readonly onFocusChange?: ((index: number) => void) | undefined;
77
+ /**
78
+ * Bring the item at `index` into view, mounting it if the list's window
79
+ * does not hold it. A virtualized renderer forwards this to its
80
+ * virtualizer's `scrollToIndex`, with `{ block: "nearest" }` so a move of
81
+ * one item scrolls by one item.
82
+ */
83
+ readonly scrollToIndex: (index: number) => void;
84
+ }
85
+ interface AttachedChecklist {
86
+ /**
87
+ * How many items the whole list holds, mounted or not. Starts at 0, so a
88
+ * renderer calls this straight after attaching, and again whenever the
89
+ * list changes — a search narrowing it, say. A focused place past the new
90
+ * end moves back to the last item.
91
+ */
92
+ readonly setCount: (count: number) => void;
93
+ /**
94
+ * Focuses the item at `index`, clamped to the list, scrolling to it first.
95
+ * An item outside the mounted window is focused once it mounts.
96
+ */
97
+ readonly focusItem: (index: number) => void;
98
+ /** Removes every listener and observer. The items keep their `tabindex`. */
99
+ readonly detach: () => void;
100
+ }
101
+ /**
102
+ * Makes `list` — a `role="listbox"` the renderer drew, usually virtualized
103
+ * so that only a window of a long list is mounted — behave as a
104
+ * multi-select checklist with one tab stop: the arrow keys, Home, End,
105
+ * PageUp and PageDown move focus among its items, and Space or a click asks
106
+ * for the focused item to be toggled. It decides; the caller is told through
107
+ * the callbacks and draws the outcome, `aria-checked` included.
108
+ *
109
+ * Items are the list's descendants with `data-gridkit-checklist-item` and a
110
+ * `data-gridkit-checklist-index`: their place in the whole list rather than
111
+ * in the window, since an item's place is all that survives it being
112
+ * unmounted and mounted again. The list is the element that scrolls: PageUp
113
+ * and PageDown move by as many items as it shows whole.
114
+ *
115
+ * One item at a time has `tabindex="0"` — the focused one, or the first
116
+ * mounted while the focused one is scrolled away — and the rest `-1`, kept
117
+ * so as items mount by an observer of the list's children rather than by
118
+ * the renderer. A move to an item outside the window asks `scrollToIndex`
119
+ * for it and focuses it once it mounts, unless focus has gone somewhere
120
+ * else in the meantime.
121
+ *
122
+ * Enter is not handled, so a form around the list hears it and applies;
123
+ * Escape is the overlay stack's.
124
+ */
125
+ declare function attachChecklist(list: HTMLElement, options: ChecklistOptions): AttachedChecklist;
126
+
127
+ /**
128
+ * How a context menu was asked for: by a pointer — a right-click, or a touch
129
+ * held still — or by the keyboard.
130
+ */
131
+ type ContextMenuSource = "pointer" | "keyboard";
132
+ /** A context menu asked for inside the root, as `onRequest` hears of it. */
133
+ interface ContextMenuRequest {
134
+ /** The element it was asked for on: the one under the pointer, or the one focused for a key. */
135
+ readonly target: Element;
136
+ /**
137
+ * Where the pointer was, in viewport coordinates — where a menu opened by
138
+ * it goes. `null` from the keyboard, which has no point: a menu it opens
139
+ * goes beside the focused element instead.
140
+ */
141
+ readonly point: {
142
+ readonly x: number;
143
+ readonly y: number;
144
+ } | null;
145
+ readonly source: ContextMenuSource;
146
+ }
147
+ interface ContextMenuTriggerOptions {
148
+ /**
149
+ * A context menu was asked for inside the root. Returns whether the caller
150
+ * opened one of its own: if so the browser's menu is prevented, and if not
151
+ * it shows as it would have.
152
+ */
153
+ readonly onRequest: (request: ContextMenuRequest) => boolean;
154
+ }
155
+ /**
156
+ * Reports every context menu the user asks for inside `root` — a right-click,
157
+ * Shift+F10, the ContextMenu key, or a touch held still for half a second —
158
+ * through `onRequest`, and returns the function that stops it.
159
+ *
160
+ * Each gesture asks once. Pressing a key, or holding a touch, also makes some
161
+ * browsers fire `contextmenu` of their own, and that event is taken, not
162
+ * reported again, once the gesture opened a menu. The click a touch brings
163
+ * as it lifts is taken too: the menu opened under the finger, and the click
164
+ * would land in it.
165
+ *
166
+ * An event something already prevented is left alone, so a cell template
167
+ * that shows a menu of its own keeps it. That is why the listeners are on the
168
+ * document, in the bubble phase: a framework that handles events at its own
169
+ * root container — React does — hears them before the document does, where
170
+ * a listener on `root` itself would hear them first. Only events whose target
171
+ * is inside `root` count.
172
+ */
173
+ declare function attachContextMenuTrigger(root: Element, options: ContextMenuTriggerOptions): () => void;
174
+
175
+ /**
176
+ * The `pointerdown` a gesture starts from. A native `PointerEvent` is one,
177
+ * and so is a framework's wrapper around it, such as React's synthetic
178
+ * event, so a renderer passes whichever its handler received.
179
+ */
180
+ interface PointerPress {
181
+ readonly pointerId: number;
182
+ readonly clientX: number;
183
+ readonly clientY: number;
184
+ readonly button: number;
185
+ readonly isPrimary: boolean;
186
+ preventDefault(): void;
187
+ }
188
+
189
+ interface ColumnResizeGestureHandlers {
190
+ /** Every pointer move, with the width the column takes there. */
191
+ readonly onMove: (width: number) => void;
192
+ /**
193
+ * The pointer was released. `width` is the width to commit, or `null` when
194
+ * it ended where it started: a click on the handle — as each half of a
195
+ * double-click is — is not a resize, and reporting one would fire a
196
+ * spurious resize on every click.
197
+ */
198
+ readonly onEnd: (width: number | null) => void;
199
+ /** Escape, or a cancelled pointer: the column goes back to `session.startWidth`. */
200
+ readonly onCancel: () => void;
201
+ }
202
+ /**
203
+ * Drags a column's edge from `press` on its resize `handle`, turning pointer
204
+ * positions into widths through `@gridkitjs/core`'s `session` — begun with
205
+ * `beginColumnResize` — and reporting them. Returns whether a resize began:
206
+ * only the primary button of the primary pointer starts one.
207
+ *
208
+ * Captures the pointer on `handle`, so the drag keeps reporting wherever the
209
+ * pointer goes, and prevents the press's default, so it does not select the
210
+ * header text it started on. No minimum distance, unlike a reorder drag: the
211
+ * handle does nothing else a press could have meant.
212
+ */
213
+ declare function startColumnResizeGesture(handle: HTMLElement, press: PointerPress, session: ColumnResizeSession, handlers: ColumnResizeGestureHandlers): boolean;
214
+ /**
215
+ * The width the widest cell in `columnId` needs to show its content in full,
216
+ * or `null` when the column has no cells — what double-clicking a resize
217
+ * handle sizes the column to, through `sizeColumnToContent`.
218
+ *
219
+ * Measured with a `Range` over each cell's contents rather than the cell's own
220
+ * `scrollWidth`. A range shrink-wraps what is actually laid out, so a column
221
+ * far wider than its content reports the content width and can be sized *down*;
222
+ * `scrollWidth` never reports less than the cell it is read from and so can
223
+ * only ever grow a column.
224
+ *
225
+ * Only rendered rows are measured — on a paged or virtualized grid, the ones
226
+ * on screen.
227
+ */
228
+ declare function measureColumnContent(table: HTMLTableElement, columnId: string): number | null;
229
+
230
+ /** What `writeClipboard` puts on the clipboard: the same copy as plain text and as HTML. */
231
+ interface ClipboardContent {
232
+ readonly text: string;
233
+ readonly html: string;
234
+ }
235
+ /**
236
+ * Whether a copy key pressed at `target` belongs to the browser: pressed in a
237
+ * text field, or with text highlighted anywhere on the page. Either way the
238
+ * user means the text in front of them, and a grid copying its own cell
239
+ * instead would take that from them.
240
+ */
241
+ declare function isNativeCopy(target: EventTarget | null): boolean;
242
+ /**
243
+ * Puts `content` on the clipboard as both `text/plain` and `text/html`, so a
244
+ * paste into a spreadsheet splits into cells and one into a word processor
245
+ * keeps its table. Call it from a user's gesture — a key press, a menu
246
+ * choice — as browsers allow the clipboard only then.
247
+ *
248
+ * Tries each way a browser offers, best first: the async Clipboard API with
249
+ * a `ClipboardItem` holding both types; then its `writeText`, plain text
250
+ * alone, where `ClipboardItem` is missing or refused; then the legacy `copy`
251
+ * event, for a page outside a secure context, where the async API is not
252
+ * there at all. Rejects only when every one fails.
253
+ */
254
+ declare function writeClipboard(content: ClipboardContent): Promise<void>;
255
+
256
+ /**
257
+ * Saves `blob` as a file named `fileName` — the browser's own download, as a
258
+ * link with a `download` attribute starts one, so nothing is opened or
259
+ * navigated to.
260
+ *
261
+ * The link is put in the document for the click, as Firefox has needed, and
262
+ * taken out straight after. Its object URL is revoked a task later rather
263
+ * than at once: Safari starts the download from the URL after the click
264
+ * returns, and a URL revoked synchronously is gone before it does.
265
+ */
266
+ declare function downloadBlob(blob: Blob, fileName: string): void;
267
+
268
+ /** What a renderer draws while a reorder drag is open. */
269
+ interface DragReorderState<Target> {
270
+ /** The item being dragged, once the press has become a drag. */
271
+ readonly draggedId: string | null;
272
+ /** Where it would land if released now, `null` for nowhere valid. */
273
+ readonly dropTarget: Target | null;
274
+ /** Where the element trailing the pointer sits, as a CSS `transform`. */
275
+ readonly ghostTransform: string | null;
276
+ }
277
+ /** The state while nothing is dragged — the one a drag reports when it ends. */
278
+ declare const NO_DRAG: DragReorderState<never>;
279
+ interface DragReorderHandlers<Target> {
280
+ /**
281
+ * Where a drop at `(clientX, clientY)` would land, or `null` for nowhere
282
+ * valid — including a gap `id` already occupies, so a drop target only
283
+ * ever promises a move that happens. Called on every pointer move once the
284
+ * drag has opened. `resolveColumnDropTarget` and
285
+ * `resolveGroupByDropTarget` are the grid's own.
286
+ */
287
+ readonly resolveTarget: (clientX: number, clientY: number, id: string) => Target | null;
288
+ /** Every change to what the renderer draws, `NO_DRAG` once the drag ends. */
289
+ readonly onState: (state: DragReorderState<Target>) => void;
290
+ /** The pointer was released over a valid target. */
291
+ readonly onDrop: (target: Target, id: string) => void;
292
+ }
293
+ /**
294
+ * The pointer mechanics behind every drag-to-reorder gesture in the grid — a
295
+ * column header dragged among the others or into the group-by bar, a
296
+ * group-by chip dragged within it: capture, the drag threshold, and the ghost
297
+ * trailing the pointer, holding the spot it was picked up by. Returns whether
298
+ * a gesture began; only the main button of the primary pointer starts one.
299
+ *
300
+ * The press is left to its default, so the pressed element still takes focus;
301
+ * until the pointer has travelled `DRAG_THRESHOLD` pixels nothing is
302
+ * reported, so a press that never moves stays a click. Escape, or a cancelled
303
+ * pointer, leaves the order as it was.
304
+ *
305
+ * Pointer events rather than HTML5 drag-and-drop: `dragenter`/`dragleave`
306
+ * fire per descendant, so a dragged item's own inner markup would each report
307
+ * the target leaving and re-entering as the pointer crossed them.
308
+ */
309
+ declare function startDragReorder<Target>(id: string, grabElement: HTMLElement, press: PointerPress, handlers: DragReorderHandlers<Target>): boolean;
310
+
311
+ /** A drop into the group-by bar, in front of `beforeColumnId` — `null` to append. */
312
+ interface GroupByDropTarget {
313
+ readonly kind: "group-by";
314
+ readonly beforeColumnId: string | null;
315
+ }
316
+ /**
317
+ * Where a dragged column header would land: among the other headers, in
318
+ * front of `beforeId` (`null` for the end), or in the group-by bar.
319
+ */
320
+ type ColumnDropTarget = {
321
+ readonly kind: "column-order";
322
+ readonly beforeId: string | null;
323
+ } | GroupByDropTarget;
324
+ interface ColumnDropContext {
325
+ /** The ids of the columns on screen, in order — what a drop is resolved among. */
326
+ readonly order: ColumnOrderState;
327
+ /** What a drop onto the group-by bar is resolved against. */
328
+ readonly groupBy: GroupByState;
329
+ /** Column ids whose header may be dragged into the group-by bar at all. */
330
+ readonly groupByDraggableIds: ReadonlySet<string>;
331
+ }
332
+ /**
333
+ * Where a header or chip dropped at `(clientX, clientY)` would land within
334
+ * the group-by bar (`[data-gridkit-group-by-bar]`, its chips
335
+ * `[data-gridkit-group-chip]`), or `null` when the pointer isn't over the bar
336
+ * at all, or when the drop would change nothing. Shared by a header dragged
337
+ * in from outside the bar and a chip dragged within it, so the two can never
338
+ * disagree about where a drop lands.
339
+ */
340
+ declare function resolveGroupByDropTarget(clientX: number, clientY: number, movedId: string, groupBy: GroupByState): GroupByDropTarget | null;
341
+ /**
342
+ * Where the header of `movedId` dropped at `(clientX, clientY)` would land:
343
+ * before or after another header (`th[data-gridkit-column]`), by which half
344
+ * of it the pointer is over, or else in the group-by bar — or `null` for
345
+ * nowhere a drop would change anything.
346
+ *
347
+ * A column already grouped never targets the bar: reordering it there is its
348
+ * chip's job, not its header's.
349
+ */
350
+ declare function resolveColumnDropTarget(clientX: number, clientY: number, movedId: string, { order, groupBy, groupByDraggableIds }: ColumnDropContext): ColumnDropTarget | null;
351
+
352
+ /**
353
+ * Reports `element`'s inner width now and on every change after, until the
354
+ * returned function disconnects it — what `resizeMode: "fit"` fills.
355
+ *
356
+ * The first report is synchronous, so a renderer calling this before paint
357
+ * lays its columns out at the right width from the first frame rather than
358
+ * visibly jumping once the observer's first callback arrives.
359
+ *
360
+ * Reads `clientWidth` rather than the observer entry's `contentRect` so the
361
+ * first report and every later one come from the same, already-rounded,
362
+ * source.
363
+ */
364
+ declare function observeElementWidth(element: HTMLElement, onWidth: (width: number) => void): () => void;
365
+
366
+ /**
367
+ * The window of rows a row virtualizer has drawn, and a way to bring a row
368
+ * into it — what a focus move needs when its target row may not be mounted.
369
+ */
370
+ interface FocusVirtualRange {
371
+ readonly startIndex: number;
372
+ readonly endIndex: number;
373
+ readonly scrollToIndex: (index: number) => void;
374
+ }
375
+ /** How to find the rendered rows the browser's focus moves among. */
376
+ interface GridFocusRows {
377
+ /** The header row's element, `null` while none is rendered. */
378
+ readonly getHeaderRow: () => HTMLTableRowElement | null;
379
+ /** Body row `rowIndex`'s element — `findBodyRow` finds it — `null` while it is not rendered. */
380
+ readonly getBodyRow: (rowIndex: number) => HTMLTableRowElement | null;
381
+ }
382
+ interface GridFocusOptions {
383
+ /** Body rows, counting group headers and summaries — every row a key could step through. */
384
+ readonly rowCount: number;
385
+ readonly columnCount: number;
386
+ /**
387
+ * Rows the vertical keys step over rather than stop on — a group's own
388
+ * summary row, which takes a row slot but is not a stop of its own.
389
+ */
390
+ readonly isSkippableRow?: ((rowIndex: number) => boolean) | undefined;
391
+ /**
392
+ * The window the renderer drew, when rows are virtualized. A move can
393
+ * target a row outside it — `ArrowDown` past its end, or `Ctrl+End` — that
394
+ * no lookup would find until it is scrolled into range.
395
+ */
396
+ readonly virtualRange?: FocusVirtualRange | null | undefined;
397
+ }
398
+ interface GridFocusController {
399
+ /**
400
+ * The cell holding the grid's single tab stop, clamped into the current
401
+ * row and column counts. The same object until it changes, so it can serve
402
+ * as a renderer's snapshot.
403
+ */
404
+ readonly getFocus: () => GridFocus;
405
+ /** Calls `listener` whenever the focused cell moves. Returns the unsubscribe. */
406
+ readonly subscribe: (listener: () => void) => () => void;
407
+ /**
408
+ * Replaces the options, as a renderer does on every render with its
409
+ * current counts and window. Calls no listener: the renderer calling it is
410
+ * already drawing.
411
+ */
412
+ readonly setOptions: (options: GridFocusOptions) => void;
413
+ /**
414
+ * Moves the tab stop, and the browser's focus with it once the renderer
415
+ * has drawn the move (see `focusDrawnCell`). Moving to the cell already
416
+ * focused does nothing, which is what lets a cell's own `focus` event
417
+ * report a click through here without a loop.
418
+ */
419
+ readonly focusCell: (rowIndex: number, columnIndex: number) => void;
420
+ /**
421
+ * Moves the tab stop as `key` does — the arrows, Home, End, the page keys,
422
+ * with Ctrl for the grid's corners. Returns whether the key was one of
423
+ * them, in which case the caller prevents its default: taken even when the
424
+ * move lands nowhere, or an arrow at the last row would scroll the page
425
+ * out from under the grid.
426
+ */
427
+ readonly handleKey: (key: string, modifiers: NavigationModifiers, pageSize: number) => boolean;
428
+ /**
429
+ * Connects the tab stop to the rendered rows, which the browser's focus
430
+ * then follows it among. Returns the function that disconnects them.
431
+ */
432
+ readonly attach: (rows: GridFocusRows) => () => void;
433
+ /**
434
+ * Called by the renderer once it has drawn `drawn` — a value `getFocus`
435
+ * returned — so the browser's focus follows a move to the cell now in the
436
+ * markup. Does nothing unless a move to exactly that drawing is pending.
437
+ */
438
+ readonly focusDrawnCell: (drawn: GridFocus) => void;
439
+ }
440
+ /**
441
+ * Body row `rowIndex` in `bodyTable`, by its `data-gridkit-row-index` rather
442
+ * than by its position in `tBodies[0].rows`: a virtualized body holds only a
443
+ * slice of the rows, behind a leading spacer `<tr>`, so once the window
444
+ * scrolls a DOM position no longer matches the row's index in the full array
445
+ * and addresses some other mounted row, or none.
446
+ *
447
+ * The attribute holds a plain integer, so the selector needs no escaping,
448
+ * unlike one built from a consumer's row id.
449
+ */
450
+ declare function findBodyRow(bodyTable: HTMLTableElement | null, rowIndex: number): HTMLTableRowElement | null;
451
+ /**
452
+ * How many rows fit `bodyTable`'s scroll wrapper — its parent, the row area
453
+ * alone, not the header, footer and pager around it — which is what a page
454
+ * key moves. Reads the first rendered row's height, and falls back to ten
455
+ * rows when nothing can be measured.
456
+ */
457
+ declare function measurePageSize(bodyTable: HTMLTableElement | null): number;
458
+ /**
459
+ * The grid's single tab stop and the keys that move it, which is what
460
+ * `role="grid"` obliges: arrow keys travel cell to cell and Tab passes the
461
+ * whole grid by. The renderer attaches its rows, draws `getFocus()` —
462
+ * `tabindex="0"` on that cell, `-1` on every other — re-reads it when
463
+ * `subscribe` calls, and calls `focusDrawnCell` after drawing.
464
+ *
465
+ * Separate from selection, because focus and selection are different states:
466
+ * a cell is focused while travelling before anything is selected, and every
467
+ * cell of a range is selected without being focused. Navigation also has to
468
+ * work with selection off entirely.
469
+ *
470
+ * Starts on the header row's first cell.
471
+ */
472
+ declare function createGridFocus(initialOptions: GridFocusOptions): GridFocusController;
473
+
474
+ interface InfiniteScrollOptions {
475
+ /**
476
+ * The element whose approach triggers a load — the always-last row of the
477
+ * body — or `null` while none is rendered.
478
+ */
479
+ readonly sentinel: Element | null;
480
+ /** Whether a further load could still return more rows. `false` stops watching altogether. */
481
+ readonly hasMore: boolean;
482
+ /**
483
+ * Called at most once per approach: when the sentinel comes within
484
+ * `rootMargin` of the root's visible area, and not again until
485
+ * `isLoadingMore` has gone from `true` back to `false`.
486
+ */
487
+ readonly onLoadMore?: (() => void) | undefined;
488
+ /** Whether a load `onLoadMore` started is still in flight. */
489
+ readonly isLoadingMore?: boolean | undefined;
490
+ /** How far ahead of the sentinel scrolling into view a load starts, as CSS margins. Defaults to `"200px"`. */
491
+ readonly rootMargin?: string | undefined;
492
+ }
493
+ interface InfiniteScroll {
494
+ /**
495
+ * Takes the current options. Cheap when nothing changed, so a renderer may
496
+ * call it after every render; a new sentinel or `rootMargin` restarts the
497
+ * observer, and `isLoadingMore` going back to `false` re-checks at once.
498
+ */
499
+ readonly update: (options: InfiniteScrollOptions) => void;
500
+ /** Stops watching for good. */
501
+ readonly detach: () => void;
502
+ }
503
+ /**
504
+ * Turns a sentinel's approach to the bottom of the scrolling `root` into
505
+ * `onLoadMore` calls, no more than one per "the sentinel is near and no load
506
+ * is in flight since the last one finished".
507
+ *
508
+ * Three facts drive it: whether the sentinel is intersecting (as of the
509
+ * observer's last report), whether `onLoadMore` has fired since the last
510
+ * load finished, and whether a load was in flight at the last update. They
511
+ * outlive the sentinel itself, which a renderer mounts and unmounts as
512
+ * `hasMore` changes, so they live here rather than on any one observer.
513
+ *
514
+ * Off means off: with `hasMore` false, or no sentinel, no observer exists.
515
+ */
516
+ declare function attachInfiniteScroll(root: Element, options: InfiniteScrollOptions): InfiniteScroll;
517
+
518
+ /**
519
+ * Where focus goes when a submenu opens: its first item, or nowhere — it
520
+ * stays on the item that opened it, as when the pointer opened it.
521
+ */
522
+ type MenuSubmenuFocus = "first" | "none";
523
+ /** Why a menu asked for every menu to close. */
524
+ type MenuCloseReason = "tab";
525
+ interface MenuOptions {
526
+ /**
527
+ * An enabled item that opens no submenu was chosen — Enter, Space or a
528
+ * click. The caller runs it, and closes every menu.
529
+ */
530
+ readonly onActivate: (id: string) => void;
531
+ /**
532
+ * An enabled submenu item asks for its submenu: Enter, Space or ArrowRight
533
+ * on it want its first item focused (`"first"`), a click or the pointer
534
+ * resting on it want focus left where it is (`"none"`). It may come for a
535
+ * submenu already open — ArrowRight on one the pointer opened — when a
536
+ * `"first"` asks for focus to move into it (`focusItem(0)` on its menu).
537
+ * Opening it closes any other submenu open from this menu.
538
+ */
539
+ readonly onOpenSubmenu?: ((id: string, options: {
540
+ readonly focus: MenuSubmenuFocus;
541
+ }) => void) | undefined;
542
+ /**
543
+ * The pointer came to rest on an item that opens no submenu, or a disabled
544
+ * one: close whatever submenu is open from this menu, if any. Moving on to
545
+ * another item is how a pointer user leaves a submenu they did not want.
546
+ */
547
+ readonly onCollapseSubmenu?: (() => void) | undefined;
548
+ /**
549
+ * ArrowLeft was pressed in this menu, which is a submenu: close it, and
550
+ * focus goes back to the item that opened it. Passed only for a submenu —
551
+ * in a menu without it, ArrowLeft does nothing.
552
+ */
553
+ readonly onCloseSubmenu?: (() => void) | undefined;
554
+ /**
555
+ * Tab or Shift+Tab was pressed: close every menu, so focus returns to where
556
+ * the first one was opened from rather than moving on past it.
557
+ */
558
+ readonly onClose: (reason: MenuCloseReason) => void;
559
+ }
560
+ interface AttachedMenu {
561
+ /**
562
+ * Focuses an item, by its `data-gridkit-menu-item` id or its place among
563
+ * the items. Does nothing for an item the menu does not have.
564
+ */
565
+ readonly focusItem: (item: string | number) => void;
566
+ /** Removes every listener. The items keep the `tabindex` they had. */
567
+ readonly detach: () => void;
568
+ }
569
+ /**
570
+ * Makes `panel` — a `role="menu"` element the renderer drew — behave as a
571
+ * menu: the arrow keys, Home, End and typeahead move focus among its items,
572
+ * Enter, Space and a click choose one, ArrowRight and ArrowLeft open and
573
+ * close submenus, and the pointer moves focus to the item under it. It
574
+ * decides; the caller is told through the callbacks and draws the outcome —
575
+ * runs the item, opens or closes a submenu.
576
+ *
577
+ * Items are the panel's descendants with a menu item role (`menuitem`,
578
+ * `menuitemcheckbox`, `menuitemradio`) and a `data-gridkit-menu-item` id,
579
+ * read afresh on every key so items drawn since attaching count. A submenu
580
+ * item says so with `aria-haspopup="menu"`; an item with
581
+ * `aria-disabled="true"` takes focus, so a screen reader finds it, but is
582
+ * never chosen. One item at a time has `tabindex="0"` — the focused one, or
583
+ * the first — and the rest `-1`: every item can take focus, and the first is
584
+ * what an overlay's initial focus finds when the menu is attached before it.
585
+ *
586
+ * Escape is not handled: a menu is shown with `attachOverlay`, whose stack
587
+ * gives Escape to the topmost overlay. A submenu is a menu of its own, in an
588
+ * overlay of its own anchored to its item, attached when it opens.
589
+ */
590
+ declare function attachMenu(panel: HTMLElement, options: MenuOptions): AttachedMenu;
591
+
592
+ /** Which side of its anchor an overlay opens on. */
593
+ type OverlaySide = "top" | "bottom" | "left" | "right";
594
+ /**
595
+ * Which edge of the anchor the overlay lines up with along that side: its
596
+ * left edge (`start`) or right edge (`end`) for `top`/`bottom`, its top or
597
+ * bottom edge for `left`/`right`.
598
+ */
599
+ type OverlayAlign = "start" | "end";
600
+ type OverlayPlacement = `${OverlaySide}-${OverlayAlign}`;
601
+ /** A box in viewport coordinates, as `getBoundingClientRect` reports one. */
602
+ interface OverlayRect {
603
+ readonly top: number;
604
+ readonly left: number;
605
+ readonly width: number;
606
+ readonly height: number;
607
+ }
608
+ interface OverlaySize {
609
+ readonly width: number;
610
+ readonly height: number;
611
+ }
612
+ interface OverlayPlacementOptions {
613
+ /** Where the overlay would rather open. Defaults to `"bottom-start"`. */
614
+ readonly placement?: OverlayPlacement | undefined;
615
+ /** Space between the anchor and the overlay, in pixels. Defaults to `0`. */
616
+ readonly gap?: number | undefined;
617
+ /**
618
+ * Pixels to shift the overlay along the anchor's edge — down beside a
619
+ * `left`/`right` side, right beside `top`/`bottom`; negative goes the other
620
+ * way. A submenu opened `right-start` takes its own top padding, negated,
621
+ * so its first item lines up with the item that opened it. The overlay is
622
+ * still kept on screen. Defaults to `0`.
623
+ */
624
+ readonly offset?: number | undefined;
625
+ }
626
+ /** Where an overlay goes: its top-left corner, and the placement it ended up with. */
627
+ interface OverlayPosition {
628
+ readonly top: number;
629
+ readonly left: number;
630
+ readonly placement: OverlayPlacement;
631
+ }
632
+ /**
633
+ * Where an overlay of size `panel` goes beside `anchor` so that it stays in
634
+ * `viewport`: on the preferred side unless only the opposite one has room,
635
+ * lined up with the preferred edge unless that runs it off screen and the
636
+ * other edge does not, and finally pushed back inside if it still overflows.
637
+ * Pure, so the rules are testable without a browser; `attachOverlay` feeds it
638
+ * the measurements.
639
+ *
640
+ * A point — a context menu opened where the pointer is — is an anchor with
641
+ * no width or height, which is what makes `start`/`end` mean "after" and
642
+ * "before" it.
643
+ */
644
+ declare function placeOverlay(anchor: OverlayRect, panel: OverlaySize, viewport: OverlaySize, options?: OverlayPlacementOptions): OverlayPosition;
645
+
646
+ /** What an overlay opens beside: an element, or a point in viewport coordinates. */
647
+ type OverlayAnchor = Element | {
648
+ readonly x: number;
649
+ readonly y: number;
650
+ };
651
+ /**
652
+ * Where focus goes when an overlay attaches: its first focusable descendant,
653
+ * the panel itself, nowhere — focus stays where it was — or a given element.
654
+ */
655
+ type OverlayInitialFocus = "first" | "panel" | "none" | HTMLElement;
656
+ /** Why an overlay asked to close. */
657
+ type OverlayDismissReason = "escape" | "outside" | "scroll" | "resize" | "blur" | "parent";
658
+ interface OverlayOptions extends OverlayPlacementOptions {
659
+ /**
660
+ * The overlay asks to close: Escape was pressed while it was the topmost
661
+ * overlay, a pointer went down outside it, something outside it scrolled,
662
+ * the window resized or lost focus, focus moved outside it, or the overlay
663
+ * it was opened above detached. The caller closes it — removes the panel
664
+ * and detaches — or keeps it open by doing nothing, except after
665
+ * `"parent"`, which detaches it regardless.
666
+ */
667
+ readonly onDismiss: (reason: OverlayDismissReason) => void;
668
+ /**
669
+ * Where focus goes on attaching. Defaults to `"first"`: a menu focuses its
670
+ * first item. A panel whose first control is not where the user starts — a
671
+ * filter editor's operator picker before its value box — passes that
672
+ * element, or `"panel"` to land on the panel itself; `"none"` leaves focus
673
+ * where it was, as a suggestion list does while typing goes on in the
674
+ * field that opened it.
675
+ */
676
+ readonly initialFocus?: OverlayInitialFocus | undefined;
677
+ /**
678
+ * Where focus goes when the overlay detaches. Defaults to the element that
679
+ * had focus when it attached; `null` leaves focus alone.
680
+ */
681
+ readonly returnFocusTo?: HTMLElement | null | undefined;
682
+ /**
683
+ * Whether a scroll outside the panel just after input inside it — an
684
+ * `input` or `change` event, in the two animation frames that follow it —
685
+ * leaves it open. A panel whose controls change the page under it opts
686
+ * in: a column chooser hiding a column narrows the grid it opened from,
687
+ * and the browser scrolls that grid back within its narrower content.
688
+ * Defaults to `false`, where every scroll outside the panel dismisses it.
689
+ */
690
+ readonly ignoreScrollAfterInput?: boolean | undefined;
691
+ /**
692
+ * Whether Tab and Shift+Tab stay inside the panel: from its last tabbable
693
+ * element Tab moves to its first, and Shift+Tab from its first to its last,
694
+ * as a dialog keeps them. Defaults to `false`, where tabbing out of the
695
+ * panel moves focus outside it, which dismisses it with `"blur"`. Only
696
+ * the keys are kept: a pointer down outside still dismisses it, and so
697
+ * does focus moved out by any other route.
698
+ */
699
+ readonly trapFocus?: boolean | undefined;
700
+ }
701
+ /**
702
+ * Shows `panel` as an overlay beside `anchor` — a menu, a popover, a filter
703
+ * editor — and returns the function that ends it. The caller renders the
704
+ * panel, usually as a child of `document.body` so no scrolling ancestor clips
705
+ * it; this positions it, moves focus into it, and reports through
706
+ * `onDismiss` when the user is done with it.
707
+ *
708
+ * The panel is positioned `fixed`, at the place `placeOverlay` picks from the
709
+ * anchor's and the panel's measured boxes, and carries that placement in
710
+ * `data-gridkit-placement` (`"top-start"` once flipped above, say) for a
711
+ * stylesheet to point an arrow by. Whenever the panel's size changes — a list
712
+ * that shrinks under a search — it is placed again beside the anchor, so a
713
+ * panel flipped above its anchor keeps its bottom edge there — or beside
714
+ * where the anchor was, once it has left the page. Focus moves
715
+ * where `initialFocus` says.
716
+ *
717
+ * Nothing that happens on or in the anchor dismisses the overlay, so a button
718
+ * that toggles it can close it with its own click rather than having the
719
+ * overlay close and the click reopen it.
720
+ *
721
+ * Overlays stack in the order they attach. Escape belongs to the topmost
722
+ * alone, which takes it: it closes a submenu without closing the menu under
723
+ * it, and nothing else on the page — a grid that clears its selection on
724
+ * Escape — hears it. An overlay counts everything in the overlays above it
725
+ * as inside itself, so a click in a submenu keeps its menu open while a click
726
+ * elsewhere in the menu closes only the submenu.
727
+ *
728
+ * Detaching removes every listener and returns focus, but only when focus is
729
+ * still in the panel, in an overlay above it, or nowhere — a click that
730
+ * dismissed it by landing on another control leaves focus on that control.
731
+ * Any overlay still above it is dismissed with `"parent"` and detached first.
732
+ */
733
+ declare function attachOverlay(anchor: OverlayAnchor, panel: HTMLElement, options: OverlayOptions): () => void;
734
+
735
+ interface RowVirtualizerOptions {
736
+ /** The height assumed for a row not yet measured. */
737
+ readonly estimatedRowHeight: number;
738
+ /** Rows rendered beyond each edge of the visible area. */
739
+ readonly overscan: number;
740
+ /**
741
+ * The window of rows to render moved: called whenever a scroll, a resize
742
+ * of the scroll element, a measured row height or a change of rows moves
743
+ * it, and never with a window equal to the last.
744
+ */
745
+ readonly onRange: (range: VisibleRange) => void;
746
+ }
747
+ interface RowVirtualizer {
748
+ /** How many rows there are. Measured heights of the rows that remain are kept. */
749
+ readonly setRowCount: (count: number) => void;
750
+ /** Forgets every measured height, for rows that are no longer the ones measured. */
751
+ readonly resetHeights: () => void;
752
+ /**
753
+ * A ref callback for the element of row `index` (its place among all the
754
+ * rows, not among those rendered): measured while mounted, forgotten once
755
+ * called with `null`. The same function for the same index every time, so
756
+ * a renderer re-attaching it on every render costs nothing.
757
+ */
758
+ readonly measureRow: (index: number) => (element: HTMLElement | null) => void;
759
+ /**
760
+ * Scrolls so row `index` is in view. A mounted row is scrolled to directly;
761
+ * one outside the window is jumped to at its estimated offset, then
762
+ * scrolled to properly once it mounts and can be measured.
763
+ */
764
+ readonly scrollToIndex: (index: number, options?: ScrollIntoViewOptions) => void;
765
+ /** Stops listening and measuring, for good. */
766
+ readonly destroy: () => void;
767
+ }
768
+ /**
769
+ * Windows a long list of rows inside `scrollElement` down to those near its
770
+ * scroll position, measuring each rendered row's real height rather than
771
+ * assuming a uniform one — rows are not uniform (`.is-wrapped` cells, group
772
+ * header and summary rows), so a fixed-height virtualizer would either break
773
+ * wrapping or rule it out. The renderer draws `onRange`'s window, with
774
+ * spacers of `offsetBefore`/`offsetAfter` around it, and hands each row's
775
+ * element to `measureRow`.
776
+ *
777
+ * The heights are a mutable array, not a structure rebuilt per measurement:
778
+ * one can arrive many times a second while scrolling. The pure arithmetic
779
+ * over them is `@gridkitjs/core`'s `visibleRange`; this owns only the
780
+ * mutability around it and the listeners that feed it — the scroll position,
781
+ * the scroll element's height, and one `ResizeObserver` shared by every
782
+ * mounted row.
783
+ */
784
+ declare function createRowVirtualizer(scrollElement: HTMLElement, options: RowVirtualizerOptions): RowVirtualizer;
785
+
786
+ /**
787
+ * Makes `toolbar` — a `role="toolbar"` element the renderer drew — one tab
788
+ * stop that the arrow keys move within, following the WAI-ARIA toolbar
789
+ * pattern: ArrowRight and ArrowLeft move focus to the next and previous
790
+ * control, wrapping round the ends, and Home and End to the first and last.
791
+ * Returns the function that stops it.
792
+ *
793
+ * The toolbar's entries are its descendants with a `data-gridkit-toolbar-item`,
794
+ * and each entry's control is the entry itself when it can take focus, else
795
+ * the first element in it that can; an entry with none — a separator, a
796
+ * spacer — is passed over. They are read afresh on every key. One control at
797
+ * a time has `tabindex="0"` — the focused one, or the first — and the rest
798
+ * `-1`, kept so as entries come and go; a control with
799
+ * `aria-disabled="true"` stays one of them, so a screen reader finds it.
800
+ *
801
+ * A text field keeps the keys it needs: Home and End, and an arrow while
802
+ * the caret has text to move through in that direction. At the start or end
803
+ * of its text, the arrow moves on to the next control.
804
+ */
805
+ declare function attachToolbar(toolbar: HTMLElement): () => void;
806
+
807
+ /**
808
+ * A toolbar entry as the overflow reads it: its kind, the key its element
809
+ * carries and, for a button or a dropdown, its label — a change to which,
810
+ * while the entry is in "More" and not drawn, makes the width remembered for
811
+ * it stale.
812
+ */
813
+ type ToolbarOverflowItem = Pick<ResolvedToolbarItem, "type" | "key"> & {
814
+ readonly label?: string | undefined;
815
+ };
816
+ interface ToolbarOverflowOptions {
817
+ /**
818
+ * Every entry of the toolbar, in order — those drawn and those moved into
819
+ * "More" alike — as `resolveToolbar` gave them.
820
+ */
821
+ readonly items: readonly ToolbarOverflowItem[];
822
+ /**
823
+ * How many entries fit, as `fitToolbarItems` counts them: called once
824
+ * straight away, then whenever the count changes.
825
+ */
826
+ readonly onChange: (fit: number) => void;
827
+ }
828
+ interface ToolbarOverflow {
829
+ /**
830
+ * Call whenever the items change, with the current options: measures
831
+ * again, and reports a changed count.
832
+ */
833
+ readonly update: (options: ToolbarOverflowOptions) => void;
834
+ /** Stops observing. */
835
+ readonly detach: () => void;
836
+ }
837
+ /**
838
+ * Measures how many of a toolbar's entries fit in it, reporting the count
839
+ * through `onChange` straight away — so a renderer calling this before
840
+ * paint draws the right entries from the first frame — and again whenever a
841
+ * resize changes it. The arithmetic is @gridkitjs/core's `fitToolbarItems`:
842
+ * the search box and custom entries always stay, and room for "More" is kept
843
+ * only when something does not fit.
844
+ *
845
+ * Entries are found by their `data-gridkit-toolbar-item`, holding each
846
+ * item's `key`, and the "More" button by `data-gridkit-toolbar-more`. An
847
+ * entry moved into "More" is no longer drawn, so each width is remembered
848
+ * by key from the last time it was; one never drawn yet counts as nothing,
849
+ * which lets it be drawn, measured, and moved back out if it does not fit —
850
+ * as one whose label changed while it was not drawn counts again.
851
+ * A spacer counts as nothing too: its width is only the room left over. The
852
+ * toolbar's padding and the `column-gap` between entries are allowed for.
853
+ *
854
+ * It measures again whenever the toolbar or an entry resizes and whenever
855
+ * entries are drawn or taken away — the ones the count it reported let in,
856
+ * or the "More" button — so a renderer only has to draw what it is told,
857
+ * and to pass new items to `update`.
858
+ */
859
+ declare function observeToolbarOverflow(toolbar: HTMLElement, options: ToolbarOverflowOptions): ToolbarOverflow;
860
+
861
+ /** What `printDocument` prints: a document's title, its body's markup and its stylesheet. */
862
+ interface PrintDocumentOptions {
863
+ /** The document's title, as text — what the browser puts in a page's header and names a saved PDF. */
864
+ readonly title: string;
865
+ /** The `<body>`'s markup, as `buildPrintDocument` writes it. */
866
+ readonly bodyHtml: string;
867
+ /** The document's whole stylesheet — `printStyles(page)`, and anything added after it. */
868
+ readonly styles: string;
869
+ }
870
+ /**
871
+ * Prints a document through a hidden frame: its own page, holding only
872
+ * `bodyHtml` under `styles`, so what is printed is what was built — every
873
+ * row, not the slice of them a virtualised grid has drawn — and nothing of
874
+ * the page around it. The frame loads, waits for its fonts, opens the
875
+ * print dialog, and is removed once the dialog closes, when the promise
876
+ * resolves; focus goes back where it was. A frame rather than a new window,
877
+ * which a popup blocker would stop.
878
+ *
879
+ * One print at a time: a call while a print is open resolves at once
880
+ * without printing.
881
+ */
882
+ declare function printDocument(options: PrintDocumentOptions): Promise<void>;
883
+
884
+ /**
885
+ * The stylesheet a print of `buildPrintDocument`'s markup needs, for `page`.
886
+ * A print frame sees none of the page's CSS, so this carries what makes the
887
+ * table read on paper and nothing of a theme: the paper's size and
888
+ * orientation; a header row repeated at the top of every page; rows kept
889
+ * whole across a page break; neutral one-pixel borders; group headers
890
+ * stepped in by depth; backgrounds printed as they are rather than dropped
891
+ * to save ink; page numbers in the bottom-right margin, where the browser
892
+ * draws `@page` margin boxes; and for `fitToWidth`, a smaller font in fixed
893
+ * columns that wrap their text.
894
+ *
895
+ * The grand total prints once, after the last row — not repeated at the
896
+ * foot of every page, where browsers repeat a `<tfoot>`, as a total of
897
+ * rows the page does not hold would mislead. Add CSS after it to restyle.
898
+ */
899
+ declare function printStyles(page: PrintPage): string;
900
+
901
+ export { type AttachedChecklist, type AttachedMenu, type BodyCellTarget, type BodyRowTarget, type ChecklistOptions, type ClipboardContent, type ColumnDropContext, type ColumnDropTarget, type ColumnResizeGestureHandlers, type ContextMenuRequest, type ContextMenuSource, type ContextMenuTriggerOptions, type DragReorderHandlers, type DragReorderState, type FocusVirtualRange, type GridFocusController, type GridFocusOptions, type GridFocusRows, type GroupByDropTarget, type GroupRowTarget, type InfiniteScroll, type InfiniteScrollOptions, type LiveAnnouncer, type MenuCloseReason, type MenuOptions, type MenuSubmenuFocus, NO_DRAG, type OverlayAlign, type OverlayAnchor, type OverlayDismissReason, type OverlayInitialFocus, type OverlayOptions, type OverlayPlacement, type OverlayPlacementOptions, type OverlayPosition, type OverlayRect, type OverlaySide, type OverlaySize, type PointerPress, type PrintDocumentOptions, type RowVirtualizer, type RowVirtualizerOptions, type ToolbarOverflow, type ToolbarOverflowItem, type ToolbarOverflowOptions, attachChecklist, attachContextMenuTrigger, attachInfiniteScroll, attachMenu, attachOverlay, attachToolbar, bodyCellFrom, bodyRowFrom, createGridFocus, createLiveAnnouncer, createRowVirtualizer, downloadBlob, findBodyRow, groupRowFrom, isNativeCopy, measureColumnContent, measurePageSize, observeElementWidth, observeToolbarOverflow, placeOverlay, printDocument, printStyles, resolveColumnDropTarget, resolveGroupByDropTarget, startColumnResizeGesture, startDragReorder, writeClipboard };