ngx-kit 9.2.2 → 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.
@@ -3169,11 +3169,11 @@ class NgxDraggable {
3169
3169
  if (!this.dragHandle())
3170
3170
  return true;
3171
3171
  // TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
3172
- return isElementTarget(target) && !!target.closest(this.dragHandle());
3172
+ return isElementTarget$1(target) && !!target.closest(this.dragHandle());
3173
3173
  }
3174
3174
  /** Should a press on `target` be left alone (no drag)? */
3175
3175
  isExcluded(target) {
3176
- if (!isElementTarget(target))
3176
+ if (!isElementTarget$1(target))
3177
3177
  return false;
3178
3178
  const selector = this.noDragSelector();
3179
3179
  if (selector && target.closest(selector))
@@ -3207,11 +3207,11 @@ function isEditableOrInteractive(el) {
3207
3207
  return !!el.closest('button,a[href],input,textarea,select,[contenteditable]:not([contenteditable="false"])');
3208
3208
  }
3209
3209
  function isEditableTarget(target) {
3210
- if (!isElementTarget(target))
3210
+ if (!isElementTarget$1(target))
3211
3211
  return false;
3212
3212
  return !!target.closest('input,textarea,select,[contenteditable]:not([contenteditable="false"])');
3213
3213
  }
3214
- function isElementTarget(target) {
3214
+ function isElementTarget$1(target) {
3215
3215
  return (!!target &&
3216
3216
  typeof target === 'object' &&
3217
3217
  'nodeType' in target &&
@@ -3221,6 +3221,43 @@ function isElementTarget(target) {
3221
3221
 
3222
3222
  const ALL_DIRECTIONS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
3223
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(',');
3224
3261
  class NgxResizable {
3225
3262
  el;
3226
3263
  disabled = model(false, /* @ts-ignore */
@@ -3250,14 +3287,16 @@ class NgxResizable {
3250
3287
  ...(ngDevMode ? [{ debugName: "resizing" }] : /* istanbul ignore next */ []));
3251
3288
  direction = 'se';
3252
3289
  start;
3253
- pointer = {
3254
- x: 0,
3255
- y: 0,
3256
- };
3290
+ pointer = { x: 0, y: 0 };
3257
3291
  beforeTransition = '';
3258
3292
  beforeWidth = '';
3259
3293
  beforeHeight = '';
3260
- 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 });
3261
3300
  pointerId = -1;
3262
3301
  startWidth = 0;
3263
3302
  startHeight = 0;
@@ -3273,47 +3312,43 @@ class NgxResizable {
3273
3312
  }
3274
3313
  }
3275
3314
  onPointerDown(e) {
3276
- if (this.disabled() || e.button !== 0) {
3315
+ if (this.disabled() || e.button !== 0 || this.resizing())
3277
3316
  return;
3278
- }
3279
3317
  const dir = this.hitDirection(e);
3280
- 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))
3281
3327
  return;
3282
- }
3283
3328
  e.preventDefault();
3284
- e.stopPropagation();
3285
- e.stopImmediatePropagation();
3286
3329
  const element = this.el.nativeElement;
3287
3330
  this.direction = dir;
3288
3331
  this.pointerId = e.pointerId;
3289
- /**
3290
- * This is the actual visual rectangle.
3291
- * It already contains any transform created by DragRef.
3292
- */
3293
3332
  this.start = element.getBoundingClientRect();
3294
- this.pointer = {
3295
- x: e.clientX,
3296
- y: e.clientY,
3297
- };
3333
+ this.pointer = { x: e.clientX, y: e.clientY };
3298
3334
  this.beforeTransition = element.style.transition;
3299
3335
  this.beforeWidth = element.style.width;
3300
3336
  this.beforeHeight = element.style.height;
3301
- this.startWidth = element.offsetWidth;
3302
- this.startHeight = element.offsetHeight;
3303
- /**
3304
- * Keep DragRef's transform as the immutable base.
3305
- *
3306
- * Resize will temporarily add its own translation
3307
- * on top of this transform.
3308
- */
3309
- 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');
3310
3345
  this.resizing.set(true);
3311
3346
  element.style.transition = 'none';
3312
3347
  try {
3313
3348
  element.setPointerCapture?.(e.pointerId);
3314
3349
  }
3315
3350
  catch {
3316
- /* synthetic / already-ended pointer: document listeners below still receive the events */
3351
+ // Document listeners remain as a fallback.
3317
3352
  }
3318
3353
  const doc = element.ownerDocument;
3319
3354
  this.removeDocumentListeners = [
@@ -3328,29 +3363,23 @@ class NgxResizable {
3328
3363
  this.resizeStart.emit(out);
3329
3364
  }
3330
3365
  onPointerMove(e) {
3331
- if (!this.resizing() || e.pointerId !== this.pointerId) {
3366
+ if (!this.resizing() || e.pointerId !== this.pointerId)
3332
3367
  return;
3333
- }
3334
3368
  const out = this.calculate(e);
3335
3369
  this.apply(out);
3336
3370
  this.resizeMove.emit(out);
3337
3371
  }
3338
3372
  onPointerUp(e) {
3339
- if (!this.resizing() || e.pointerId !== this.pointerId) {
3373
+ if (!this.resizing() || e.pointerId !== this.pointerId)
3340
3374
  return;
3341
- }
3342
3375
  const out = this.calculate(e);
3343
3376
  this.apply(out);
3344
3377
  this.resizeEnd.emit(out);
3345
3378
  this.finish();
3346
3379
  }
3347
3380
  onDocumentKeyDown(e) {
3348
- if (!this.resizing()) {
3381
+ if (!this.resizing() || e.key !== 'Escape')
3349
3382
  return;
3350
- }
3351
- if (e.key !== 'Escape') {
3352
- return;
3353
- }
3354
3383
  e.preventDefault();
3355
3384
  this.restoreBeforeResize();
3356
3385
  this.finish();
@@ -3365,115 +3394,187 @@ class NgxResizable {
3365
3394
  const element = this.el.nativeElement;
3366
3395
  element.style.width = this.beforeWidth;
3367
3396
  element.style.height = this.beforeHeight;
3368
- element.style.transform = this.beforeTransform;
3397
+ element.style.translate = this.beforeTranslate;
3369
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
+ */
3370
3407
  onHandleKeyDown(e) {
3371
- if (this.disabled()) {
3408
+ if (this.disabled() || this.resizing())
3372
3409
  return;
3373
- }
3374
- // Only when the resizable element itself is focused: arrow keys pressed inside an input /
3375
- // contenteditable that lives in the block are caret movement, not a resize request.
3376
- if (e.target !== this.el.nativeElement) {
3410
+ if (e.target !== this.el.nativeElement)
3377
3411
  return;
3378
- }
3379
- const rtl = isRtl(this.el.nativeElement);
3380
- 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);
3381
3417
  let dw = 0;
3382
3418
  let dh = 0;
3383
- if (e.key === 'ArrowRight') {
3384
- 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');
3385
3449
  }
3386
- else if (e.key === 'ArrowLeft') {
3387
- dw = rtl ? step : -step;
3450
+ if (dh !== 0) {
3451
+ height = this.clampSize(oldHeight + dh, 'y');
3388
3452
  }
3389
- else if (e.key === 'ArrowDown') {
3390
- dh = step;
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;
3391
3467
  }
3392
- else if (e.key === 'ArrowUp') {
3393
- 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;
3474
+ }
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);
3394
3486
  }
3395
3487
  else {
3396
- return;
3488
+ element.style.translate = this.keyboardBaseInlineTranslate;
3397
3489
  }
3398
- e.preventDefault();
3399
- const rect = this.el.nativeElement.getBoundingClientRect();
3400
- const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), this.el.nativeElement.offsetWidth + dw)), 'x');
3401
- const height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), this.el.nativeElement.offsetHeight + dh)), 'y');
3402
- const styled = this.toStyleSize(width, height);
3403
- this.el.nativeElement.style.width = `${styled.width}px`;
3404
- this.el.nativeElement.style.height = `${styled.height}px`;
3405
3490
  const out = {
3406
3491
  width,
3407
3492
  height,
3408
- moveLeft: 0,
3409
- moveTop: 0,
3410
- left: rect.left,
3411
- top: rect.top,
3412
- direction: 'se',
3493
+ moveLeft,
3494
+ moveTop,
3495
+ left: rect.left + moveLeft,
3496
+ top: rect.top + moveTop,
3497
+ direction: this.keyboardDirection(horizontalEdge, verticalEdge),
3413
3498
  };
