ngx-kit 9.2.1 → 9.2.2

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.
@@ -110,3 +110,8 @@ Known limits: cross-origin iframes (no coordinate access), iframes clipped by an
110
110
  ## Tests
111
111
 
112
112
  `tests/browser/*.spec.ts` run in real Chromium (Playwright + vitest, set `CHROMIUM_PATH`); `tests/browser/page-builder.spec.ts` covers palette → iframe canvas, iframe scroll/auto-scroll, scaled iframes, editing inside blocks, resizing and export cleanup.
113
+
114
+ ## Keyboard & auto-scroll (kanban boards)
115
+
116
+ - Focus a card and press an arrow key to lift it. **One press = one slot**; at the edge of a list the card hops to the neighbouring connected list (Left/Right between columns, Up/Down between the sections of a column). Enter drops, Esc cancels.
117
+ - Auto-scroll picks the nearest scroller **per axis**, so a column that scrolls vertically inside a board that scrolls horizontally scrolls both ways; with the pointer outside every list it uses the dragged item's own scrollers.
@@ -593,6 +593,92 @@ class DragRef {
593
593
  this.applyTransform();
594
594
  }
595
595
  }
596
+ /**
597
+ * Keyboard sorting for list items: jump by whole SLOTS instead of pixels (an 8px step can
598
+ * never cross the centre of a 190px card), and hop to the neighbouring connected list when the
599
+ * edge of the current one is reached — Left/Right between kanban columns, Up/Down between
600
+ * the sections of a column. Returns `false` for free drags (use `nudge`).
601
+ */
602
+ nudgeToSlot(direction) {
603
+ if (!this.listDrag)
604
+ return false;
605
+ if (!this.isDragging())
606
+ this.startDrag(this.lastPointer);
607
+ const list = this.activeDropList;
608
+ if (!list?.el)
609
+ return true;
610
+ const vertical = direction === 'up' || direction === 'down';
611
+ const sign = direction === 'down' || direction === 'right' ? 1 : -1;
612
+ const cur = { x: this.startPointer.x + this.moveDx, y: this.startPointer.y + this.moveDy };
613
+ const listAxis = list.getAxis();
614
+ // A grid has no single main axis: step through its slots along the pressed key's axis.
615
+ const axis = listAxis === 'grid' ? (vertical ? 'y' : 'x') : listAxis;
616
+ let target = null;
617
+ if ((axis === 'y') === vertical)
618
+ target = this.neighbourSlot(list, axis, sign, cur);
619
+ target ??= this.neighbourList(list, vertical ? 'y' : 'x', sign, cur);
620
+ if (target)
621
+ this.dragMove(target);
622
+ return true;
623
+ }
624
+ neighbourSlot(list, axis, sign, cur) {
625
+ const mid = (r) => (axis === 'y' ? r.top + r.height / 2 : r.left + r.width / 2);
626
+ const slots = Array.from(list.el.children)
627
+ .filter((n) => n instanceof HTMLElement &&
628
+ n !== this.el &&
629
+ !n.classList.contains('ngx-drag-preview') &&
630
+ n.getClientRects().length > 0)
631
+ .map((el) => ({ el, r: el.getBoundingClientRect() }))
632
+ .sort((a, b) => mid(a.r) - mid(b.r));
633
+ const p = slots.findIndex((s) => s.el.classList.contains('ngx-drag-placeholder'));
634
+ const next = slots[p + sign];
635
+ if (p < 0 || !next)
636
+ return null;
637
+ // a little PAST the neighbour's centre, beyond the sort hysteresis
638
+ const past = mid(next.r) + 8 * sign;
639
+ return axis === 'y' ? { x: cur.x, y: past } : { x: past, y: cur.y };
640
+ }
641
+ neighbourList(from, along, sign, cur) {
642
+ const doc = this.el.ownerDocument;
643
+ const origin = visibleRect(from.el);
644
+ if (!origin)
645
+ return null;
646
+ let best = null;
647
+ for (const l of this.cachedLists) {
648
+ if (l === from || !l.el || l.el.ownerDocument !== doc)
649
+ continue;
650
+ const r = visibleRect(l.el);
651
+ if (!r || r.width < 8 || r.height < 8)
652
+ continue;
653
+ const dist = along === 'x'
654
+ ? (r.left + r.width / 2 - (origin.left + origin.width / 2)) * sign
655
+ : (r.top + r.height / 2 - (origin.top + origin.height / 2)) * sign;
656
+ if (dist <= 4)
657
+ continue;
658
+ let score;
659
+ if (along === 'x') {
660
+ // prefer the list that is level with the pointer
661
+ const off = Math.max(r.top - cur.y, 0, cur.y - r.bottom);
662
+ score = dist + off * 0.5;
663
+ }
664
+ else {
665
+ // Up/Down only moves between lists that overlap horizontally (sections of one column)
666
+ const overlap = Math.min(r.right, origin.right) - Math.max(r.left, origin.left);
667
+ if (overlap < Math.min(r.width, origin.width) * 0.5)
668
+ continue;
669
+ score = dist;
670
+ }
671
+ if (!best || score < best.score)
672
+ best = { rect: r, score };
673
+ }
674
+ if (!best)
675
+ return null;
676
+ const r = best.rect;
677
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
678
+ return along === 'y'
679
+ ? { x: clamp(cur.x, r.left + 8, r.right - 8), y: sign > 0 ? r.top + 8 : r.bottom - 8 }
680
+ : { x: r.left + r.width / 2, y: clamp(cur.y, r.top + 8, r.bottom - 8) };
681
+ }
596
682
  nudge(dx, dy) {
597
683
  if (!this.isDragging())
598
684
  this.startDrag(this.lastPointer);
@@ -922,6 +1008,34 @@ function isDescendantOfList(list, ancestor) {
922
1008
  }
923
1009
  return false;
924
1010
  }
