@gridkitjs/dom 0.4.0 → 0.6.0

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.
package/dist/index.d.ts CHANGED
@@ -96,7 +96,11 @@ interface AttachedChecklist {
96
96
  * An item outside the mounted window is focused once it mounts.
97
97
  */
98
98
  readonly focusItem: (index: number) => void;
99
- /** Removes every listener and observer. The items keep their `tabindex`. */
99
+ /**
100
+ * Removes every listener and observer. The items keep their `tabindex`.
101
+ * After it, `setCount` and `focusItem` do nothing: they neither scroll
102
+ * nor focus, and a move still waiting for its item is dropped.
103
+ */
100
104
  readonly detach: () => void;
101
105
  }
102
106
  /**
@@ -191,10 +195,11 @@ interface ColumnResizeGestureHandlers {
191
195
  /** Every pointer move, with the width the column takes there. */
192
196
  readonly onMove: (width: number) => void;
193
197
  /**
194
- * The pointer was released. `width` is the width to commit, or `null` when
195
- * it ended where it started: a click on the handle — as each half of a
196
- * double-click is — is not a resize, and reporting one would fire a
197
- * spurious resize on every click.
198
+ * The pointer was released. `width` is the width to commit, even when a
199
+ * drag brought it back to where it started, or `null` when the width never
200
+ * changed: a click on the handle — as each half of a double-click is — is
201
+ * not a resize, and reporting one would fire a spurious resize on every
202
+ * click.
198
203
  */
199
204
  readonly onEnd: (width: number | null) => void;
200
205
  /**
@@ -276,15 +281,22 @@ declare function writeClipboard(content: ClipboardContent): Promise<void>;
276
281
  */
277
282
  declare function downloadBlob(blob: Blob, fileName: string): void;
278
283
 
279
- /** What a renderer draws while a reorder drag is open. */
280
- interface DragReorderState<Target> {
281
- /** The item being dragged, once the press has become a drag. */
282
- readonly draggedId: string | null;
284
+ /**
285
+ * What a renderer draws for a reorder gesture: nothing while `idle`, which
286
+ * covers a press still short of the drag threshold, then while `dragging` the
287
+ * dragged item, where it would land and the ghost trailing the pointer.
288
+ */
289
+ type DragReorderState<Target> = {
290
+ readonly kind: "idle";
291
+ } | {
292
+ readonly kind: "dragging";
293
+ /** The item being dragged. */
294
+ readonly draggedId: string;
283
295
  /** Where it would land if released now, `null` for nowhere valid. */
284
296
  readonly dropTarget: Target | null;
285
297
  /** Where the element trailing the pointer sits, as a CSS `transform`. */
286
- readonly ghostTransform: string | null;
287
- }
298
+ readonly ghostTransform: string;
299
+ };
288
300
  /** The state while nothing is dragged — the one a drag reports when it ends. */
289
301
  declare const NO_DRAG: DragReorderState<never>;
