@gridkitjs/dom 0.3.2 → 0.4.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.js CHANGED
@@ -175,8 +175,7 @@ function attachChecklist(list, options) {
175
175
  item.focus({ preventScroll: true });
176
176
  }
177
177
  }
178
- const observer = new MutationObserver(() => {
179
- setTabStop();
178
+ function focusPendingIfMounted() {
180
179
  const item = pending === null ? null : itemAt(pending);
181
180
  if (item === null) {
182
181
  return;
@@ -186,6 +185,10 @@ function attachChecklist(list, options) {
186
185
  if (focused === null || focused === document.body || list.contains(focused)) {
187
186
  item.focus({ preventScroll: true });
188
187
  }
188
+ }
189
+ const observer = new MutationObserver(() => {
190
+ setTabStop();
191
+ focusPendingIfMounted();
189
192
  });
190
193
  observer.observe(list, {
191
194
  childList: true,
@@ -200,7 +203,11 @@ function attachChecklist(list, options) {
200
203
  setCount(next) {
201
204
  count = next;
202
205
  active = Math.max(0, Math.min(active, count - 1));
206
+ if (pending !== null) {
207
+ pending = count === 0 ? null : Math.min(pending, count - 1);
208
+ }
203
209
  setTabStop();
210
+ focusPendingIfMounted();
204
211
  },
205
212
  focusItem: moveTo,
206
213
  detach() {
@@ -322,61 +329,77 @@ function attachContextMenuTrigger(root, options) {
322
329
  import { applyColumnResize } from "@gridkitjs/core";
323
330
 
324
331
  // src/pointerGesture.ts
325
- function isPrimaryPress(press) {
326
- return press.button === 0 && press.isPrimary;
332
+ function mayStartGesture(press, signal) {
333
+ return press.button === 0 && press.isPrimary && signal?.aborted !== true;
327
334
  }
328
- function startPointerGesture(element, pointerId, handlers) {
335
+ function startPointerGesture(element, pointerId, handlers, signal) {
329
336
  element.setPointerCapture(pointerId);
330
337
  function stop() {
331
338
  element.removeEventListener("pointermove", move);
332
339
  element.removeEventListener("pointerup", end);
333
340
  element.removeEventListener("pointercancel", cancel);
334
341
  window.removeEventListener("keydown", onKeyDown2);
342
+ signal?.removeEventListener("abort", abandon);
335
343
  if (element.hasPointerCapture(pointerId)) {
336
344
  element.releasePointerCapture(pointerId);
337
345
  }
338
346
  }
339
347
  function move(event) {
340
- handlers.onMove(event);
348
+ if (event.pointerId === pointerId) {
349
+ handlers.onMove(event);
350
+ }
341
351
  }
342
- function end() {
343
- stop();
344
- handlers.onEnd();
352
+ function end(event) {
353
+ if (event.pointerId === pointerId) {
354
+ stop();
355
+ handlers.onEnd();
356
+ }
345
357
  }
346
- function cancel() {
358
+ function cancel(event) {
359
+ if (event.pointerId === pointerId) {
360
+ abandon();
361
+ }
362
+ }
363
+ function abandon() {
347
364
  stop();
348
365
  handlers.onCancel();
349
366
  }
350
367
  function onKeyDown2(event) {
351
368
  if (event.key === "Escape") {
352
- cancel();
369
+ abandon();
353
370
  }
354
371
  }
355
372
  element.addEventListener("pointermove", move);
356
373
  element.addEventListener("pointerup", end);
357
374
  element.addEventListener("pointercancel", cancel);
358
375
  window.addEventListener("keydown", onKeyDown2);
376
+ signal?.addEventListener("abort", abandon);
359
377
  }
360
378
 
361
379
  // src/columnResize.ts
362
380
  function startColumnResizeGesture(handle, press, session, handlers) {
363
- if (!isPrimaryPress(press)) {
381
+ if (!mayStartGesture(press, handlers.signal)) {
364
382
  return false;
365
383
  }
366
384
  press.preventDefault();
367
385
  let width = session.startWidth;
368
- startPointerGesture(handle, press.pointerId, {
369
- onMove(event) {
370
- width = applyColumnResize(session, event.clientX);
371
- handlers.onMove(width);
372
- },
373
- onEnd() {
374
- handlers.onEnd(width === session.startWidth ? null : width);
386
+ startPointerGesture(
387
+ handle,
388
+ press.pointerId,
389
+ {
390
+ onMove(event) {
391
+ width = applyColumnResize(session, event.clientX);
392
+ handlers.onMove(width);
393
+ },
394
+ onEnd() {
395
+ handlers.onEnd(width === session.startWidth ? null : width);
396
+ },
397
+ onCancel() {
398
+ handlers.onCancel();
399
+ }
375
400
  },
376
- onCancel() {
377
- handlers.onCancel();
378
- }
379
- });
401
+ handlers.signal
402
+ );
380
403
  return true;
381
404
  }
382
405
  function measureColumnContent(table, columnId) {
@@ -503,41 +526,43 @@ var NO_DRAG = {
503
526
  ghostTransform: null
504
527
  };
505
528
  function startDragReorder(id, grabElement, press, handlers) {
506
- if (!isPrimaryPress(press)) {
529
+ if (!mayStartGesture(press, handlers.signal)) {
507
530
  return false;
508
531
  }
509
- const { pointerId, clientX: startX } = press;
532
+ const { pointerId, clientX: startX, clientY: startY } = press;
510
533
  const grabX = startX - grabElement.getBoundingClientRect().left;
511
- let dragging = false;
512
- let target = null;
513
- startPointerGesture(grabElement, pointerId, {
514
- onMove(moveEvent) {
515
- const { clientX, clientY } = moveEvent;
516
- if (!dragging) {
517
- if (Math.abs(clientX - startX) < DRAG_THRESHOLD) {
534
+ let phase = { kind: "pressed" };
535
+ startPointerGesture(
536
+ grabElement,
537
+ pointerId,
538
+ {
539
+ onMove(moveEvent) {
540
+ const { clientX, clientY } = moveEvent;
541
+ if (phase.kind === "pressed" && Math.hypot(clientX - startX, clientY - startY) < DRAG_THRESHOLD) {
518
542
  return;
519
543
  }
520
- dragging = true;
521
- }
522
- target = handlers.resolveTarget(clientX, clientY, id);
523
- handlers.onState({
524
- draggedId: id,
525
- dropTarget: target,
526
- ghostTransform: `translate(${String(clientX - grabX)}px, ${String(clientY)}px)`
527
- });
528
- },
529
- onEnd() {
530
- const dropped = target;
531
- handlers.onState(NO_DRAG);
532
- if (dragging && dropped !== null) {
533
- handlers.onDrop(dropped, id);
544
+ phase = {
545
+ kind: "dragging",
546
+ target: handlers.resolveTarget(clientX, clientY, id)
547
+ };
548
+ handlers.onState({
549
+ draggedId: id,
550
+ dropTarget: phase.target,
551
+ ghostTransform: `translate(${String(clientX - grabX)}px, ${String(clientY)}px)`
552
+ });
553
+ },
554
+ onEnd() {
555
+ handlers.onState(NO_DRAG);
556
+ if (phase.kind === "dragging" && phase.target !== null) {
557
+ handlers.onDrop(phase.target, id);
558
+ }
559
+ },
560
+ onCancel() {
561
+ handlers.onState(NO_DRAG);
534
562
  }
535
563
  },
536
- onCancel() {
537
- target = null;
538
- handlers.onState(NO_DRAG);
539
- }
540
- });
564
+ handlers.signal
565
+ );
541
566
  return true;
542
567
  }
543
568
 
@@ -640,6 +665,25 @@ function createGridFocus(initialOptions) {
640
665
  }
641
666
  return snapshot.focus;
642
667
  }
668
+ function dropPendingMove() {
669
+ pendingMove = null;
670
+ }
671
+ function listenForTakeover(root) {
672
+ function onFocusOut(event) {
673
+ const to = event.relatedTarget;
674
+ if (to instanceof Node && !root.contains(to)) {
675
+ dropPendingMove();
676
+ }
677
+ }
678
+ root.addEventListener("focusout", onFocusOut);
679
+ root.addEventListener("wheel", dropPendingMove, { passive: true });
680
+ root.addEventListener("pointerdown", dropPendingMove);
681
+ return () => {
682
+ root.removeEventListener("focusout", onFocusOut);
683
+ root.removeEventListener("wheel", dropPendingMove);
684
+ root.removeEventListener("pointerdown", dropPendingMove);
685
+ };
686
+ }
643
687
  function focusCell(rowIndex, columnIndex) {
644
688
  const next = clampFocus(
645
689
  { rowIndex, columnIndex },
@@ -684,9 +728,11 @@ function createGridFocus(initialOptions) {
684
728
  focusCell(next.rowIndex, next.columnIndex);
685
729
  return true;
686
730
  },
687
- attach(next) {
731
+ attach(next, root) {
688
732
  rows = next;
733
+ const stopListening = root == null ? void 0 : listenForTakeover(root);
689
734
  return () => {
735
+ stopListening?.();
690
736
  if (rows === next) {
691
737
  rows = null;
692
738
  }
@@ -716,20 +762,36 @@ function createGridFocus(initialOptions) {
716
762
  }
717
763
 
718
764
  // src/infiniteScroll.ts
765
+ var IDLE = { kind: "idle" };
766
+ var REQUESTED = { kind: "requested" };
767
+ var LOADING = { kind: "loading" };
768
+ var DETACHED = { kind: "detached" };
769
+ function afterUpdate(state, loading, hasMoreChanged) {
770
+ if (loading) {
771
+ return LOADING;
772
+ }
773
+ return state.kind === "loading" || hasMoreChanged ? IDLE : state;
774
+ }
719
775
  function attachInfiniteScroll(root, options) {
720
776
  let latest = options;
721
777
  let intersecting = false;
722
- let fired = false;
723
- let wasLoading = options.isLoadingMore ?? false;
778
+ let state = options.isLoadingMore ?? false ? LOADING : IDLE;
724
779
  let observer = null;
725
780
  let observed = null;
726
781
  function maybeLoadMore() {
727
- if (!intersecting || !latest.hasMore || (latest.isLoadingMore ?? false) || fired) {
782
+ if (state.kind !== "idle" || !intersecting || !latest.hasMore) {
728
783
  return;
729
784
  }
730
- fired = true;
785
+ state = REQUESTED;
731
786
  latest.onLoadMore?.();
732
787
  }
788
+ function report(isIntersecting) {
789
+ intersecting = isIntersecting;
790
+ if (!intersecting && state.kind === "requested") {
791
+ state = IDLE;
792
+ }
793
+ maybeLoadMore();
794
+ }
733
795
  function disconnect() {
734
796
  observer?.disconnect();
735
797
  observer = null;
@@ -749,11 +811,9 @@ function attachInfiniteScroll(root, options) {
749
811
  observer = new IntersectionObserver(
750
812
  (entries) => {
751
813
  const entry = entries[entries.length - 1];
752
- if (entry === void 0) {
753
- return;
814
+ if (entry !== void 0) {
815
+ report(entry.isIntersecting);
754
816
  }
755
- intersecting = entry.isIntersecting;
756
- maybeLoadMore();
757
817
  },
758
818
  { root, rootMargin }
759
819
  );
@@ -763,16 +823,19 @@ function attachInfiniteScroll(root, options) {
763
823
  observe();
764
824
  return {
765
825
  update(next) {
766
- latest = next;
767
- const loading = next.isLoadingMore ?? false;
768
- if (wasLoading && !loading) {
769
- fired = false;
770
- maybeLoadMore();
826
+ if (state.kind === "detached") {
827
+ return;
771
828
  }
772
- wasLoading = loading;
829
+ const hasMoreChanged = next.hasMore !== latest.hasMore;
830
+ latest = next;
831
+ state = afterUpdate(state, next.isLoadingMore ?? false, hasMoreChanged);
773
832
  observe();
833
+ maybeLoadMore();
774
834
  },
775
- detach: disconnect
835
+ detach() {
836
+ disconnect();
837
+ state = DETACHED;
838
+ }
776
839
  };
777
840
  }
778
841
 
@@ -792,6 +855,8 @@ var MOVES = {
792
855
  Home: "first",
793
856
  End: "last"
794
857
  };
858
+ var IDLE2 = { kind: "idle" };
859
+ var AWAY = { kind: "away" };
795
860
  function idOf(item) {
796
861
  return item.getAttribute("data-gridkit-menu-item") ?? "";
797
862
  }
@@ -809,10 +874,8 @@ function isTypeahead(event) {
809
874
  return /^\S$/u.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
810
875
  }
811
876
  function attachMenu(panel, options) {
812
- let typed = "";
813
- let typeaheadTimer;
814
- let hovered = null;
815
- let hoverTimer;
877
+ let typeahead = IDLE2;
878
+ let hover = AWAY;
816
879
  function items() {
817
880
  return Array.from(panel.querySelectorAll(ITEM2));
818
881
  }
@@ -841,21 +904,58 @@ function attachMenu(panel, options) {
841
904
  options.onActivate(idOf(item));
842
905
  }
843
906
  }
844
- function typeahead(key, all, from) {
845
- typed += key;
846
- window.clearTimeout(typeaheadTimer);
847
- typeaheadTimer = window.setTimeout(() => {
848
- typed = "";
849
- }, TYPEAHEAD_TIMEOUT);
907
+ function endTyping() {
908
+ if (typeahead.kind === "typing") {
909
+ window.clearTimeout(typeahead.timer);
910
+ }
911
+ typeahead = IDLE2;
912
+ }
913
+ function endHover() {
914
+ if (hover.kind === "resting") {
915
+ window.clearTimeout(hover.timer);
916
+ }
917
+ hover = AWAY;
918
+ }
919
+ function type(key, all, from) {
920
+ const typed = (typeahead.kind === "typing" ? typeahead.typed : "") + key;
921
+ endTyping();
922
+ typeahead = {
923
+ kind: "typing",
924
+ typed,
925
+ timer: window.setTimeout(() => {
926
+ typeahead = IDLE2;
927
+ }, TYPEAHEAD_TIMEOUT)
928
+ };
850
929
  const match = all[typeaheadIndex(all.map(labelOf), from, typed)];
851
930
  if (match !== void 0) {
852
931
  focus(match);
853
932
  }
854
933
  }
934
+ function rest(item) {
935
+ hover = {
936
+ kind: "resting",
937
+ item,
938
+ timer: window.setTimeout(() => {
939
+ hover = { kind: "rested", item };
940
+ if (opensSubmenu(item) && !isDisabled(item)) {
941
+ openSubmenu(item, "none");
942
+ } else {
943
+ options.onCollapseSubmenu?.();
944
+ }
945
+ }, HOVER_DELAY)
946
+ };
947
+ }
855
948
  function onKeyDown2(event) {
856
- if (event.isComposing) {
949
+ if (event.isComposing || !actOnKey(event)) {
857
950
  return;
858
951
  }
952
+ event.preventDefault();
953
+ endHover();
954
+ if (!isTypeahead(event)) {
955
+ endTyping();
956
+ }
957
+ }
958
+ function actOnKey(event) {
859
959
  const all = items();
860
960
  const item = itemOf(event.target);
861
961
  const from = item === null ? -1 : all.indexOf(item);
@@ -882,11 +982,11 @@ function attachMenu(panel, options) {
882
982
  } else if (event.key === "Tab") {
883
983
  options.onClose("tab");
884
984
  } else if (isTypeahead(event)) {
885
- typeahead(event.key, all, from);
985
+ type(event.key, all, from);
886
986
  } else {
887
- return;
987
+ return false;
888
988
  }
889
- event.preventDefault();
989
+ return true;
890
990
  }
891
991
  function onFocusIn(event) {
892
992
  const item = itemOf(event.target);
@@ -908,24 +1008,16 @@ function attachMenu(panel, options) {
908
1008
  if (item !== null && document.activeElement !== item) {
909
1009
  focus(item);
910
1010
  }
911
- if (item === hovered) {
1011
+ if (hover.kind !== "away" && hover.item === item) {
912
1012
  return;
913
1013
  }
914
- hovered = item;
915
- window.clearTimeout(hoverTimer);
1014
+ endHover();
916
1015
  if (item !== null) {
917
- hoverTimer = window.setTimeout(() => {
918
- if (opensSubmenu(item) && !isDisabled(item)) {
919
- openSubmenu(item, "none");
920
- } else {
921
- options.onCollapseSubmenu?.();
922
- }
923
- }, HOVER_DELAY);
1016
+ rest(item);
924
1017
  }
925
1018
  }
926
1019
  function onPointerLeave() {
927
- hovered = null;
928
- window.clearTimeout(hoverTimer);
1020
+ endHover();
929
1021
  }
930
1022
  const focused = itemOf(document.activeElement);
931
1023
  setTabStop(focused ?? items()[0]);
@@ -943,8 +1035,8 @@ function attachMenu(panel, options) {
943
1035
  }
944
1036
  },
945
1037
  detach() {
946
- window.clearTimeout(typeaheadTimer);
947
- window.clearTimeout(hoverTimer);
1038
+ endTyping();
1039
+ endHover();
948
1040
  panel.removeEventListener("keydown", onKeyDown2);
949
1041
  panel.removeEventListener("focusin", onFocusIn);
950
1042
  panel.removeEventListener("click", onClick);
@@ -1238,15 +1330,18 @@ function attachOverlay(anchor, panel, options) {
1238
1330
  if (settlingFrame !== null) {
1239
1331
  cancelAnimationFrame(settlingFrame);
1240
1332
  }
1241
- if (returnFocusTo !== null && focusInside) {
1242
- returnFocusTo.focus({ preventScroll: true });
1333
+ if (!focusInside) {
1334
+ return;
1243
1335
  }
1336
+ const target = returnFocusTo?.isConnected === true ? returnFocusTo : options.returnFocusFallback?.();
1337
+ target?.focus({ preventScroll: true });
1244
1338
  }
1245
1339
  return detach;
1246
1340
  }
1247
1341
 
1248
1342
  // src/rowVirtualizer.ts
1249
1343
  import { visibleRange } from "@gridkitjs/core";
1344
+ var READY = { kind: "ready" };
1250
1345
  function sameRange(a, b) {
1251
1346
  return a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.offsetBefore === b.offsetBefore && a.offsetAfter === b.offsetAfter;
1252
1347
  }
@@ -1256,13 +1351,12 @@ function createRowVirtualizer(scrollElement, options) {
1256
1351
  let scrollTop = scrollElement.scrollTop;
1257
1352
  let viewportHeight = scrollElement.clientHeight;
1258
1353
  let range = null;
1259
- let destroyed = false;
1260
- let pendingScroll;
1354
+ let phase = READY;
1261
1355
  const elements = /* @__PURE__ */ new Map();
1262
1356
  const indexByElement = /* @__PURE__ */ new Map();
1263
1357
  const refCallbacks = /* @__PURE__ */ new Map();
1264
1358
  function recompute() {
1265
- if (destroyed) {
1359
+ if (phase.kind === "destroyed") {
1266
1360
  return;
1267
1361
  }
1268
1362
  const next = visibleRange(
@@ -1313,6 +1407,9 @@ function createRowVirtualizer(scrollElement, options) {
1313
1407
  recompute();
1314
1408
  return {
1315
1409
  setRowCount(count) {
1410
+ if (phase.kind === "awaiting-row" && phase.index >= count) {
1411
+ phase = READY;
1412
+ }
1316
1413
  heights = Array.from({ length: count }, (_, index) => heights[index]);
1317
1414
  recompute();
1318
1415
  },
@@ -1330,15 +1427,15 @@ function createRowVirtualizer(scrollElement, options) {
1330
1427
  indexByElement.delete(previous);
1331
1428
  elements.delete(index);
1332
1429
  }
1333
- if (element === null || destroyed) {
1430
+ if (element === null || phase.kind === "destroyed") {
1334
1431
  return;
1335
1432
  }
1336
1433
  elements.set(index, element);
1337
1434
  indexByElement.set(element, index);
1338
1435
  rowObserver.observe(element);
1339
- if (pendingScroll?.index === index) {
1340
- element.scrollIntoView(pendingScroll.options);
1341
- pendingScroll = void 0;
1436
+ if (phase.kind === "awaiting-row" && phase.index === index) {
1437
+ element.scrollIntoView(phase.options);
1438
+ phase = READY;
1342
1439
  }
1343
1440
  };
1344
1441
  refCallbacks.set(index, callback);
@@ -1346,21 +1443,21 @@ function createRowVirtualizer(scrollElement, options) {
1346
1443
  return callback;
1347
1444
  },
1348
1445
  scrollToIndex(index, scrollOptions) {
1349
- if (heights.length === 0) {
1446
+ if (phase.kind === "destroyed" || heights.length === 0) {
1350
1447
  return;
1351
1448
  }
1352
1449
  const clamped = Math.max(0, Math.min(index, heights.length - 1));
1353
1450
  const mounted = elements.get(clamped);
1354
1451
  if (mounted !== void 0) {
1452
+ phase = READY;
1355
1453
  mounted.scrollIntoView(scrollOptions);
1356
1454
  return;
1357
1455
  }
1358
- pendingScroll = { index: clamped, options: scrollOptions };
1456
+ phase = { kind: "awaiting-row", index: clamped, options: scrollOptions };
1359
1457
  scrollElement.scrollTop = estimateOffsetBefore(clamped);
1360
1458
  },
1361
1459
  destroy() {
1362
- destroyed = true;
1363
- pendingScroll = void 0;
1460
+ phase = { kind: "destroyed" };
1364
1461
  scrollElement.removeEventListener("scroll", handleScroll);
1365
1462
  viewportObserver.disconnect();
1366
1463
  rowObserver.disconnect();
@@ -1589,17 +1686,26 @@ function createFrame(bodyHtml) {
1589
1686
  }
1590
1687
  function load(frame) {
1591
1688
  return new Promise((resolve, reject) => {
1592
- frame.addEventListener(
1593
- "load",
1594
- () => {
1595
- if (frame.contentWindow === null) {
1596
- reject(new Error("The print frame has no window."));
1597
- } else {
1598
- resolve(frame.contentWindow);
1599
- }
1600
- },
1601
- { once: true }
1602
- );
1689
+ const removal = new MutationObserver(() => {
1690
+ if (!frame.isConnected) {
1691
+ settle();
1692
+ reject(new Error("The print frame was removed before it loaded."));
1693
+ }
1694
+ });
1695
+ function loaded() {
1696
+ settle();
1697
+ if (frame.contentWindow === null) {
1698
+ reject(new Error("The print frame has no window."));
1699
+ } else {
1700
+ resolve(frame.contentWindow);
1701
+ }
1702
+ }
1703
+ function settle() {
1704
+ removal.disconnect();
1705
+ frame.removeEventListener("load", loaded);
1706
+ }
1707
+ frame.addEventListener("load", loaded);
1708
+ removal.observe(document.body, { childList: true });
1603
1709
  document.body.append(frame);
1604
1710
  });
1605
1711
  }
@@ -1610,7 +1716,7 @@ function fill(view, { title, styles }) {
1610
1716
  style.textContent = styles;
1611
1717
  frameDocument.head.append(style);
1612
1718
  }
1613
- function print(view) {
1719
+ async function print(view) {
1614
1720
  const listeners = [
1615
1721
  [view, "afterprint"],
1616
1722
  ...RETURN_EVENTS.flatMap((type) => [
@@ -1618,19 +1724,22 @@ function print(view) {
1618
1724
  [window, type]
1619
1725
  ])
1620
1726
  ];
1621
- return new Promise((resolve) => {
1622
- function done() {
1623
- for (const [target, type] of listeners) {
1624
- target.removeEventListener(type, done, { capture: true });
1625
- }
1626
- resolve();
1627
- }
1628
- for (const [target, type] of listeners) {
1629
- target.addEventListener(type, done, { capture: true });
1630
- }
1727
+ let done = () => void 0;
1728
+ const closed = new Promise((resolve) => {
1729
+ done = resolve;
1730
+ });
1731
+ for (const [target, type] of listeners) {
1732
+ target.addEventListener(type, done, { capture: true });
1733
+ }
1734
+ try {
1631
1735
  view.focus();
1632
1736
  view.print();
1633
- });
1737
+ await closed;
1738
+ } finally {
1739
+ for (const [target, type] of listeners) {
1740
+ target.removeEventListener(type, done, { capture: true });
1741
+ }
1742
+ }
1634
1743
  }
1635
1744
  async function printDocument(options) {
1636
1745
  if (printing) {