ngx-kit 9.2.1 → 9.2.3

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.
@@ -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
  }
@@ -3006,11 +3169,11 @@ class NgxDraggable {
3006
3169
  if (!this.dragHandle())
3007
3170
  return true;
3008
3171
  // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
3009
- return isElementTarget(target) && !!target.closest(this.dragHandle());
3172
+ return isElementTarget$1(target) && !!target.closest(this.dragHandle());
3010
3173
  }
3011
3174
  /** Should a press on `target` be left alone (no drag)? */
3012
3175
  isExcluded(target) {
3013
- if (!isElementTarget(target))
3176
+ if (!isElementTarget$1(target))
3014
3177
  return false;
3015
3178
  const selector = this.noDragSelector();
3016
3179
  if (selector && target.closest(selector))
@@ -3044,11 +3207,11 @@ function isEditableOrInteractive(el) {
3044
3207
  return !!el.closest('button,a[href],input,textarea,select,[contenteditable]:not([contenteditable="false"])');
3045
3208
  }
3046
3209
  function isEditableTarget(target) {
3047
- if (!isElementTarget(target))
3210
+ if (!isElementTarget$1(target))
3048
3211
  return false;
3049
3212
  return !!target.closest('input,textarea,select,[contenteditable]:not([contenteditable="false"])');
3050
3213
  }
3051
- function isElementTarget(target) {
3214
+ function isElementTarget$1(target) {
3052
3215
  return (!!target &&
3053
3216
  typeof target === 'object' &&
3054
3217
  'nodeType' in target &&
@@ -3058,6 +3221,43 @@ function isElementTarget(target) {
3058
3221
 
3059
3222
  const ALL_DIRECTIONS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
3060
3223
  const KEYBOARD_STEP = 8;
3224
+ /**
3225
+ * Elements matching this selector keep their normal pointer/touch behaviour.
3226
+ * Add data-ngx-resize-ignore to any custom interactive widget that should never
3227
+ * initiate resizing when it is close to the panel edge.
3228
+ */
3229
+ const INTERACTIVE_SELECTOR = [
3230
+ 'input',
3231
+ 'textarea',
3232
+ 'select',
3233
+ 'option',
3234
+ 'button',
3235
+ 'a',
3236
+ 'summary',
3237
+ '[contenteditable="true"]',
3238
+ 'audio',
3239
+ 'video',
3240
+ 'iframe',
3241
+ 'label',
3242
+ '[role="textbox"]',
3243
+ '[role="menuitem"]',
3244
+ '[role="tab"]',
3245
+ 'mat-slider',
3246
+ 'mat-select',
3247
+ 'mat-checkbox',
3248
+ 'mat-radio-button',
3249
+ 'mat-slide-toggle',
3250
+ '.mat-mdc-slider',
3251
+ '.mat-mdc-select',
3252
+ '[role="button"]',
3253
+ '[role="slider"]',
3254
+ '[role="switch"]',
3255
+ '[role="checkbox"]',
3256
+ '[role="radio"]',
3257
+ '[role="combobox"]',
3258
+ '[role="listbox"]',
3259
+ '[data-ngx-resize-ignore]',
3260
+ ].join(',');
3061
3261
  class NgxResizable {
3062
3262
  el;
3063
3263
  disabled = model(false, /* @ts-ignore */
@@ -3087,14 +3287,16 @@ class NgxResizable {
3087
3287
  ...(ngDevMode ? [{ debugName: "resizing" }] : /* istanbul ignore next */ []));
3088
3288
  direction = 'se';
3089
3289
  start;
3090
- pointer = {
3091
- x: 0,
3092
- y: 0,
3093
- };
3290
+ pointer = { x: 0, y: 0 };
3094
3291
  beforeTransition = '';
3095
3292
  beforeWidth = '';
3096
3293
  beforeHeight = '';
3097
- beforeTransform = '';
3294
+ beforeTranslate = '';
3295
+ baseComputedTranslate = 'none';
3296
+ keyboardBaseTranslate = null;
3297
+ keyboardBaseInlineTranslate = '';
3298
+ startMatrix = { a: 1, b: 0, c: 0, d: 1 };
3299
+ pointerInverse = invertLinear({ a: 1, b: 0, c: 0, d: 1 });
3098
3300
  pointerId = -1;
3099
3301
  startWidth = 0;
3100
3302
  startHeight = 0;
@@ -3110,47 +3312,43 @@ class NgxResizable {
3110
3312
  }
3111
3313
  }
3112
3314
  onPointerDown(e) {
3113
- if (this.disabled() || e.button !== 0) {
3315
+ if (this.disabled() || e.button !== 0 || this.resizing())
3114
3316
  return;
3115
- }
3116
3317
  const dir = this.hitDirection(e);
3117
- if (!dir) {
3318
+ if (dir)
3319
+ this.beginResize(e, dir);
3320
+ }
3321
+ /** Entry point used by the typed NgxResizeHandle directive. */
3322
+ beginResize(e, direction) {
3323
+ if (this.disabled() || e.button !== 0 || this.resizing())
3324
+ return;
3325
+ const dir = resolveLogicalDirection(direction, isRtl(this.el.nativeElement));
3326
+ if (!this.resolvedDirections().includes(dir))
3118
3327
  return;
3119
- }
3120
3328
  e.preventDefault();
3121
- e.stopPropagation();
3122
- e.stopImmediatePropagation();
3123
3329
  const element = this.el.nativeElement;
3124
3330
  this.direction = dir;
3125
3331
  this.pointerId = e.pointerId;
3126
- /**
3127
- * This is the actual visual rectangle.
3128
- * It already contains any transform created by DragRef.
3129
- */
3130
3332
  this.start = element.getBoundingClientRect();
3131
- this.pointer = {
3132
- x: e.clientX,
3133
- y: e.clientY,
3134
- };
3333
+ this.pointer = { x: e.clientX, y: e.clientY };
3135
3334
  this.beforeTransition = element.style.transition;
3136
3335
  this.beforeWidth = element.style.width;
3137
3336
  this.beforeHeight = element.style.height;
3138
- this.startWidth = element.offsetWidth;
3139
- this.startHeight = element.offsetHeight;
3140
- /**
3141
- * Keep DragRef's transform as the immutable base.
3142
- *
3143
- * Resize will temporarily add its own translation
3144
- * on top of this transform.
3145
- */
3146
- this.beforeTransform = element.style.transform;
3337
+ this.beforeTranslate = element.style.translate;
3338
+ this.baseComputedTranslate =
3339
+ element.ownerDocument.defaultView?.getComputedStyle(element).translate ?? 'none';
3340
+ this.keyboardBaseTranslate = null;
3341
+ this.startMatrix = getLocalToViewportMatrix(element);
3342
+ this.pointerInverse = invertLinear(this.startMatrix);
3343
+ this.startWidth = readBorderBoxSize(element, 'x');
3344
+ this.startHeight = readBorderBoxSize(element, 'y');
3147
3345
  this.resizing.set(true);
3148
3346
  element.style.transition = 'none';
3149
3347
  try {
3150
3348
  element.setPointerCapture?.(e.pointerId);
3151
3349
  }
3152
3350
  catch {
3153
- /* synthetic / already-ended pointer: document listeners below still receive the events */
3351
+ // Document listeners remain as a fallback.
3154
3352
  }
3155
3353
  const doc = element.ownerDocument;
3156
3354
  this.removeDocumentListeners = [
@@ -3165,29 +3363,23 @@ class NgxResizable {
3165
3363
  this.resizeStart.emit(out);
3166
3364
  }
3167
3365
  onPointerMove(e) {
3168
- if (!this.resizing() || e.pointerId !== this.pointerId) {
3366
+ if (!this.resizing() || e.pointerId !== this.pointerId)
3169
3367
  return;
3170
- }
3171
3368
  const out = this.calculate(e);
3172
3369
  this.apply(out);
3173
3370
  this.resizeMove.emit(out);
3174
3371
  }
3175
3372
  onPointerUp(e) {
3176
- if (!this.resizing() || e.pointerId !== this.pointerId) {
3373
+ if (!this.resizing() || e.pointerId !== this.pointerId)
3177
3374
  return;
3178
- }
3179
3375
  const out = this.calculate(e);
3180
3376
  this.apply(out);
3181
3377
  this.resizeEnd.emit(out);
3182
3378
  this.finish();
3183
3379
  }
3184
3380
  onDocumentKeyDown(e) {
3185
- if (!this.resizing()) {
3381
+ if (!this.resizing() || e.key !== 'Escape')
3186
3382
  return;
3187
- }
3188
- if (e.key !== 'Escape') {
3189
- return;
3190
- }
3191
3383
  e.preventDefault();
3192
3384
  this.restoreBeforeResize();
3193
3385
  this.finish();
@@ -3202,115 +3394,187 @@ class NgxResizable {
3202
3394
  const element = this.el.nativeElement;
3203
3395
  element.style.width = this.beforeWidth;
3204
3396
  element.style.height = this.beforeHeight;
3205
- element.style.transform = this.beforeTransform;
3397
+ element.style.translate = this.beforeTranslate;
3206
3398
  }
3399
+ /**
3400
+ * Keyboard resizing respects the configured directions.
3401
+ * The arrow keys move the active physical edge:
3402
+ * - if only west is enabled, horizontal keys operate on west;
3403
+ * - if only east is enabled, horizontal keys operate on east;
3404
+ * - north/south follow the same rule vertically.
3405
+ * Disabled axes are never modified.
3406
+ */
3207
3407
  onHandleKeyDown(e) {
3208
- if (this.disabled()) {
3408
+ if (this.disabled() || this.resizing())
3209
3409
  return;
3210
- }
3211
- // Only when the resizable element itself is focused: arrow keys pressed inside an input /
3212
- // contenteditable that lives in the block are caret movement, not a resize request.
3213
- if (e.target !== this.el.nativeElement) {
3410
+ if (e.target !== this.el.nativeElement)
3214
3411
  return;
3215
- }
3216
- const rtl = isRtl(this.el.nativeElement);
3217
- const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
3412
+ if (this.isEditableTarget(e.target))
3413
+ return;
3414
+ const allowed = this.resolvedDirections();
3415
+ const horizontalEdge = this.chooseAxisEdge(allowed, 'x', e.key);
3416
+ const verticalEdge = this.chooseAxisEdge(allowed, 'y', e.key);
3218
3417
  let dw = 0;
3219
3418
  let dh = 0;
3220
- if (e.key === 'ArrowRight') {
3221
- dw = rtl ? -step : step;
3419
+ const configuredStep = this.keyboardStep();
3420
+ const step = (Number.isFinite(configuredStep) ? Math.max(1, configuredStep) : KEYBOARD_STEP) *
3421
+ (e.shiftKey ? 4 : 1);
3422
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
3423
+ if (!horizontalEdge)
3424
+ return;
3425
+ const positive = e.key === 'ArrowRight';
3426
+ const rtl = isRtl(this.el.nativeElement);
3427
+ const physicalDelta = (positive ? 1 : -1) * (rtl ? -1 : 1);
3428
+ // East edge follows pointer direction; west edge moves the opposite way
3429
+ // relative to the resulting width.
3430
+ dw = horizontalEdge === 'e' ? physicalDelta * step : -physicalDelta * step;
3431
+ }
3432
+ else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
3433
+ if (!verticalEdge)
3434
+ return;
3435
+ const physicalDelta = e.key === 'ArrowDown' ? 1 : -1;
3436
+ dh = verticalEdge === 's' ? physicalDelta * step : -physicalDelta * step;
3437
+ }
3438
+ else {
3439
+ return;
3440
+ }
3441
+ e.preventDefault();
3442
+ const element = this.el.nativeElement;
3443
+ const oldWidth = readBorderBoxSize(element, 'x');
3444
+ const oldHeight = readBorderBoxSize(element, 'y');
3445
+ let width = oldWidth;
3446
+ let height = oldHeight;
3447
+ if (dw !== 0) {
3448
+ width = this.clampSize(oldWidth + dw, 'x');
3222
3449
  }
3223
- else if (e.key === 'ArrowLeft') {
3224
- dw = rtl ? step : -step;
3450
+ if (dh !== 0) {
3451
+ height = this.clampSize(oldHeight + dh, 'y');
3452
+ }
3453
+ // Keep the opposite edge fixed for west/north keyboard resizing.
3454
+ const rect = element.getBoundingClientRect();
3455
+ const matrix = getLocalToViewportMatrix(element);
3456
+ const scaleX = Math.hypot(matrix.a, matrix.b) || 1;
3457
+ const scaleY = Math.hypot(matrix.c, matrix.d) || 1;
3458
+ let moveLeft = horizontalEdge === 'w' ? (oldWidth - width) * scaleX : 0;
3459
+ let moveTop = verticalEdge === 'n' ? (oldHeight - height) * scaleY : 0;
3460
+ const boundary = this.boundary();
3461
+ if (boundary) {
3462
+ const clamped = clampResizeWithinBoundary(boundary.getBoundingClientRect(), rect, width * scaleX, height * scaleY, moveLeft, moveTop);
3463
+ width = clamped.width / scaleX;
3464
+ height = clamped.height / scaleY;
3465
+ moveLeft = clamped.left;
3466
+ moveTop = clamped.top;
3225
3467
  }
3226
- else if (e.key === 'ArrowDown') {
3227
- dh = step;
3468
+ // Keyboard resize keeps a stable base translation and recomputes the total offset
3469
+ // on every key press. This prevents an ever-growing chain of calc()/translate values.
3470
+ if (this.keyboardBaseTranslate === null) {
3471
+ const computed = element.ownerDocument.defaultView?.getComputedStyle(element);
3472
+ this.keyboardBaseTranslate = computed?.translate ?? 'none';
3473
+ this.keyboardBaseInlineTranslate = element.style.translate;
3228
3474
  }
3229
- else if (e.key === 'ArrowUp') {
3230
- dh = -step;
3475
+ const style = this.toStyleSize(width, height);
3476
+ element.style.width = `${style.width}px`;
3477
+ element.style.height = `${style.height}px`;
3478
+ element.style.translate = this.keyboardBaseInlineTranslate;
3479
+ // Keep the opposite edge fixed without touching `transform` (owned by NgxDraggable).
3480
+ const current = element.getBoundingClientRect();
3481
+ const correctionX = rect.left + moveLeft - current.left;
3482
+ const correctionY = rect.top + moveTop - current.top;
3483
+ if (correctionX || correctionY) {
3484
+ const local = viewportDeltaToLocal(element, { x: correctionX, y: correctionY });
3485
+ writeTranslateWithOffset(element, this.keyboardBaseTranslate, local);
3231
3486
  }
3232
3487
  else {
3233
- return;
3488
+ element.style.translate = this.keyboardBaseInlineTranslate;
3234
3489
  }
3235
- e.preventDefault();
3236
- const rect = this.el.nativeElement.getBoundingClientRect();
3237
- const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), this.el.nativeElement.offsetWidth + dw)), 'x');
3238
- const height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), this.el.nativeElement.offsetHeight + dh)), 'y');
3239
- const styled = this.toStyleSize(width, height);
3240
- this.el.nativeElement.style.width = `${styled.width}px`;
3241
- this.el.nativeElement.style.height = `${styled.height}px`;
3242
3490
  const out = {
3243
3491
  width,
3244
3492
  height,
3245
- moveLeft: 0,
3246
- moveTop: 0,
3247
- left: rect.left,
3248
- top: rect.top,
3249
- direction: 'se',
3493
+ moveLeft,
3494
+ moveTop,
3495
+ left: rect.left + moveLeft,
3496
+ top: rect.top + moveTop,
3497
+ direction: this.keyboardDirection(horizontalEdge, verticalEdge),
3250
3498
  };
3499
+ this.resizeStart.emit(out);
3251
3500
  this.resizeMove.emit(out);
3252
3501
  this.resizeEnd.emit(out);
3253
3502
  }
3503
+ chooseAxisEdge(allowed, axis, key) {
3504
+ if (axis === 'x') {
3505
+ const hasWest = allowed.some((d) => d.includes('w'));
3506
+ const hasEast = allowed.some((d) => d.includes('e'));
3507
+ if (key === 'ArrowLeft') {
3508
+ if (hasWest)
3509
+ return 'w';
3510
+ if (hasEast)
3511
+ return 'e';
3512
+ }
3513
+ if (key === 'ArrowRight') {
3514
+ if (hasEast)
3515
+ return 'e';
3516
+ if (hasWest)
3517
+ return 'w';
3518
+ }
3519
+ }
3520
+ else {
3521
+ const hasNorth = allowed.some((d) => d.includes('n'));
3522
+ const hasSouth = allowed.some((d) => d.includes('s'));
3523
+ if (key === 'ArrowUp') {
3524
+ if (hasNorth)
3525
+ return 'n';
3526
+ if (hasSouth)
3527
+ return 's';
3528
+ }
3529
+ if (key === 'ArrowDown') {
3530
+ if (hasSouth)
3531
+ return 's';
3532
+ if (hasNorth)
3533
+ return 'n';
3534
+ }
3535
+ }
3536
+ return null;
3537
+ }
3538
+ keyboardDirection(horizontal, vertical) {
3539
+ if (horizontal && vertical)
3540
+ return `${vertical}${horizontal}`;
3541
+ return horizontal ?? vertical ?? 'se';
3542
+ }
3543
+ isEditableTarget(target) {
3544
+ if (!isElementTarget(target))
3545
+ return false;
3546
+ return !!target.closest(INTERACTIVE_SELECTOR);
3547
+ }
3254
3548
  calculate(e) {
3255
- const localDelta = viewportDeltaToLocal(this.el.nativeElement, {
3549
+ const delta = applyLinear(this.pointerInverse, {
3256
3550
  x: e.clientX - this.pointer.x,
3257
3551
  y: e.clientY - this.pointer.y,
3258
3552
  });
3259
- const dx = localDelta.x;
3260
- const dy = localDelta.y;
3261
3553
  let width = this.startWidth;
3262
3554
  let height = this.startHeight;
3263
- /**
3264
- * East
3265
- */
3266
- if (this.direction.includes('e')) {
3267
- width += dx;
3268
- }
3269
- /**
3270
- * West
3271
- */
3272
- if (this.direction.includes('w')) {
3273
- width -= dx;
3274
- }
3275
- /**
3276
- * South
3277
- */
3278
- if (this.direction.includes('s')) {
3279
- height += dy;
3280
- }
3281
- /**
3282
- * North
3283
- */
3284
- if (this.direction.includes('n')) {
3285
- height -= dy;
3286
- }
3287
- width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), width)), 'x');
3288
- height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), height)), 'y');
3289
- /**
3290
- * Calculate the intended visual position.
3291
- *
3292
- * This is useful for boundary calculations and
3293
- * emitted resize events.
3294
- */
3295
- const matrix = getLocalToViewportMatrix(this.el.nativeElement);
3296
- const scaleX = Math.hypot(matrix.a, matrix.b) || 1;
3297
- const scaleY = Math.hypot(matrix.c, matrix.d) || 1;
3555
+ if (this.direction.includes('e'))
3556
+ width += delta.x;
3557
+ if (this.direction.includes('w'))
3558
+ width -= delta.x;
3559
+ if (this.direction.includes('s'))
3560
+ height += delta.y;
3561
+ if (this.direction.includes('n'))
3562
+ height -= delta.y;
3563
+ width = this.clampSize(width, 'x');
3564
+ height = this.clampSize(height, 'y');
3565
+ const scaleX = Math.hypot(this.startMatrix.a, this.startMatrix.b) || 1;
3566
+ const scaleY = Math.hypot(this.startMatrix.c, this.startMatrix.d) || 1;
3298
3567
  let left = this.start.left;
