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.
package/drag-resize/README.md
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
2576
|
+
this.retarget(fromEl);
|
|
2457
2577
|
this.onTick = onTick;
|
|
2458
2578
|
}
|
|
2459
2579
|
/** فقط container را عوض میکند بدون قطع حلقهی در حال اجرا یا صفر کردن speed. */
|
|
2460
2580
|
retarget(fromEl) {
|
|
2461
|
-
this.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
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
|
|
2503
|
-
|
|
2504
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
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 &&
|
|
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.
|
|
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
|
-
|
|
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
|