1011
+ /** The part of `el` that is actually on screen (clipped by scroll containers and the viewport). */
1012
+ function visibleRect(el) {
1013
+ const win = el.ownerDocument.defaultView;
1014
+ if (!win)
1015
+ return null;
1016
+ const r = el.getBoundingClientRect();
1017
+ let left = r.left;
1018
+ let top = r.top;
1019
+ let right = r.right;
1020
+ let bottom = r.bottom;
1021
+ for (let n = el.parentElement; n; n = n.parentElement) {
1022
+ const cs = win.getComputedStyle(n);
1023
+ if (cs.overflowX === 'visible' && cs.overflowY === 'visible')
1024
+ continue;
1025
+ const nr = n.getBoundingClientRect();
1026
+ left = Math.max(left, nr.left);
1027
+ top = Math.max(top, nr.top);
1028
+ right = Math.min(right, nr.right);
1029
+ bottom = Math.min(bottom, nr.bottom);
1030
+ }
1031
+ left = Math.max(left, 0);
1032
+ top = Math.max(top, 0);
1033
+ right = Math.min(right, win.innerWidth);
1034
+ bottom = Math.min(bottom, win.innerHeight);
1035
+ if (right <= left || bottom <= top)
1036
+ return null;
1037
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
1038
+ }
925
1039
 
