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.
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,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
|
-
|
|
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.
|
|
2576
|
+
this.retarget(fromEl);
|
|
2456
2577
|
this.onTick = onTick;
|
|
2457
2578
|
}
|
|
2458
2579
|
/** فقط container را عوض میکند بدون قطع حلقهی در حال اجرا یا صفر کردن speed. */
|
|
2459
2580
|
retarget(fromEl) {
|
|
2460
|
-
this.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
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
|
-
|
|
2501
|
-
|
|
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
|
-
|
|
2506
|
-
|
|
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 &&
|
|
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.
|
|
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
|
-
|
|
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
|