3499
+ this.resizeStart.emit(out);
3414
3500
  this.resizeMove.emit(out);
3415
3501
  this.resizeEnd.emit(out);
3416
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
+ }
3417
3548
  calculate(e) {
3418
- const localDelta = viewportDeltaToLocal(this.el.nativeElement, {
3549
+ const delta = applyLinear(this.pointerInverse, {
3419
3550
  x: e.clientX - this.pointer.x,
3420
3551
  y: e.clientY - this.pointer.y,
3421
3552
  });
3422
- const dx = localDelta.x;
3423
- const dy = localDelta.y;
3424
3553
  let width = this.startWidth;
3425
3554
  let height = this.startHeight;
3426
- /**
3427
- * East
3428
- */
3429
- if (this.direction.includes('e')) {
3430
- width += dx;
3431
- }
3432
- /**
3433
- * West
3434
- */
3435
- if (this.direction.includes('w')) {
3436
- width -= dx;
3437
- }
3438
- /**
3439
- * South
3440
- */
3441
- if (this.direction.includes('s')) {
3442
- height += dy;
3443
- }
3444
- /**
3445
- * North
3446
- */
3447
- if (this.direction.includes('n')) {
3448
- height -= dy;
3449
- }
3450
- width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), width)), 'x');
3451
- height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), height)), 'y');
3452
- /**
3453
- * Calculate the intended visual position.
3454
- *
3455
- * This is useful for boundary calculations and
3456
- * emitted resize events.
3457
- */
3458
- const matrix = getLocalToViewportMatrix(this.el.nativeElement);
3459
- const scaleX = Math.hypot(matrix.a, matrix.b) || 1;
3460
- 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;
3461
3567
  let left = this.start.left;