3299
3568
  let top = this.start.top;
3300
- if (this.direction.includes('w')) {
3569
+ if (this.direction.includes('w'))
3301
3570
  left = this.start.right - width * scaleX;
3302
- }
3303
- if (this.direction.includes('n')) {
3571
+ if (this.direction.includes('n'))
3304
3572
  top = this.start.bottom - height * scaleY;
3305
- }
3306
3573
  let moveLeft = left - this.start.left;
3307
3574
  let moveTop = top - this.start.top;
3308
- /**
3309
- * Boundary constraint.
3310
- */
3311
- const boundaryEl = this.boundary();
3312
- if (boundaryEl) {
3313
- const clamped = clampResizeWithinBoundary(boundaryEl.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
3575
+ const boundary = this.boundary();
3576
+ if (boundary) {
3577
+ const clamped = clampResizeWithinBoundary(boundary.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
3314
3578
  width = clamped.width / scaleX;
3315
3579
  height = clamped.height / scaleY;
3316
3580
  moveLeft = clamped.left;
@@ -3330,63 +3594,22 @@ class NgxResizable {
3330
3594
  }
3331
3595
  apply(out) {
3332
3596
  const element = this.el.nativeElement;
3333
- /**
3334
- * Always reset to the transform that existed before
3335
- * this resize operation started.
3336
- *
3337
- * This prevents resize corrections from accumulating.
3338
- */
3339
- element.style.transform = this.beforeTransform;
3340
- /**
3341
- * Apply the actual size first.
3342
- *
3343
- * Changing width/height may cause the browser to re-layout
3344
- * the element, especially for relative/flex/grid/static layouts.
3345
- */
3597
+ element.style.translate = this.beforeTranslate;
3346
3598
  const style = this.toStyleSize(out.width, out.height);
3347
3599
  element.style.width = `${style.width}px`;
3348
3600
  element.style.height = `${style.height}px`;
3349
- /**
3350
- * Force/read layout after changing the size.
3351
- */
3352
3601
  const current = element.getBoundingClientRect();
3353
- /**
3354
- * These are the positions where the element MUST visually be.
3355
- *
3356
- * `out.moveLeft` / `out.moveTop` are calculated in `calculate()`
3357
- * from the original visual rectangle.
3358
- */
3359
3602
  const targetLeft = this.start.left + out.moveLeft;
3360
3603
  const targetTop = this.start.top + out.moveTop;
3361
- /**
3362
- * Browser's actual position after width/height changed.
3363
- *
3364
- * The difference between target and current position is the
3365
- * correction that must be applied using transform.
3366
- */
3367
3604
  const correctionX = targetLeft - current.left;
3368
3605
  const correctionY = targetTop - current.top;
3369
- const hasCorrection = correctionX !== 0 || correctionY !== 0;
3370
- if (!hasCorrection) {
3371
- element.style.transform = this.beforeTransform;
3606
+ if (!correctionX && !correctionY) {
3607
+ element.style.translate = this.beforeTranslate;
3372
3608
  return;
3373
3609
  }
3374
- /**
3375
- * Preserve DragRef's existing transform and add only the
3376
- * correction required by Resize.
3377
- */
3378
- const baseTransform = this.beforeTransform && this.beforeTransform !== 'none' ? `${this.beforeTransform} ` : '';
3379
- const localCorrection = viewportDeltaToLocal(element, { x: correctionX, y: correctionY });
3380
- element.style.transform =
3381
- `${baseTransform}translate3d(` + `${localCorrection.x}px, ` + `${localCorrection.y}px, 0)`;
3610
+ const local = viewportDeltaToLocal(element, { x: correctionX, y: correctionY });
3611
+ writeTranslateWithOffset(element, this.baseComputedTranslate, local);
3382
3612
  }
3383
- /**
3384
- * `offsetWidth/Height` (what we measure and report) are BORDER-box sizes. The inline
3385
- * `width/height` we write are interpreted according to `box-sizing`: for the CSS default
3386
- * (`content-box`) writing the border-box number would make a padded/bordered block jump by
3387
- * its padding+border the instant a resize starts. Convert so the box is exactly the size
3388
- * reported in `IResizableOutput`.
3389
- */
3390
3613
  toStyleSize(width, height) {
3391
3614
  const el = this.el.nativeElement;
3392
3615
  const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
@@ -3395,17 +3618,25 @@ class NgxResizable {
3395
3618
  const px = (v) => Number.parseFloat(v) || 0;
3396
3619
  const extraX = px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
3397
3620
  const extraY = px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
3398
- return { width: Math.max(0, width - extraX), height: Math.max(0, height - extraY) };
3621
+ return {
3622
+ width: Math.max(0, width - extraX),
3623
+ height: Math.max(0, height - extraY),
3624
+ };
3399
3625
  }
3400
- clampGrid(value, axis) {
3401
- const g = this.grid();
3402
- // NOTE: the previous code returned the input *signal function* for a numeric grid, so every
3403
- // snapped size became NaN and the resize silently stopped working.
3404
- const grid = typeof g === 'number' ? g : g?.[axis];
3405
- if (!grid || !Number.isFinite(grid) || grid <= 0) {
3406
- return value;
3407
- }
3408
- return Math.round(value / grid) * grid;
3626
+ clampSize(value, axis) {
3627
+ const minInput = axis === 'x' ? this.minWidth() : this.minHeight();
3628
+ const maxInput = axis === 'x' ? this.maxWidth() : this.maxHeight();
3629
+ const min = Number.isFinite(minInput) ? Math.max(0, minInput) : 0;
3630
+ const max = Number.isFinite(maxInput) ? Math.max(min, maxInput) : Infinity;
3631
+ const bounded = Math.min(max, Math.max(min, Number.isFinite(value) ? value : min));
3632
+ const grid = this.grid();
3633
+ const step = typeof grid === 'number' ? grid : grid?.[axis];
3634
+ if (!step || !Number.isFinite(step) || step <= 0)
3635
+ return bounded;
3636
+ // Snap relative to the minimum, then clamp again so grid rounding can never
3637
+ // violate the configured min/max constraints.
3638
+ const snapped = min + Math.round((bounded - min) / step) * step;
3639
+ return Math.min(max, Math.max(min, snapped));
3409
3640
  }
3410
3641
  finish() {
3411
3642
  this.resizing.set(false);
@@ -3421,9 +3652,12 @@ class NgxResizable {
3421
3652
  }
3422
3653
  }
3423
3654
  catch {
3424
- /* already released */
3655
+ // Pointer capture may already have been released.
3425
3656
  }
3426
- this.beforeTransform = '';
3657
+ this.beforeTranslate = '';
3658
+ this.baseComputedTranslate = 'none';
3659
+ this.keyboardBaseTranslate = null;
3660
+ this.keyboardBaseInlineTranslate = '';
3427
3661
  }
3428
3662
  resolvedDirections() {
3429
3663
  const rtl = isRtl(this.el.nativeElement);
@@ -3435,68 +3669,59 @@ class NgxResizable {
3435
3669
  }
3436
3670
  hitDirection(e) {
3437
3671
  const element = this.el.nativeElement;
3438
- const rect = element.getBoundingClientRect();
3439
- const edge = Math.min(12, Math.max(6, Math.min(rect.width, rect.height) * 0.18));
3440
- const isLeft = e.clientX - rect.left <= edge;
3441
- const isRight = rect.right - e.clientX <= edge;
3442
- const isTop = e.clientY - rect.top <= edge;
3443
- const isBottom = rect.bottom - e.clientY <= edge;
3444
- /**
3445
- * Explicit resize handle has priority.
3446
- */
3447
- // TODO notsafe instanceof:node.nodeType === Node.ELEMENT_NODE
3448
- const handle = isHTMLElementTarget(e.target)
3449
- ? e.target.closest('[data-ngx-resize-handle]')
3672
+ const target = e.target;
3673
+ // Explicit handles are allowed even when they are inside the panel.
3674
+ const handle = isElementTarget(target)
3675
+ ? target.closest('[data-ngx-resize-handle]')
3450
3676
  : null;
3451
3677
  if (handle && element.contains(handle)) {
3452
3678
  const raw = handle.dataset['ngxResizeHandle'];
3453
- if (!raw) {
3679
+ if (!raw)
3454
3680
  return null;
3455
- }
3456
3681
  const resolved = resolveLogicalDirection(raw, isRtl(element));
3457
3682
  return this.resolvedDirections().includes(resolved) ? resolved : null;
3458
3683
  }
3459
- /**
3460
- * These are PHYSICAL screen directions.
3461
- *
3462
- * left -> w
3463
- * right -> e
3464
- *
3465
- * RTL is handled only when a logical direction
3466
- * such as "start" / "end" is requested.
3467
- */
3468
- const direction = isTop && isLeft
3469
- ? 'nw'
3470
- : isTop && isRight
3471
- ? 'ne'
3472
- : isBottom && isLeft
3473
- ? 'sw'
3474
- : isBottom && isRight
3475
- ? 'se'
3476
- : isTop
3477
- ? 'n'
3478
- : isRight
3479
- ? 'e'
3480
- : isBottom
3481
- ? 's'
3482
- : isLeft
3483
- ? 'w'
3484
- : null;
3485
- if (!direction) {
3684
+ // Never steal pointer events from nested interactive controls.
3685
+ if (isElementTarget(target) && target.closest(INTERACTIVE_SELECTOR))
3486
3686
  return null;
3487
- }
3488
- return this.resolvedDirections().includes(direction) ? direction : null;
3687
+ const rect = element.getBoundingClientRect();
3688
+ const edge = Math.min(12, Math.max(6, Math.min(rect.width, rect.height) * 0.18));
3689
+ const isLeft = e.clientX - rect.left <= edge;
3690
+ const isRight = rect.right - e.clientX <= edge;
3691
+ const isTop = e.clientY - rect.top <= edge;
3692
+ const isBottom = rect.bottom - e.clientY <= edge;
3693
+ const candidates = [];
3694
+ if (isTop && isLeft)
3695
+ candidates.push('nw', 'n', 'w');
3696
+ else if (isTop && isRight)
3697
+ candidates.push('ne', 'n', 'e');
3698
+ else if (isBottom && isLeft)
3699
+ candidates.push('sw', 's', 'w');
3700
+ else if (isBottom && isRight)
3701
+ candidates.push('se', 's', 'e');
3702
+ else if (isTop)
3703
+ candidates.push('n');
3704
+ else if (isRight)
3705
+ candidates.push('e');
3706
+ else if (isBottom)
3707
+ candidates.push('s');
3708
+ else if (isLeft)
3709
+ candidates.push('w');
3710
+ const allowed = new Set(this.resolvedDirections());
3711
+ return candidates.find((candidate) => allowed.has(candidate)) ?? null;
3489
3712
  }
3490
3713
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxResizable, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
3491
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxResizable, isStandalone: true, selector: "[NgxResizable],[ngxResizable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, directions: { classPropertyName: "directions", publicName: "directions", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", minWidth: "minWidthChange", minHeight: "minHeightChange", maxWidth: "maxWidthChange", maxHeight: "maxHeightChange", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { listeners: { "pointerdown": "onPointerDown($event)", "keydown": "onHandleKeyDown($event)" }, properties: { "style.touch-action": "\"none\"" }, classAttribute: "ngx-resizable" }, ngImport: i0 });
3714
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxResizable, isStandalone: true, selector: "[NgxResizable],[ngxResizable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, directions: { classPropertyName: "directions", publicName: "directions", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", minWidth: "minWidthChange", minHeight: "minHeightChange", maxWidth: "maxWidthChange", maxHeight: "maxHeightChange", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { listeners: { "pointerdown": "onPointerDown($event)", "keydown": "onHandleKeyDown($event)" }, properties: { "attr.aria-keyshortcuts": "\"ArrowLeft ArrowRight ArrowUp ArrowDown\"" }, classAttribute: "ngx-resizable" }, ngImport: i0 });
3492
3715
  }
3493
3716
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxResizable, decorators: [{
3494
3717
  type: Directive,
3495
3718
  args: [{
3496
3719
  selector: '[NgxResizable],[ngxResizable]',
3497
3720
  host: {
3498
- '[style.touch-action]': '"none"',
3499
3721
  class: 'ngx-resizable',
3722
+ // Do not set touch-action:none on the whole panel: it breaks nested controls.
3723
+ // '[style.touch-action]': '"none"',
3724
+ '[attr.aria-keyshortcuts]': '"ArrowLeft ArrowRight ArrowUp ArrowDown"',
3500
3725
  },
3501
3726
  }]
3502
3727
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }, { type: i0.Output, args: ["minWidthChange"] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }, { type: i0.Output, args: ["minHeightChange"] }], maxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxWidth", required: false }] }, { type: i0.Output, args: ["maxWidthChange"] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }, { type: i0.Output, args: ["maxHeightChange"] }], directions: [{ type: i0.Input, args: [{ isSignal: true, alias: "directions", required: false }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], boundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "boundary", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], resizeStart: [{ type: i0.Output, args: ["resizeStart"] }], resizeMove: [{ type: i0.Output, args: ["resizeMove"] }], resizeEnd: [{ type: i0.Output, args: ["resizeEnd"] }], onPointerDown: [{
@@ -3506,13 +3731,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
3506
3731
  type: HostListener,
3507
3732
  args: ['keydown', ['$event']]
3508
3733
  }] } });
