ngx-kit 9.2.0 → 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,11 +2554,14 @@ 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;
2443
2563
  speed = { x: 0, y: 0 };
2564
+ carry = { x: 0, y: 0 };
2444
2565
  onTick;
2445
2566
  edge;
2446
2567
  maxSpeed;
@@ -2452,60 +2573,84 @@ class AutoScroller {
2452
2573
  start(fromEl, onTick) {
2453
2574
  this.win = fromEl.ownerDocument.defaultView ?? undefined;
2454
2575
  this.pointerDoc = fromEl.ownerDocument;
2455
- this.container = findScrollableAncestor(fromEl);
2576
+ this.retarget(fromEl);
2456
2577
  this.onTick = onTick;
2457
2578
  }
2458
2579
  /** فقط container را عوض می‌کند بدون قطع حلقه‌ی در حال اجرا یا صفر کردن speed. */
2459
2580
  retarget(fromEl) {
2460
- this.container = findScrollableAncestor(fromEl);
2581
+ this.containerX = findScrollableAncestorForAxis(fromEl, 'x');
2582
+ this.containerY = findScrollableAncestorForAxis(fromEl, 'y');
2461
2583
  }
2462
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) {
2463
2594
  // The scroll container may live in another document (a page-builder iframe). Its rect is in
2464
2595
  // THAT document's viewport pixels, so bring the pointer into the same space first.
2465
2596
  let x = clientX;
2466
2597
  let y = clientY;
2467
- const containerDoc = documentOf(this.container);
2598
+ const containerDoc = documentOf(container);
2468
2599
  if (this.pointerDoc && containerDoc && containerDoc !== this.pointerDoc) {
2469
2600
  const p = convertPointBetweenDocuments({ x, y }, this.pointerDoc, containerDoc);
2470
- if (!p) {
2471
- this.speed = { x: 0, y: 0 };
2472
- return;
2473
- }
2601
+ if (!p)
2602
+ return 0;
2474
2603
  x = p.x;
2475
2604
  y = p.y;
2476
2605
  }
2477
- const rect = getScrollRect(this.container);
2478
- this.speed = {
2479
- x: edgeSpeed(x, rect.left, rect.right, this.edge, this.maxSpeed),
2480
- y: edgeSpeed(y, rect.top, rect.bottom, this.edge, this.maxSpeed),
2481
- };
2482
- if ((this.speed.x || this.speed.y) && !this.raf) {
2483
- this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
2484
- }
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);
2485
2610
  }
2486
2611
  stop() {
2487
2612
  if (this.raf)
2488
2613
  this.win?.cancelAnimationFrame(this.raf);
2489
2614
  this.raf = 0;
2490
2615
  this.speed = { x: 0, y: 0 };
2616
+ this.carry = { x: 0, y: 0 };
2491
2617
  this.onTick = undefined;
2492
2618
  this.win = undefined;
2493
2619
  this.pointerDoc = undefined;
2494
2620
  }
2621
+ /** Scrolls one container by a speed measured in viewport pixels (carrying sub-pixel remainders). */
2622
+ scrollContainer(container, sx, sy) {
2623
+ if (!sx && !sy)
2624
+ return;
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 });
2629
+ // Browsers snap scroll offsets to whole pixels, so a slow speed (< 1px/frame, i.e. when the
2630
+ // pointer has just entered the edge zone) would never move anything. Carry the remainder.
2631
+ this.carry.x += local.x;
2632
+ this.carry.y += local.y;
2633
+ const dx = Math.trunc(this.carry.x);
2634
+ const dy = Math.trunc(this.carry.y);
2635
+ this.carry.x -= dx;
2636
+ this.carry.y -= dy;
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)
2641
+ container.scrollBy({ left: dx, top: dy, behavior: 'instant' });
2642
+ }
2495
2643
  tick = () => {
2496
2644
  if (!this.speed.x && !this.speed.y) {
2497
2645
  this.raf = 0;
2498
2646
  return;
2499
2647
  }
2500
- const container = this.container;
2501
- if (isWindow(container)) {
2502
- container.scrollBy(this.speed.x, this.speed.y);
2648
+ if (this.containerX === this.containerY) {
2649
+ this.scrollContainer(this.containerX, this.speed.x, this.speed.y);
2503
2650
  }
2504
2651
  else {
2505
- // Edge speed is measured in viewport pixels; scrollLeft/Top are local to the scroller.
2506
- const local = viewportDeltaToLocal(container, this.speed);
2507
- container.scrollLeft += local.x;
2508
- container.scrollTop += local.y;
2652
+ this.scrollContainer(this.containerX, this.speed.x, 0);
2653
+ this.scrollContainer(this.containerY, 0, this.speed.y);
2509
2654
  }
2510
2655
  // Pointer ثابت مانده ولی محتوا اسکرول شده؛
2511
2656
  // بنابراین موقعیت placeholder/sort را دوباره محاسبه کن.
@@ -2548,7 +2693,8 @@ function findScrollableAncestor(el) {
2548
2693
  break;
2549
2694
  const style = win.getComputedStyle(node);
2550
2695
  const scrollable = /(auto|scroll|overlay)/.test(style.overflowY + style.overflowX);
2551
- if (scrollable && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)) {
2696
+ if (scrollable &&
2697
+ (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)) {
2552
2698
  // <html>/<body> scroll the whole document: their own rect spans the full (tall) page, so
2553
2699
  // edge detection must use the window's viewport instead.
2554
2700
  const doc = node.ownerDocument;
@@ -2560,6 +2706,26 @@ function findScrollableAncestor(el) {
2560
2706
  }
2561
2707
  return el.ownerDocument.defaultView ?? el;
2562
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
+ }
2563
2729
  function getScrollPosition(container) {
2564
2730
  if (isWindow(container)) {
2565
2731
  return { left: container.scrollX, top: container.scrollY };
@@ -2772,7 +2938,7 @@ class NgxDraggable {
2772
2938
  const after = this._ref.activeDropList;
2773
2939
  // Follow the hovered list; outside every list, scroll the page.
2774
2940
  if (after !== before)
2775
- this.scroller.retarget(after?.el ?? this._ref.el.ownerDocument.body);
2941
+ this.scroller.retarget(after?.el ?? this._ref.dropList?.el ?? this._ref.el);
2776
2942
  }
2777
2943
  this.dragMove.emit(p);
2778
2944
  }
@@ -2944,7 +3110,13 @@ class NgxDraggable {
2944
3110
  this.beginKeyboardSession();
2945
3111
  this.dragStart.emit(origin);
2946
3112
  }
2947
- 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);
2948
3120
  this.dragMove.emit(this._ref.pointer);
2949
3121
  return;
2950
3122
  }
@@ -3732,5 +3904,5 @@ function getCleanHtml(el, options) {
3732
3904
  * Generated bundle index. Do not edit.
3733
3905
  */
3734
3906
 
3735
- 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 };
3736
3908
  //# sourceMappingURL=ngx-kit-drag-resize.mjs.map