3462
3568
  let top = this.start.top;
3463
- if (this.direction.includes('w')) {
3569
+ if (this.direction.includes('w'))
3464
3570
  left = this.start.right - width * scaleX;
3465
- }
3466
- if (this.direction.includes('n')) {
3571
+ if (this.direction.includes('n'))
3467
3572
  top = this.start.bottom - height * scaleY;
3468
- }
3469
3573
  let moveLeft = left - this.start.left;
3470
3574
  let moveTop = top - this.start.top;
3471
- /**
3472
- * Boundary constraint.
3473
- */
3474
- const boundaryEl = this.boundary();
3475
- if (boundaryEl) {
3476
- 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);
3477
3578
  width = clamped.width / scaleX;
3478
3579
  height = clamped.height / scaleY;
3479
3580
  moveLeft = clamped.left;
@@ -3493,63 +3594,22 @@ class NgxResizable {
3493
3594
  }
3494
3595
  apply(out) {
3495
3596
  const element = this.el.nativeElement;
3496
- /**
3497
- * Always reset to the transform that existed before
3498
- * this resize operation started.
3499
- *
3500
- * This prevents resize corrections from accumulating.
3501
- */
3502
- element.style.transform = this.beforeTransform;
3503
- /**
3504
- * Apply the actual size first.
3505
- *
3506
- * Changing width/height may cause the browser to re-layout
3507
- * the element, especially for relative/flex/grid/static layouts.
3508
- */
3597
+ element.style.translate = this.beforeTranslate;
3509
3598
  const style = this.toStyleSize(out.width, out.height);
3510
3599
  element.style.width = `${style.width}px`;
3511
3600
  element.style.height = `${style.height}px`;
3512
- /**
3513
- * Force/read layout after changing the size.
3514
- */
3515
3601
  const current = element.getBoundingClientRect();
3516
- /**
3517
- * These are the positions where the element MUST visually be.
3518
- *
3519
- * `out.moveLeft` / `out.moveTop` are calculated in `calculate()`
3520
- * from the original visual rectangle.
3521
- */
3522
3602
  const targetLeft = this.start.left + out.moveLeft;
3523
3603
  const targetTop = this.start.top + out.moveTop;
3524
- /**
3525
- * Browser's actual position after width/height changed.
3526
- *
3527
- * The difference between target and current position is the
3528
- * correction that must be applied using transform.
3529
- */
3530
3604
  const correctionX = targetLeft - current.left;
3531
3605
  const correctionY = targetTop - current.top;
3532
- const hasCorrection = correctionX !== 0 || correctionY !== 0;
3533
- if (!hasCorrection) {
3534
- element.style.transform = this.beforeTransform;
3606
+ if (!correctionX && !correctionY) {
3607
+ element.style.translate = this.beforeTranslate;
3535
3608
  return;
3536
3609
  }
3537
- /**
3538
- * Preserve DragRef's existing transform and add only the
3539
- * correction required by Resize.
3540
- */
3541
- const baseTransform = this.beforeTransform && this.beforeTransform !== 'none' ? `${this.beforeTransform} ` : '';
3542
- const localCorrection = viewportDeltaToLocal(element, { x: correctionX, y: correctionY });
3543
- element.style.transform =
3544
- `${baseTransform}translate3d(` + `${localCorrection.x}px, ` + `${localCorrection.y}px, 0)`;
3610
+ const local = viewportDeltaToLocal(element, { x: correctionX, y: correctionY });
3611
+ writeTranslateWithOffset(element, this.baseComputedTranslate, local);
3545
3612
  }
3546
- /**
3547
- * `offsetWidth/Height` (what we measure and report) are BORDER-box sizes. The inline
3548
- * `width/height` we write are interpreted according to `box-sizing`: for the CSS default
3549
- * (`content-box`) writing the border-box number would make a padded/bordered block jump by
3550
- * its padding+border the instant a resize starts. Convert so the box is exactly the size
3551
- * reported in `IResizableOutput`.
3552
- */
3553
3613
  toStyleSize(width, height) {
3554
3614
  const el = this.el.nativeElement;
3555
3615
  const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
@@ -3558,17 +3618,25 @@ class NgxResizable {
3558
3618
  const px = (v) => Number.parseFloat(v) || 0;
3559
3619
  const extraX = px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
3560
3620
  const extraY = px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
3561
- 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
+ };
3562
3625
  }
3563
- clampGrid(value, axis) {
3564
- const g = this.grid();
3565
- // NOTE: the previous code returned the input *signal function* for a numeric grid, so every
3566
- // snapped size became NaN and the resize silently stopped working.
3567
- const grid = typeof g === 'number' ? g : g?.[axis];
3568
- if (!grid || !Number.isFinite(grid) || grid <= 0) {
3569
- return value;
3570
- }
3571
- 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));
3572
3640
  }