3509
- function isHTMLElementTarget(target) {
3734
+ function isElementTarget(target) {
3510
3735
  return (!!target &&
3511
3736
  typeof target === 'object' &&
3512
3737
  'nodeType' in target &&
3513
3738
  target.nodeType === 1 &&
3514
3739
  'closest' in target);
3515
3740
  }
3741
+ function readBorderBoxSize(element, axis) {
3742
+ const win = element.ownerDocument.defaultView;
3743
+ const style = win?.getComputedStyle(element);
3744
+ if (!style)
3745
+ return axis === 'x' ? element.offsetWidth : element.offsetHeight;
3746
+ const horizontal = axis === 'x';
3747
+ const size = Number.parseFloat(horizontal ? style.width : style.height) || 0;
3748
+ if (style.boxSizing === 'border-box')
3749
+ return size;
3750
+ const px = (value) => Number.parseFloat(value) || 0;
3751
+ return (size +
3752
+ (horizontal
3753
+ ? px(style.paddingLeft) +
3754
+ px(style.paddingRight) +
3755
+ px(style.borderLeftWidth) +
3756
+ px(style.borderRightWidth)
3757
+ : px(style.paddingTop) +
3758
+ px(style.paddingBottom) +
3759
+ px(style.borderTopWidth) +
3760
+ px(style.borderBottomWidth)));
3761
+ }
3762
+ /**
3763
+ * Compose a resize-only offset with the existing CSS individual `translate` value.
3764
+ * Keeping the original CSS tokens (instead of parseFloat) preserves percentages, calc(),
3765
+ * and variables. The `transform` channel remains exclusively owned by NgxDraggable.
3766
+ */
3767
+ function writeTranslateWithOffset(element, baseValue, offset) {
3768
+ const [baseX, baseY, baseZ] = splitCssComponents(!baseValue || baseValue === 'none' ? '0px 0px 0px' : baseValue);
3769
+ const x = offset.x ? `calc(${baseX ?? '0px'} ${formatOffset(offset.x)})` : (baseX ?? '0px');
3770
+ const y = offset.y ? `calc(${baseY ?? '0px'} ${formatOffset(offset.y)})` : (baseY ?? '0px');
3771
+ element.style.translate = `${x} ${y} ${baseZ ?? '0px'}`;
3772
+ }
3773
+ function splitCssComponents(value) {
3774
+ const result = [];
3775
+ let depth = 0;
3776
+ let token = '';
3777
+ for (const char of value.trim()) {
3778
+ if (char === '(')
3779
+ depth++;
3780
+ if (char === ')')
3781
+ depth = Math.max(0, depth - 1);
3782
+ if (/\s/.test(char) && depth === 0) {
3783
+ if (token)
3784
+ result.push(token);
3785
+ token = '';
3786
+ }
3787
+ else {
3788
+ token += char;
3789
+ }
3790
+ }
3791
+ if (token)
3792
+ result.push(token);
3793
+ return result;
3794
+ }
3795
+ function formatOffset(value) {
3796
+ return value < 0
3797
+ ? `- ${Number(Math.abs(value).toFixed(4))}px`
3798
+ : `+ ${Number(value.toFixed(4))}px`;
3799
+ }
3516
3800
 
3517
3801
  const NGX_PLACEHOLDER = new InjectionToken('ngx-place-holder');
3518
3802
  /**
@@ -3656,6 +3940,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
3656
3940
  }]
3657
3941
  }] });
3658
3942
 
3943
+ /**
3944
+ * Typed handle API. Prefer ngxResizeHandle="se" in templates; the legacy
3945
+ * data-ngx-resize-handle attribute remains available for backwards compatibility.
3946
+ */
3947
+ class NgxResizeHandle {
3948
+ ngxResizeHandle = input.required(/* @ts-ignore */
3949
+ ...(ngDevMode ? [{ debugName: "ngxResizeHandle" }] : /* istanbul ignore next */ []));
3950
+ parent = inject(NgxResizable, { optional: true, skipSelf: true });
3951
+ onPointerDown(event) {
3952
+ this.parent?.beginResize(event, this.ngxResizeHandle());
3953
+ }
3954
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxResizeHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3955
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxResizeHandle, isStandalone: true, selector: "[ngxResizeHandle]", inputs: { ngxResizeHandle: { classPropertyName: "ngxResizeHandle", publicName: "ngxResizeHandle", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "pointerdown": "onPointerDown($event)" }, properties: { "style.touch-action": "\"none\"", "style.user-select": "\"none\"" } }, ngImport: i0 });
3956
+ }
3957
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxResizeHandle, decorators: [{
3958
+ type: Directive,
3959
+ args: [{
3960
+ selector: '[ngxResizeHandle]',
3961
+ standalone: true,
3962
+ host: {
3963
+ '[style.touch-action]': '"none"',
3964
+ '[style.user-select]': '"none"',
3965
+ },
3966
+ }]
3967
+ }], propDecorators: { ngxResizeHandle: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngxResizeHandle", required: true }] }], onPointerDown: [{
3968
+ type: HostListener,
3969
+ args: ['pointerdown', ['$event']]
3970
+ }] } });
3971
+
3659
3972
  function moveItemInArray(array, from, to) {
3660
3973
  const f = clamp(from, array.length - 1);
3661
3974
  const t = clamp(to, array.length - 1);
@@ -3741,5 +4054,5 @@ function getCleanHtml(el, options) {
3741
4054
  * Generated bundle index. Do not edit.
3742
4055
  */
3743
4056
 
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 };
4057
+ export { AutoScroller, DragDropService, DragRef, DropListGroupRef, DropListRef, NGX_DRAGGABLE, NGX_DRAG_PREVIEW, NGX_DROPLIST, NGX_DROPLIST_GROUP, NGX_PLACEHOLDER, NgxDragPreview, NgxDraggable, NgxDropList, NgxDropListGroup, NgxPlaceholder, NgxResizable, NgxResizeHandle, PlaceHolderRef, adjustForZoom, checkBoundX, checkBoundY, clampResizeWithinBoundary, copyArrayItem, findScrollableAncestor, findScrollableAncestorForAxis, getCleanHtml, getPageZoom, getScrollPosition, isRtl, moveItemInArray, resolveLogicalDirection, stripEditorArtifacts, transferArrayItem };
3745
4058
  //# sourceMappingURL=ngx-kit-drag-resize.mjs.map