@gridkitjs/dom 0.4.0 → 0.5.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
@@ -796,8 +815,8 @@ interface RowVirtualizer {
796
815
  * Scrolls so row `index` is in view. A mounted row is scrolled to directly;
797
816
  * one outside the window is jumped to at its estimated offset, then
798
817
  * 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.
818
+ * replaces one still waiting for its row; a row count that no longer holds
819
+ * that row, or `resetHeights`, drops it.
801
820
  */
802
821
  readonly scrollToIndex: (index: number, options?: ScrollIntoViewOptions) => void;
803
822
  /**
@@ -912,8 +931,8 @@ interface PrintDocumentOptions {
912
931
  * Prints a document through a hidden frame: its own page, holding only
913
932
  * `bodyHtml` under `styles`, so what is printed is what was built — every
914
933
  * 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
934
+ * the page around it. The frame loads, waits for its fonts (a few seconds
935
+ * at most), opens the print dialog, and is removed once the dialog closes, when the promise
917
936
  * resolves; focus goes back where it was. A frame rather than a new window,
918
937
  * which a popup blocker would stop.
919
938
  *
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);
@@ -383,16 +391,18 @@ function startColumnResizeGesture(handle, press, session, handlers) {
383
391
  }
384
392
  press.preventDefault();
385
393
  let width = session.startWidth;
394
+ let resized = false;
386
395
  startPointerGesture(
387
396
  handle,
388
397
  press.pointerId,
389
398
  {
390
399
  onMove(event) {
391
400
  width = applyColumnResize(session, event.clientX);
401
+ resized ||= width !== session.startWidth;
392
402
  handlers.onMove(width);
393
403
  },
394
404
  onEnd() {
395
- handlers.onEnd(width === session.startWidth ? null : width);
405
+ handlers.onEnd(resized ? width : null);
396
406
  },
397
407
  onCancel() {
398
408
  handlers.onCancel();
@@ -520,11 +530,7 @@ function downloadBlob(blob, fileName) {
520
530
 
521
531
  // src/dragReorder.ts
522
532
  var DRAG_THRESHOLD = 4;
523
- var NO_DRAG = {
524
- draggedId: null,
525
- dropTarget: null,
526
- ghostTransform: null
527
- };
533
+ var NO_DRAG = { kind: "idle" };
528
534
  function startDragReorder(id, grabElement, press, handlers) {
529
535
  if (!mayStartGesture(press, handlers.signal)) {
530
536
  return false;
@@ -546,6 +552,7 @@ function startDragReorder(id, grabElement, press, handlers) {
546
552
  target: handlers.resolveTarget(clientX, clientY, id)
547
553
  };
548
554
  handlers.onState({
555
+ kind: "dragging",
549
556
  draggedId: id,
550
557
  dropTarget: phase.target,
551
558
  ghostTransform: `translate(${String(clientX - grabX)}px, ${String(clientY)}px)`
@@ -619,7 +626,7 @@ function observeElementWidth(element, onWidth) {
619
626
  // src/gridFocus.ts
620
627
  import {
621
628
  clampFocus,
622
- HEADER_ROW,
629
+ headerFocus,
623
630
  nextFocusForKey
624
631
  } from "@gridkitjs/core";
625
632
  var FALLBACK_PAGE = 10;
@@ -629,6 +636,12 @@ function cellIn(row, columnIndex) {
629
636
  }
630
637
  return row.cells[Math.min(columnIndex, row.cells.length - 1)] ?? null;
631
638
  }
639
+ function sameCell(a, b) {
640
+ if (a.area === "body" && b.area === "body") {
641
+ return a.rowIndex === b.rowIndex && a.columnIndex === b.columnIndex;
642
+ }
643
+ return a.area === b.area && a.columnIndex === b.columnIndex;
644
+ }
632
645
  function findBodyRow(bodyTable, rowIndex) {
633
646
  return bodyTable?.tBodies[0]?.querySelector(
634
647
  `:scope > tr[data-gridkit-row-index="${String(rowIndex)}"]`
@@ -646,7 +659,7 @@ function createGridFocus(initialOptions) {
646
659
  let options = initialOptions;
647
660
  let rows = null;
648
661
  const listeners = /* @__PURE__ */ new Set();
649
- let stored = { rowIndex: HEADER_ROW, columnIndex: 0 };
662
+ let stored = headerFocus(0);
650
663
  let snapshot = {
651
664
  stored,
652
665
  rowCount: options.rowCount,
@@ -684,14 +697,9 @@ function createGridFocus(initialOptions) {
684
697
  root.removeEventListener("pointerdown", dropPendingMove);
685
698
  };
686
699
  }
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) {
700
+ function focusCell(focus) {
701
+ const next = clampFocus(focus, options.rowCount, options.columnCount);
702
+ if (sameCell(next, getFocus())) {
695
703
  return;
696
704
  }
697
705
  pendingMove = next;
@@ -725,7 +733,7 @@ function createGridFocus(initialOptions) {
725
733
  if (next === null) {
726
734
  return false;
727
735
  }
728
- focusCell(next.rowIndex, next.columnIndex);
736
+ focusCell(next);
729
737
  return true;
730
738
  },
731
739
  attach(next, root) {
@@ -743,7 +751,7 @@ function createGridFocus(initialOptions) {
743
751
  return;
744
752
  }
745
753
  const cell = cellIn(
746
- drawn.rowIndex === HEADER_ROW ? rows.getHeaderRow() : rows.getBodyRow(drawn.rowIndex),
754
+ drawn.area === "header" ? rows.getHeaderRow() : rows.getBodyRow(drawn.rowIndex),
747
755
  drawn.columnIndex
748
756
  );
749
757
  if (cell !== null) {
@@ -752,7 +760,7 @@ function createGridFocus(initialOptions) {
752
760
  return;
753
761
  }
754
762
  const range = options.virtualRange;
755
- if (range != null && drawn.rowIndex !== HEADER_ROW && (drawn.rowIndex < range.startIndex || drawn.rowIndex > range.endIndex)) {
763
+ if (range != null && drawn.area === "body" && (drawn.rowIndex < range.startIndex || drawn.rowIndex > range.endIndex)) {
756
764
  range.scrollToIndex(drawn.rowIndex);
757
765
  return;
758
766
  }
@@ -763,15 +771,17 @@ function createGridFocus(initialOptions) {
763
771
 
764
772
  // src/infiniteScroll.ts
765
773
  var IDLE = { kind: "idle" };
766
- var REQUESTED = { kind: "requested" };
767
774
  var LOADING = { kind: "loading" };
768
- var DETACHED = { kind: "detached" };
775
+ var DETACHED2 = { kind: "detached" };
769
776
  function afterUpdate(state, loading, hasMoreChanged) {
770
777
  if (loading) {
771
778
  return LOADING;
772
779
  }
773
780
  return state.kind === "loading" || hasMoreChanged ? IDLE : state;
774
781
  }
782
+ function contentOffset(root, sentinel) {
783
+ return sentinel.getBoundingClientRect().top - root.getBoundingClientRect().top + root.scrollTop;
784
+ }
775
785
  function attachInfiniteScroll(root, options) {
776
786
  let latest = options;
777
787
  let intersecting = false;
@@ -779,12 +789,22 @@ function attachInfiniteScroll(root, options) {
779
789
  let observer = null;
780
790
  let observed = null;
781
791
  function maybeLoadMore() {
782
- if (state.kind !== "idle" || !intersecting || !latest.hasMore) {
792
+ if (state.kind !== "idle" || !intersecting || observed === null || !latest.hasMore) {
783
793
  return;
784
794
  }
785
- state = REQUESTED;
795
+ state = {
796
+ kind: "requested",
797
+ sentinelOffset: contentOffset(root, observed.sentinel)
798
+ };
786
799
  latest.onLoadMore?.();
787
800
  }
801
+ function requestAnswered() {
802
+ if (state.kind !== "requested" || observed === null) {
803
+ return false;
804
+ }
805
+ const moved = contentOffset(root, observed.sentinel) - state.sentinelOffset;
806
+ return Math.abs(moved) >= 1;
807
+ }
788
808
  function report(isIntersecting) {
789
809
  intersecting = isIntersecting;
790
810
  if (!intersecting && state.kind === "requested") {
@@ -830,11 +850,16 @@ function attachInfiniteScroll(root, options) {
830
850
  latest = next;
831
851
  state = afterUpdate(state, next.isLoadingMore ?? false, hasMoreChanged);
832
852
  observe();
853
+ if (requestAnswered()) {
854
+ state = IDLE;
855
+ disconnect();
856
+ observe();
857
+ }
833
858
  maybeLoadMore();
834
859
  },
835
860
  detach() {
836
861
  disconnect();
837
- state = DETACHED;
862
+ state = DETACHED2;
838
863
  }
839
864
  };
840
865
  }
@@ -1414,6 +1439,9 @@ function createRowVirtualizer(scrollElement, options) {
1414
1439
  recompute();
1415
1440
  },
1416
1441
  resetHeights() {
1442
+ if (phase.kind === "awaiting-row") {
1443
+ phase = READY;
1444
+ }
1417
1445
  heights = new Array(heights.length).fill(void 0);
1418
1446
  recompute();
1419
1447
  },
@@ -1670,6 +1698,7 @@ function observeToolbarOverflow(toolbar, options) {
1670
1698
 
1671
1699
  // src/print/printDocument.ts
1672
1700
  var RETURN_EVENTS = ["pointerdown", "keydown"];
1701
+ var FONTS_TIMEOUT_MS = 3e3;
1673
1702
  var printing = false;
1674
1703
  function createFrame(bodyHtml) {
1675
1704
  const frame = document.createElement("iframe");
@@ -1716,6 +1745,17 @@ function fill(view, { title, styles }) {
1716
1745
  style.textContent = styles;
1717
1746
  frameDocument.head.append(style);
1718
1747
  }
1748
+ async function fontsLoaded(view) {
1749
+ let timer = 0;
1750
+ const timeout = new Promise((resolve) => {
1751
+ timer = window.setTimeout(resolve, FONTS_TIMEOUT_MS);
1752
+ });
1753
+ try {
1754
+ await Promise.race([view.document.fonts.ready, timeout]);
1755
+ } finally {
1756
+ window.clearTimeout(timer);
1757
+ }
1758
+ }
1719
1759
  async function print(view) {
1720
1760
  const listeners = [
1721
1761
  [view, "afterprint"],
@@ -1751,7 +1791,7 @@ async function printDocument(options) {
1751
1791
  try {
1752
1792
  const view = await load(frame);
1753
1793
  fill(view, options);
1754
- await view.document.fonts.ready;
1794
+ await fontsLoaded(view);
1755
1795
  await print(view);
1756
1796
  } finally {
1757
1797
  frame.remove();