3573
3641
  finish() {
3574
3642
  this.resizing.set(false);
@@ -3584,9 +3652,12 @@ class NgxResizable {
3584
3652
  }
3585
3653
  }
3586
3654
  catch {
3587
- /* already released */
3655
+ // Pointer capture may already have been released.
3588
3656
  }
3589
- this.beforeTransform = '';
3657
+ this.beforeTranslate = '';
3658
+ this.baseComputedTranslate = 'none';
3659
+ this.keyboardBaseTranslate = null;
3660
+ this.keyboardBaseInlineTranslate = '';
3590
3661
  }
3591
3662
  resolvedDirections() {
3592
3663
  const rtl = isRtl(this.el.nativeElement);
@@ -3598,68 +3669,59 @@ class NgxResizable {
3598
3669
  }
3599
3670
  hitDirection(e) {
3600
3671
  const element = this.el.nativeElement;
3601
- const rect = element.getBoundingClientRect();
3602
- const edge = Math.min(12, Math.max(6, Math.min(rect.width, rect.height) * 0.18));
3603
- const isLeft = e.clientX - rect.left <= edge;
3604
- const isRight = rect.right - e.clientX <= edge;
3605
- const isTop = e.clientY - rect.top <= edge;
3606
- const isBottom = rect.bottom - e.clientY <= edge;
3607
- /**
3608
- * Explicit resize handle has priority.
3609
- */
3610
- // TODO notsafe instanceof:node.nodeType === Node.ELEMENT_NODE
3611
- const handle = isHTMLElementTarget(e.target)
3612
- ? 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]')
3613
3676
  : null;
3614
3677
  if (handle && element.contains(handle)) {
3615
3678
  const raw = handle.dataset['ngxResizeHandle'];
3616
- if (!raw) {
3679
+ if (!raw)
3617
3680
  return null;
3618
- }
3619
3681
  const resolved = resolveLogicalDirection(raw, isRtl(element));
3620
3682
  return this.resolvedDirections().includes(resolved) ? resolved : null;
3621
3683
  }
3622
- /**
3623
- * These are PHYSICAL screen directions.
3624
- *
3625
- * left -> w
3626
- * right -> e
3627
- *
3628
- * RTL is handled only when a logical direction
3629
- * such as "start" / "end" is requested.
3630
- */
3631
- const direction = isTop && isLeft
3632
- ? 'nw'
3633
- : isTop && isRight
3634
- ? 'ne'
3635
- : isBottom && isLeft
3636
- ? 'sw'
3637
- : isBottom && isRight
3638
- ? 'se'
3639
- : isTop
3640
- ? 'n'
3641
- : isRight
3642
- ? 'e'
3643
- : isBottom
3644
- ? 's'
3645
- : isLeft
3646
- ? 'w'
3647
- : null;
3648
- if (!direction) {
3684
+ // Never steal pointer events from nested interactive controls.
3685
+ if (isElementTarget(target) && target.closest(INTERACTIVE_SELECTOR))
3649
3686
  return null;
3650
- }
3651
- 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;
3652
3712
  }