290
302
  interface DragReorderHandlers<Target> {
@@ -434,7 +446,7 @@ interface GridFocusController {
434
446
  * focused does nothing, which is what lets a cell's own `focus` event
435
447
  * report a click through here without a loop.
436
448
  */
437
- readonly focusCell: (rowIndex: number, columnIndex: number) => void;
449
+ readonly focusCell: (focus: GridFocus) => void;
438
450
  /**
439
451
  * Moves the tab stop as `key` does — the arrows, Home, End, the page keys,
440
452
  * with Ctrl for the grid's corners. Returns whether the key was one of
@@ -505,8 +517,9 @@ interface InfiniteScrollOptions {
505
517
  /**
506
518
  * Called at most once per approach: when the sentinel comes within
507
519
  * `rootMargin` of the root's visible area. Not again until that load has
508
- * finished (`isLoadingMore` back to `false`), `hasMore` has changed, or
509
- * the sentinel has left `rootMargin` and come back.
520
+ * finished (`isLoadingMore` back to `false`), `hasMore` has changed, rows
521
+ * have been added or removed in front of the sentinel, or the sentinel has
522
+ * left `rootMargin` and come back.
510
523
  */
511
524
  readonly onLoadMore?: (() => void) | undefined;
512
525
  /** Whether a load `onLoadMore` started is still in flight. */
@@ -519,6 +532,8 @@ interface InfiniteScroll {
519
532
  * Takes the current options. Cheap when nothing changed, so a renderer may
520
533
  * call it after every render; a new sentinel or `rootMargin` restarts the
521
534
  * observer, and a load finishing or `hasMore` changing re-checks at once.
535
+ * While a load it asked for has not reported `isLoadingMore`, it measures
536
+ * where the sentinel sits, to tell whether rows answered it.
522
537
  */
523
538
  readonly update: (options: InfiniteScrollOptions) => void;
524
539
  /** Stops watching for good: a later `update` does nothing. */
@@ -783,7 +798,11 @@ interface RowVirtualizerOptions {
783
798
  interface RowVirtualizer {
784
799
  /** How many rows there are. Measured heights of the rows that remain are kept. */
785
800
  readonly setRowCount: (count: number) => void;
786
- /** Forgets every measured height, for rows that are no longer the ones measured. */
801
+ /**
802
+ * Forgets every measured height, for rows that are no longer the ones
803
+ * measured. A `scrollToIndex` still waiting for its row is dropped too:
804
+ * the row it asked for is no longer at that index.
805
+ */
787
806
  readonly resetHeights: () => void;
788
807
  /**
789
808
  * A ref callback for the element of row `index` (its place among all the
@@ -792,12 +811,21 @@ interface RowVirtualizer {
792
811
  * a renderer re-attaching it on every render costs nothing.
793
812
  */
794
813
  readonly measureRow: (index: number) => (element: HTMLElement | null) => void;
814
+ /**
815
+ * Reads the heights of rows already laid out, each paired with its index
816
+ * among all the rows, and works out the window once for all of them: for
817
+ * rows drawn before this virtualizer existed, so its first window is the
818
+ * one on screen rather than one placed by `estimatedRowHeight`. Indices
819
+ * past the row count are skipped. It does not observe the rows;
820
+ * `measureRow` does that once the renderer hands them over.
821
+ */
822
+ readonly measureRows: (rows: Iterable<readonly [index: number, element: Element]>) => void;
795
823
  /**
796
824
  * Scrolls so row `index` is in view. A mounted row is scrolled to directly;
797
825
  * one outside the window is jumped to at its estimated offset, then
798
826
  * scrolled to properly once it mounts and can be measured. A later call
799
- * replaces one still waiting for its row, and so does a row count that no
800
- * longer holds that row.
827
+ * replaces one still waiting for its row; a row count that no longer holds
828
+ * that row, or `resetHeights`, drops it.
801
829
  */
802
830
  readonly scrollToIndex: (index: number, options?: ScrollIntoViewOptions) => void;
803
831
  /**
@@ -912,8 +940,8 @@ interface PrintDocumentOptions {
912
940
  * Prints a document through a hidden frame: its own page, holding only
913
941
  * `bodyHtml` under `styles`, so what is printed is what was built — every
914
942
  * row, not the slice of them a virtualised grid has drawn — and nothing of
915
- * the page around it. The frame loads, waits for its fonts, opens the
916
- * print dialog, and is removed once the dialog closes, when the promise
943
+ * the page around it. The frame loads, waits for its fonts (a few seconds
944
+ * at most), opens the print dialog, and is removed once the dialog closes, when the promise
917
945
  * resolves; focus goes back where it was. A frame rather than a new window,
918
946
  * which a popup blocker would stop.
919
947
  *
package/dist/index.js CHANGED
@@ -69,13 +69,15 @@ function bodyRowFrom(target) {
69
69
  // src/checklist/attachChecklist.ts
70
70
  var ITEM = "[data-gridkit-checklist-item]";
71
71
  var INDEX = "data-gridkit-checklist-index";
72
+ var SETTLED = { kind: "settled" };
73
+ var DETACHED = { kind: "detached" };
72
74
  function indexOf(item) {
73
75
  return Number(item.getAttribute(INDEX));
74
76
  }
75
77
  function attachChecklist(list, options) {
76
78
  let count = 0;
77
79
  let active = 0;
78
- let pending = null;
80
+ let state = SETTLED;
79
81
  function items() {
80
82
  return Array.from(list.querySelectorAll(ITEM));
81
83
  }
@@ -102,7 +104,7 @@ function attachChecklist(list, options) {
102
104
  }
103
105
  }
104
106
  function moveTo(index) {
105
- if (count === 0) {
107
+ if (state.kind === "detached" || count === 0) {
106
108
  return;
107
109
  }
108
110
  const target = Math.max(0, Math.min(index, count - 1));
@@ -111,9 +113,9 @@ function attachChecklist(list, options) {
111
113
  options.scrollToIndex(target);
112
114
  const item = itemAt(target);
113
115
  if (item === null) {
114
- pending = target;
116
+ state = { kind: "awaiting-item", index: target };
115
117
  } else {
116
- pending = null;
118
+ state = SETTLED;
117
119
  item.focus({ preventScroll: true });
118
120
  }
119
121
  }
@@ -163,7 +165,7 @@ function attachChecklist(list, options) {
163
165
  function onFocusIn(event) {
164
166
  const item = itemOf(event.target);
165
167
  if (item !== null) {
166
- pending = null;
168
+ state = SETTLED;
167
169
  makeActive(indexOf(item));
168
170
  setTabStop();
169
171
  }
@@ -175,12 +177,12 @@ function attachChecklist(list, options) {
175
177
  item.focus({ preventScroll: true });
176
178
  }
177
179
  }
178
- function focusPendingIfMounted() {
179
- const item = pending === null ? null : itemAt(pending);
180
+ function focusAwaitedItemIfMounted() {
181
+ const item = state.kind === "awaiting-item" ? itemAt(state.index) : null;
180
182
  if (item === null) {
181
183
  return;
182
184
  }
183
- pending = null;
185
+ state = SETTLED;
184
186
  const focused = document.activeElement;
185
187
  if (focused === null || focused === document.body || list.contains(focused)) {
186
188
  item.focus({ preventScroll: true });
@@ -188,7 +190,7 @@ function attachChecklist(list, options) {
188
190
  }
189
191
  const observer = new MutationObserver(() => {
190
192
  setTabStop();
191
- focusPendingIfMounted();
193
+ focusAwaitedItemIfMounted();
192
194
  });
193
195
  observer.observe(list, {
194
196
  childList: true,
@@ -201,17 +203,23 @@ function attachChecklist(list, options) {
201
203
  list.addEventListener("click", onClick);
202
204
  return {
203
205
  setCount(next) {
206
+ if (state.kind === "detached") {
207
+ return;
208
+ }
204
209
  count = next;
205
210
  active = Math.max(0, Math.min(active, count - 1));
206
- if (pending !== null) {
207
- pending = count === 0 ? null : Math.min(pending, count - 1);
211
+ if (state.kind === "awaiting-item") {
212
+ state = count === 0 ? SETTLED : {
213
+ kind: "awaiting-item",
214
+ index: Math.min(state.index, count - 1)
215
+ };
208
216
  }
209
217
  setTabStop();
210
- focusPendingIfMounted();
218
+ focusAwaitedItemIfMounted();
211
219
  },
212
220
  focusItem: moveTo,
213
221
  detach() {
214
- pending = null;
222
+ state = DETACHED;
215
223
  observer.disconnect();
216
224
  list.removeEventListener("keydown", onKeyDown2);
217
225
  list.removeEventListener("focusin", onFocusIn);
@@ -333,11 +341,13 @@ function mayStartGesture(press, signal) {
333
341
  return press.button === 0 && press.isPrimary && signal?.aborted !== true;
334
342
  }
335
343
  function startPointerGesture(element, pointerId, handlers, signal) {
344
+ const { ownerDocument } = element;
336
345
  element.setPointerCapture(pointerId);
337
346
  function stop() {
338
- element.removeEventListener("pointermove", move);
339
- element.removeEventListener("pointerup", end);
340
- element.removeEventListener("pointercancel", cancel);
347
+ ownerDocument.removeEventListener("pointermove", move, true);
348
+ ownerDocument.removeEventListener("pointerup", end, true);
349
+ ownerDocument.removeEventListener("pointercancel", cancel, true);
350
+ element.removeEventListener("lostpointercapture", recapture);
341
351
  window.removeEventListener("keydown", onKeyDown2);
342
352
  signal?.removeEventListener("abort", abandon);
343
353
  if (element.hasPointerCapture(pointerId)) {
@@ -360,6 +370,11 @@ function startPointerGesture(element, pointerId, handlers, signal) {
360
370
  abandon();
361
371
  }
362
372
  }
373
+ function recapture(event) {
374
+ if (event.pointerId === pointerId && element.isConnected) {
375
+ element.setPointerCapture(pointerId);
376
+ }
377
+ }
363
378
  function abandon() {
364
379
  stop();
365
380
  handlers.onCancel();
@@ -369,9 +384,10 @@ function startPointerGesture(element, pointerId, handlers, signal) {
369
384
  abandon();
370
385
  }
371
386
  }
372
- element.addEventListener("pointermove", move);
373
- element.addEventListener("pointerup", end);
374
- element.addEventListener("pointercancel", cancel);
387
+ ownerDocument.addEventListener("pointermove", move, true);
388
+ ownerDocument.addEventListener("pointerup", end, true);
389
+ ownerDocument.addEventListener("pointercancel", cancel, true);
390
+ element.addEventListener("lostpointercapture", recapture);
375
391
  window.addEventListener("keydown", onKeyDown2);
376
392
  signal?.addEventListener("abort", abandon);
377
393
  }
@@ -383,16 +399,18 @@ function startColumnResizeGesture(handle, press, session, handlers) {
383
399
  }
384
400
  press.preventDefault();
385
401
  let width = session.startWidth;
402
+ let resized = false;
386
403
  startPointerGesture(
387
404
  handle,
388
405
  press.pointerId,
389
406
  {
390
407
  onMove(event) {
391
408
  width = applyColumnResize(session, event.clientX);
409
+ resized ||= width !== session.startWidth;
392
410
  handlers.onMove(width);
393
411
  },
394
412
  onEnd() {
395
- handlers.onEnd(width === session.startWidth ? null : width);
413
+ handlers.onEnd(resized ? width : null);
396
414
  },
397
415
  onCancel() {
398
416
  handlers.onCancel();
@@ -520,11 +538,7 @@ function downloadBlob(blob, fileName) {
520
538
 
521
539
  // src/dragReorder.ts
522
540
  var DRAG_THRESHOLD = 4;
523
- var NO_DRAG = {
524
- draggedId: null,
525
- dropTarget: null,
526
- ghostTransform: null
527
- };
541
+ var NO_DRAG = { kind: "idle" };
528
542
  function startDragReorder(id, grabElement, press, handlers) {
529
543
  if (!mayStartGesture(press, handlers.signal)) {
530
544
  return false;
@@ -546,6 +560,7 @@ function startDragReorder(id, grabElement, press, handlers) {
546
560
  target: handlers.resolveTarget(clientX, clientY, id)
547
561
  };
548
562
  handlers.onState({
563
+ kind: "dragging",
549
564
  draggedId: id,
550
565
  dropTarget: phase.target,
551
566
  ghostTransform: `translate(${String(clientX - grabX)}px, ${String(clientY)}px)`
@@ -619,7 +634,7 @@ function observeElementWidth(element, onWidth) {
619
634
  // src/gridFocus.ts
620
635
  import {
621
636
  clampFocus,
622
- HEADER_ROW,
637
+ headerFocus,
623
638
  nextFocusForKey
624
639
  } from "@gridkitjs/core";
625
640
  var FALLBACK_PAGE = 10;
@@ -629,6 +644,12 @@ function cellIn(row, columnIndex) {
629
644
  }
630
645
  return row.cells[Math.min(columnIndex, row.cells.length - 1)] ?? null;
631
646
  }
647
+ function sameCell(a, b) {
648
+ if (a.area === "body" && b.area === "body") {
649
+ return a.rowIndex === b.rowIndex && a.columnIndex === b.columnIndex;
650
+ }
651
+ return a.area === b.area && a.columnIndex === b.columnIndex;
652
+ }
632
653
  function findBodyRow(bodyTable, rowIndex) {
633
654
  return bodyTable?.tBodies[0]?.querySelector(
634
655
  `:scope > tr[data-gridkit-row-index="${String(rowIndex)}"]`
@@ -646,7 +667,7 @@ function createGridFocus(initialOptions) {
646
667
  let options = initialOptions;
647
668
  let rows = null;
648
669
  const listeners = /* @__PURE__ */ new Set();
649
- let stored = { rowIndex: HEADER_ROW, columnIndex: 0 };
670
+ let stored = headerFocus(0);
650
671
  let snapshot = {
651
672
  stored,
652
673
  rowCount: options.rowCount,
@@ -684,14 +705,9 @@ function createGridFocus(initialOptions) {
684
705
  root.removeEventListener("pointerdown", dropPendingMove);
685
706
  };
686
707
  }
687
- function focusCell(rowIndex, columnIndex) {
688
- const next = clampFocus(
689
- { rowIndex, columnIndex },
690
- options.rowCount,
691
- options.columnCount
692
- );
693
- const current = getFocus();
694
- if (next.rowIndex === current.rowIndex && next.columnIndex === current.columnIndex) {
708
+ function focusCell(focus) {
709
+ const next = clampFocus(focus, options.rowCount, options.columnCount);
710
+ if (sameCell(next, getFocus())) {
695
711
  return;
696
712
  }
697
713
  pendingMove = next;
@@ -725,7 +741,7 @@ function createGridFocus(initialOptions) {
725
741
  if (next === null) {
726
742
  return false;
727
743
  }
728
- focusCell(next.rowIndex, next.columnIndex);
744
+ focusCell(next);
729
745
  return true;
730
746
  },
731
747
  attach(next, root) {
@@ -743,7 +759,7 @@ function createGridFocus(initialOptions) {
743
759
  return;
744
760
  }
745
761
  const cell = cellIn(
746
- drawn.rowIndex === HEADER_ROW ? rows.getHeaderRow() : rows.getBodyRow(drawn.rowIndex),
762
+ drawn.area === "header" ? rows.getHeaderRow() : rows.getBodyRow(drawn.rowIndex),
747
763
  drawn.columnIndex
748
764
  );
749
765
  if (cell !== null) {
@@ -752,7 +768,7 @@ function createGridFocus(initialOptions) {
752
768
  return;
753
769
  }
754
770
  const range = options.virtualRange;
755
- if (range != null && drawn.rowIndex !== HEADER_ROW && (drawn.rowIndex < range.startIndex || drawn.rowIndex > range.endIndex)) {
771
+ if (range != null && drawn.area === "body" && (drawn.rowIndex < range.startIndex || drawn.rowIndex > range.endIndex)) {
756
772
  range.scrollToIndex(drawn.rowIndex);
757
773
  return;
758
774
  }
@@ -763,15 +779,17 @@ function createGridFocus(initialOptions) {
763
779
 
764
780
  // src/infiniteScroll.ts
765
781
  var IDLE = { kind: "idle" };
766
- var REQUESTED = { kind: "requested" };
767
782
  var LOADING = { kind: "loading" };
768
- var DETACHED = { kind: "detached" };
783
+ var DETACHED2 = { kind: "detached" };
769
784
  function afterUpdate(state, loading, hasMoreChanged) {
770
785
  if (loading) {
771
786
  return LOADING;
772
787
  }
773
788
  return state.kind === "loading" || hasMoreChanged ? IDLE : state;
774
789
  }
790
+ function contentOffset(root, sentinel) {
791
+ return sentinel.getBoundingClientRect().top - root.getBoundingClientRect().top + root.scrollTop;
792
+ }
775
793
  function attachInfiniteScroll(root, options) {
776
794
  let latest = options;
777
795
  let intersecting = false;
@@ -779,12 +797,22 @@ function attachInfiniteScroll(root, options) {
779
797
  let observer = null;
780
798
  let observed = null;
781
799
  function maybeLoadMore() {
782
- if (state.kind !== "idle" || !intersecting || !latest.hasMore) {
800
+ if (state.kind !== "idle" || !intersecting || observed === null || !latest.hasMore) {
783
801
  return;
784
802
  }
785
- state = REQUESTED;
803
+ state = {
804
+ kind: "requested",
805
+ sentinelOffset: contentOffset(root, observed.sentinel)
806
+ };
786
807
  latest.onLoadMore?.();
787
808
  }
809
+ function requestAnswered() {
810
+ if (state.kind !== "requested" || observed === null) {
811
+ return false;
812
+ }
813
+ const moved = contentOffset(root, observed.sentinel) - state.sentinelOffset;
814
+ return Math.abs(moved) >= 1;
815
+ }
788
816
  function report(isIntersecting) {
789
817
  intersecting = isIntersecting;
790
818
  if (!intersecting && state.kind === "requested") {
@@ -830,11 +858,16 @@ function attachInfiniteScroll(root, options) {
830
858
  latest = next;
831
859
  state = afterUpdate(state, next.isLoadingMore ?? false, hasMoreChanged);
832
860
  observe();
861
+ if (requestAnswered()) {
862
+ state = IDLE;
863
+ disconnect();
864
+ observe();
865
+ }
833
866
  maybeLoadMore();
834
867
  },
835
868
  detach() {
836
869
  disconnect();
837
- state = DETACHED;
870
+ state = DETACHED2;
838
871
  }
839
872
  };
840
873
  }
@@ -1414,6 +1447,9 @@ function createRowVirtualizer(scrollElement, options) {
1414
1447
  recompute();
1415
1448
  },
1416
1449
  resetHeights() {
1450
+ if (phase.kind === "awaiting-row") {
1451
+ phase = READY;
1452
+ }
1417
1453
  heights = new Array(heights.length).fill(void 0);
1418
1454
  recompute();
1419
1455
  },
@@ -1442,6 +1478,17 @@ function createRowVirtualizer(scrollElement, options) {
1442
1478
  }
1443
1479
  return callback;
1444
1480
  },
1481
+ measureRows(rows) {
1482
+ if (phase.kind === "destroyed") {
1483
+ return;
1484
+ }
1485
+ for (const [index, element] of rows) {
1486
+ if (index >= 0 && index < heights.length) {
1487
+ heights[index] = element.getBoundingClientRect().height;
1488
+ }
1489
+ }
1490
+ recompute();
1491
+ },
1445
1492
  scrollToIndex(index, scrollOptions) {
1446
1493
  if (phase.kind === "destroyed" || heights.length === 0) {
1447
1494
  return;
@@ -1670,6 +1717,7 @@ function observeToolbarOverflow(toolbar, options) {
1670
1717
 
1671
1718
  // src/print/printDocument.ts
1672
1719
  var RETURN_EVENTS = ["pointerdown", "keydown"];
1720
+ var FONTS_TIMEOUT_MS = 3e3;
1673
1721
  var printing = false;
1674
1722
  function createFrame(bodyHtml) {
1675
1723
  const frame = document.createElement("iframe");
@@ -1716,6 +1764,17 @@ function fill(view, { title, styles }) {
1716
1764
  style.textContent = styles;
1717
1765
  frameDocument.head.append(style);
1718
1766
  }
1767
+ async function fontsLoaded(view) {
1768
+ let timer = 0;
1769
+ const timeout = new Promise((resolve) => {
1770
+ timer = window.setTimeout(resolve, FONTS_TIMEOUT_MS);
1771
+ });
1772
+ try {
1773
+ await Promise.race([view.document.fonts.ready, timeout]);
1774
+ } finally {
1775
+ window.clearTimeout(timer);
1776
+ }
1777
+ }
1719
1778
  async function print(view) {
1720
1779
  const listeners = [
1721
1780
  [view, "afterprint"],
@@ -1751,7 +1810,7 @@ async function printDocument(options) {
1751
1810
  try {
1752
1811
  const view = await load(frame);
1753
1812
  fill(view, options);
1754
- await view.document.fonts.ready;
1813
+ await fontsLoaded(view);
1755
1814
  await print(view);
1756
1815
  } finally {
1757
1816
  frame.remove();