926
1040
  class ElementHelper {
927
1041
  static collectionHas(a, b) {
@@ -1713,6 +1827,10 @@ class DropListRef {
1713
1827
  parentList = null;
1714
1828
  childLists = new Set();
1715
1829
  session = null;
1830
+ /** Main axis of this list: the measured one while sorting, else a CSS-based guess. */
1831
+ getAxis() {
1832
+ return this.session?.axis ?? fallbackAxis(this.el);
1833
+ }
1716
1834
  placeholder;
1717
1835
  customPlaceholder;
1718
1836
  activeDrag = null;
@@ -2436,7 +2554,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2436
2554
  */
2437
2555
  class AutoScroller {
2438
2556
  raf = 0;
2439
- container;
2557
+ /** Nearest scroller per axis: a kanban column scrolls vertically while its BOARD scrolls horizontally. */
2558
+ containerX;
2559
+ containerY;
2440
2560
  win;
2441
2561
  /** Document the pointer coordinates (`update(x, y)`) are expressed in. */
2442
2562
  pointerDoc;
@@ -2453,36 +2573,40 @@ class AutoScroller {
2453
2573
  start(fromEl, onTick) {
2454
2574
  this.win = fromEl.ownerDocument.defaultView ?? undefined;
2455
2575
  this.pointerDoc = fromEl.ownerDocument;
2456
- this.container = findScrollableAncestor(fromEl);
2576
+ this.retarget(fromEl);
2457
2577
  this.onTick = onTick;
2458
2578
  }
2459
2579
  /** فقط container را عوض می‌کند بدون قطع حلقه‌ی در حال اجرا یا صفر کردن speed. */
2460
2580
  retarget(fromEl) {
2461
- this.container = findScrollableAncestor(fromEl);
2581
+ this.containerX = findScrollableAncestorForAxis(fromEl, 'x');
2582
+ this.containerY = findScrollableAncestorForAxis(fromEl, 'y');
2462
2583
  }
2463
2584
  update(clientX, clientY) {
2585
+ this.speed = {
2586
+ x: this.axisSpeed(this.containerX, 'x', clientX, clientY),
2587
+ y: this.axisSpeed(this.containerY, 'y', clientX, clientY),
2588
+ };
2589
+ if ((this.speed.x || this.speed.y) && !this.raf) {
2590
+ this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
2591
+ }
2592
+ }
2593
+ axisSpeed(container, axis, clientX, clientY) {
2464
2594
  // The scroll container may live in another document (a page-builder iframe). Its rect is in
2465
2595
  // THAT document's viewport pixels, so bring the pointer into the same space first.
2466
2596
  let x = clientX;
2467
2597
  let y = clientY;
2468
- const containerDoc = documentOf(this.container);
2598
+ const containerDoc = documentOf(container);
2469
2599
  if (this.pointerDoc && containerDoc && containerDoc !== this.pointerDoc) {
2470
2600
  const p = convertPointBetweenDocuments({ x, y }, this.pointerDoc, containerDoc);
2471
- if (!p) {
2472
- this.speed = { x: 0, y: 0 };
2473
- return;
2474
- }
2601
+ if (!p)
2602
+ return 0;
2475
2603
  x = p.x;
2476
2604
  y = p.y;
2477
2605
  }
2478
- const rect = getScrollRect(this.container);
2479
- this.speed = {
2480
- x: edgeSpeed(x, rect.left, rect.right, this.edge, this.maxSpeed),
2481
- y: edgeSpeed(y, rect.top, rect.bottom, this.edge, this.maxSpeed),
2482
- };
2483
- if ((this.speed.x || this.speed.y) && !this.raf) {
2484
- this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
2485
- }
2606
+ const rect = getScrollRect(container);
2607
+ return axis === 'x'
2608
+ ? edgeSpeed(x, rect.left, rect.right, this.edge, this.maxSpeed)
2609
+ : edgeSpeed(y, rect.top, rect.bottom, this.edge, this.maxSpeed);
2486
2610
  }
2487
2611
  stop() {
2488
2612
  if (this.raf)
@@ -2494,14 +2618,14 @@ class AutoScroller {
2494
2618
  this.win = undefined;
2495
2619
  this.pointerDoc = undefined;
2496
2620
  }
2497
- tick = () => {
2498
- if (!this.speed.x && !this.speed.y) {
2499
- this.raf = 0;
2621
+ /** Scrolls one container by a speed measured in viewport pixels (carrying sub-pixel remainders). */
2622
+ scrollContainer(container, sx, sy) {
2623
+ if (!sx && !sy)
2500
2624
  return;
2501
- }
2502
- const container = this.container;
2503
- // Edge speed is measured in viewport pixels; an element's scrollLeft/Top are local to it.
2504
- const local = isWindow(container) ? this.speed : viewportDeltaToLocal(container, this.speed);
2625
+ // An element's scrollLeft/Top are local to it; a window's are viewport pixels already.
2626
+ const local = isWindow(container)
2627
+ ? { x: sx, y: sy }
2628
+ : viewportDeltaToLocal(container, { x: sx, y: sy });
2505
2629
  // Browsers snap scroll offsets to whole pixels, so a slow speed (< 1px/frame, i.e. when the
2506
2630
  // pointer has just entered the edge zone) would never move anything. Carry the remainder.
2507
2631
  this.carry.x += local.x;
@@ -2510,11 +2634,23 @@ class AutoScroller {
2510
2634
  const dy = Math.trunc(this.carry.y);
2511
2635
  this.carry.x -= dx;
2512
2636
  this.carry.y -= dy;
2513
- if (dx || dy) {
2514
- // `behavior: 'instant'` matters: with `scroll-behavior: smooth` on <html> or on the scroller
2515
- // (very common in apps) every per-frame scroll would start a new animation that cancels the
2516
- // previous one, and the list would not move at all.
2637
+ // `behavior: 'instant'` matters: with `scroll-behavior: smooth` on <html> or on the scroller
2638
+ // (very common in apps) every per-frame scroll would start a new animation that cancels the
2639
+ // previous one, and the list would not move at all.
2640
+ if (dx || dy)
2517
2641
  container.scrollBy({ left: dx, top: dy, behavior: 'instant' });
2642
+ }
2643
+ tick = () => {
2644
+ if (!this.speed.x && !this.speed.y) {
2645
+ this.raf = 0;
2646
+ return;
2647
+ }
2648
+ if (this.containerX === this.containerY) {
2649
+ this.scrollContainer(this.containerX, this.speed.x, this.speed.y);
2650
+ }
2651
+ else {
2652
+ this.scrollContainer(this.containerX, this.speed.x, 0);
2653
+ this.scrollContainer(this.containerY, 0, this.speed.y);
2518
2654
  }
2519
2655
  // Pointer ثابت مانده ولی محتوا اسکرول شده؛
2520
2656
  // بنابراین موقعیت placeholder/sort را دوباره محاسبه کن.
@@ -2557,7 +2693,8 @@ function findScrollableAncestor(el) {
2557
2693
  break;
2558
2694
  const style = win.getComputedStyle(node);
2559
2695
  const scrollable = /(auto|scroll|overlay)/.test(style.overflowY + style.overflowX);
2560
- if (scrollable && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)) {
2696
+ if (scrollable &&
2697
+ (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)) {
2561
2698
  // <html>/<body> scroll the whole document: their own rect spans the full (tall) page, so
2562
2699
  // edge detection must use the window's viewport instead.
2563
2700
  const doc = node.ownerDocument;
@@ -2569,6 +2706,26 @@ function findScrollableAncestor(el) {
2569
2706
  }
2570
2707
  return el.ownerDocument.defaultView ?? el;
2571
2708
  }
2709
+ /** Nearest ancestor that can scroll along ONE axis (`<html>`/`<body>` resolve to the window). */
2710
+ function findScrollableAncestorForAxis(el, axis) {
2711
+ let node = el;
2712
+ while (node) {
2713
+ const win = node.ownerDocument.defaultView;
2714
+ if (!win)
2715
+ break;
2716
+ const style = win.getComputedStyle(node);
2717
+ const overflow = axis === 'x' ? style.overflowX : style.overflowY;
2718
+ const canScroll = axis === 'x' ? node.scrollWidth > node.clientWidth : node.scrollHeight > node.clientHeight;
2719
+ if (/(auto|scroll|overlay)/.test(overflow) && canScroll) {
2720
+ const doc = node.ownerDocument;
2721
+ if (node === doc.documentElement || node === doc.body)
2722
+ return win;
2723
+ return node;
2724
+ }
2725
+ node = node.parentElement;
2726
+ }
2727
+ return el.ownerDocument.defaultView ?? el;
2728
+ }
2572
2729
  function getScrollPosition(container) {
2573
2730
  if (isWindow(container)) {
2574
2731
  return { left: container.scrollX, top: container.scrollY };
@@ -2781,7 +2938,7 @@ class NgxDraggable {
2781
2938
  const after = this._ref.activeDropList;
2782
2939
  // Follow the hovered list; outside every list, scroll the page.
2783
2940
  if (after !== before)
2784
- this.scroller.retarget(after?.el ?? this._ref.el.ownerDocument.body);
2941
+ this.scroller.retarget(after?.el ?? this._ref.dropList?.el ?? this._ref.el);
2785
2942
  }
2786
2943
  this.dragMove.emit(p);
2787
2944
  }
@@ -2953,7 +3110,13 @@ class NgxDraggable {
2953
3110
  this.beginKeyboardSession();
2954
3111
  this.dragStart.emit(origin);
2955
3112
  }
2956
- this._ref.nudge(delta.x, delta.y);
3113
+ // Holding the key down repeats faster than the 200ms slide animation settles.
3114
+ if (e.repeat && this._ref.isListDrag)
3115
+ return;
3116
+ const name = { ArrowLeft: 'left', ArrowRight: 'right', ArrowUp: 'up', ArrowDown: 'down' }[e.key];
3117
+ // List items hop by whole slots / neighbouring lists; free drags move by `keyboardStep` px.
3118
+ if (!this._ref.nudgeToSlot(name))
3119
+ this._ref.nudge(delta.x, delta.y);
2957
3120
  this.dragMove.emit(this._ref.pointer);
2958
3121
  return;
2959
3122
  }
@@ -3741,5 +3904,5 @@ function getCleanHtml(el, options) {
3741
3904
  * Generated bundle index. Do not edit.
3742
3905
  */
3743
3906
 
3744
- export { AutoScroller, DragDropService, DragRef, DropListGroupRef, DropListRef, NGX_DRAGGABLE, NGX_DRAG_PREVIEW, NGX_DROPLIST, NGX_DROPLIST_GROUP, NGX_PLACEHOLDER, NgxDragPreview, NgxDraggable, NgxDropList, NgxDropListGroup, NgxPlaceholder, NgxResizable, PlaceHolderRef, adjustForZoom, checkBoundX, checkBoundY, clampResizeWithinBoundary, copyArrayItem, findScrollableAncestor, getCleanHtml, getPageZoom, getScrollPosition, isRtl, moveItemInArray, resolveLogicalDirection, stripEditorArtifacts, transferArrayItem };
3907
+ export { AutoScroller, DragDropService, DragRef, DropListGroupRef, DropListRef, NGX_DRAGGABLE, NGX_DRAG_PREVIEW, NGX_DROPLIST, NGX_DROPLIST_GROUP, NGX_PLACEHOLDER, NgxDragPreview, NgxDraggable, NgxDropList, NgxDropListGroup, NgxPlaceholder, NgxResizable, PlaceHolderRef, adjustForZoom, checkBoundX, checkBoundY, clampResizeWithinBoundary, copyArrayItem, findScrollableAncestor, findScrollableAncestorForAxis, getCleanHtml, getPageZoom, getScrollPosition, isRtl, moveItemInArray, resolveLogicalDirection, stripEditorArtifacts, transferArrayItem };
3745
3908
  //# sourceMappingURL=ngx-kit-drag-resize.mjs.map