3653
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 });
3654
- 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 });
3655
3715
  }
3656
3716
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxResizable, decorators: [{
3657
3717
  type: Directive,
3658
3718
  args: [{
3659
3719
  selector: '[NgxResizable],[ngxResizable]',
3660
3720
  host: {
3661
- '[style.touch-action]': '"none"',
3662
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"',
3663
3725
  },
3664
3726
  }]
3665
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: [{
@@ -3669,13 +3731,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
3669
3731
  type: HostListener,
3670
3732
  args: ['keydown', ['$event']]
3671
3733
  }] } });
3672
- function isHTMLElementTarget(target) {
3734
+ function isElementTarget(target) {
3673
3735
  return (!!target &&
3674
3736
  typeof target === 'object' &&
3675
3737
  'nodeType' in target &&
3676
3738
  target.nodeType === 1 &&
3677
3739
  'closest' in target);
3678
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
+ }
3679
3800
 
3680
3801
  const NGX_PLACEHOLDER = new InjectionToken('ngx-place-holder');
3681
3802
  /**
@@ -3819,6 +3940,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
3819
3940
  }]
3820
3941
  }] });
3821
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
+
3822
3972
  function moveItemInArray(array, from, to) {
3823
3973
  const f = clamp(from, array.length - 1);
3824
3974
  const t = clamp(to, array.length - 1);
@@ -3904,5 +4054,5 @@ function getCleanHtml(el, options) {
3904
4054
  * Generated bundle index. Do not edit.
3905
4055
  */
3906
4056
 
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 };
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 };
3908
4058
  //# sourceMappingURL=ngx-kit-drag-resize.mjs.map