ngx-kit 9.1.0 → 9.2.0
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 +36 -0
- package/fesm2022/ngx-kit-drag-resize.mjs +475 -60
- package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -1
- package/fesm2022/ngx-kit-image-viewer.mjs +2 -23
- package/fesm2022/ngx-kit-image-viewer.mjs.map +1 -1
- package/fesm2022/ngx-kit-media-control.mjs +2 -2
- package/fesm2022/ngx-kit-media-control.mjs.map +1 -1
- package/fesm2022/ngx-kit-notify.mjs +2 -2
- package/fesm2022/ngx-kit-side-nav.mjs +2 -0
- package/fesm2022/ngx-kit-side-nav.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ngx-kit-drag-resize.d.ts +86 -5
- package/types/ngx-kit-image-viewer.d.ts +0 -21
- package/types/ngx-kit-side-nav.d.ts +1 -1
|
@@ -132,6 +132,54 @@ function resolveLogicalDirection(dir, rtl) {
|
|
|
132
132
|
|
|
133
133
|
/** Above any app chrome (modals, sticky headers) but below the browser's own UI. */
|
|
134
134
|
const PREVIEW_Z_INDEX = '2147483000';
|
|
135
|
+
/**
|
|
136
|
+
* The clone lives in <body>, so rules that depend on its ORIGINAL ancestors (`.palette .tile`,
|
|
137
|
+
* `.board { font: ... }`, `[dir=rtl] .x`) no longer match it. These are the properties that make
|
|
138
|
+
* the difference between "looks like the block I grabbed" and "looks like an unstyled <div>":
|
|
139
|
+
* the inherited text properties plus the root box's own visual properties. Only the ROOT is
|
|
140
|
+
* handled (one `getComputedStyle` at drag start); use `*ngxDragPreview` for a bespoke preview.
|
|
141
|
+
*/
|
|
142
|
+
const PREVIEW_STYLE_PROPS = [
|
|
143
|
+
// inherited
|
|
144
|
+
'direction',
|
|
145
|
+
'color',
|
|
146
|
+
'font-family',
|
|
147
|
+
'font-size',
|
|
148
|
+
'font-weight',
|
|
149
|
+
'font-style',
|
|
150
|
+
'line-height',
|
|
151
|
+
'letter-spacing',
|
|
152
|
+
'text-align',
|
|
153
|
+
'text-transform',
|
|
154
|
+
'white-space',
|
|
155
|
+
// the root box
|
|
156
|
+
'background-color',
|
|
157
|
+
'background-image',
|
|
158
|
+
'background-size',
|
|
159
|
+
'background-position',
|
|
160
|
+
'background-repeat',
|
|
161
|
+
'border-top',
|
|
162
|
+
'border-right',
|
|
163
|
+
'border-bottom',
|
|
164
|
+
'border-left',
|
|
165
|
+
'border-radius',
|
|
166
|
+
'box-shadow',
|
|
167
|
+
'padding-top',
|
|
168
|
+
'padding-right',
|
|
169
|
+
'padding-bottom',
|
|
170
|
+
'padding-left',
|
|
171
|
+
'opacity',
|
|
172
|
+
];
|
|
173
|
+
function copyVisualStyles(source, target) {
|
|
174
|
+
const cs = source.ownerDocument.defaultView?.getComputedStyle(source);
|
|
175
|
+
if (!cs)
|
|
176
|
+
return;
|
|
177
|
+
for (const prop of PREVIEW_STYLE_PROPS) {
|
|
178
|
+
const value = cs.getPropertyValue(prop);
|
|
179
|
+
if (value)
|
|
180
|
+
target.style.setProperty(prop, value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
135
183
|
class DragPreviewRef {
|
|
136
184
|
tpl;
|
|
137
185
|
/** Set by `ngxDragPreview` so the embedded view can join Angular's change-detection tree. */
|
|
@@ -163,6 +211,7 @@ class DragPreviewRef {
|
|
|
163
211
|
// Avoid duplicated ids in the document.
|
|
164
212
|
this.element.removeAttribute('id');
|
|
165
213
|
this.element.querySelectorAll('[id]').forEach((n) => n.removeAttribute('id'));
|
|
214
|
+
copyVisualStyles(dragEl, this.element);
|
|
166
215
|
this.element.classList.add('ngx-drag-preview');
|
|
167
216
|
this.element.setAttribute('aria-hidden', 'true');
|
|
168
217
|
this.element.setAttribute('inert', '');
|
|
@@ -214,21 +263,56 @@ class DragPreviewRef {
|
|
|
214
263
|
}
|
|
215
264
|
|
|
216
265
|
function viewportDeltaToLocal(element, delta, includeElementTransform = true) {
|
|
217
|
-
const
|
|
266
|
+
const inverse = invertLinear(getLocalToViewportMatrix(element, includeElementTransform));
|
|
267
|
+
return applyLinear(inverse, delta);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Inverse of a 2D linear transform, or `null` when it is not invertible (zero-scale ancestor).
|
|
271
|
+
* Exposed so hot paths (every pointer move) can compute the matrix ONCE per drag instead of
|
|
272
|
+
* walking the ancestor chain with `getComputedStyle` on every event.
|
|
273
|
+
*/
|
|
274
|
+
function invertLinear(matrix) {
|
|
218
275
|
const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
|
|
219
|
-
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-8)
|
|
220
|
-
|
|
276
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-8)
|
|
277
|
+
return null;
|
|
278
|
+
return {
|
|
279
|
+
a: matrix.d / determinant,
|
|
280
|
+
b: -matrix.b / determinant,
|
|
281
|
+
c: -matrix.c / determinant,
|
|
282
|
+
d: matrix.a / determinant,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** Applies a linear transform to a vector. A `null` matrix (see `invertLinear`) yields `{0,0}`. */
|
|
286
|
+
function applyLinear(matrix, delta) {
|
|
287
|
+
if (!matrix)
|
|
221
288
|
return { x: 0, y: 0 };
|
|
222
|
-
}
|
|
223
|
-
const inverseA = matrix.d / determinant;
|
|
224
|
-
const inverseB = -matrix.b / determinant;
|
|
225
|
-
const inverseC = -matrix.c / determinant;
|
|
226
|
-
const inverseD = matrix.a / determinant;
|
|
227
289
|
return {
|
|
228
|
-
x:
|
|
229
|
-
y:
|
|
290
|
+
x: matrix.a * delta.x + matrix.c * delta.y,
|
|
291
|
+
y: matrix.b * delta.x + matrix.d * delta.y,
|
|
230
292
|
};
|
|
231
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Converts a SIZE measured in the viewport pixels of `fromDocument` into the viewport pixels of
|
|
296
|
+
* `toDocument` (they differ when the iframe element is scaled, e.g. a zoomed-out device preview).
|
|
297
|
+
* Returns `null` when the documents are unrelated / cross-origin.
|
|
298
|
+
*/
|
|
299
|
+
function convertSizeBetweenDocuments(size, fromDocument, toDocument) {
|
|
300
|
+
if (fromDocument === toDocument)
|
|
301
|
+
return { ...size };
|
|
302
|
+
const a = convertPointBetweenDocuments({ x: 0, y: 0 }, fromDocument, toDocument);
|
|
303
|
+
const b = convertPointBetweenDocuments({ x: size.x, y: size.y }, fromDocument, toDocument);
|
|
304
|
+
if (!a || !b)
|
|
305
|
+
return null;
|
|
306
|
+
return { x: b.x - a.x, y: b.y - a.y };
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Axis scale (always positive) between `element`'s local CSS pixels and viewport pixels,
|
|
310
|
+
* i.e. how many screen pixels one CSS pixel inside `element` occupies. Rotation is ignored.
|
|
311
|
+
*/
|
|
312
|
+
function getLocalScale(element) {
|
|
313
|
+
const m = getLocalToViewportMatrix(element);
|
|
314
|
+
return { x: Math.hypot(m.a, m.b) || 1, y: Math.hypot(m.c, m.d) || 1 };
|
|
315
|
+
}
|
|
232
316
|
function localDeltaToViewport(element, delta) {
|
|
233
317
|
const matrix = getLocalToViewportMatrix(element);
|
|
234
318
|
return {
|
|
@@ -398,6 +482,14 @@ class DragRef {
|
|
|
398
482
|
lastEffective = { x: 0, y: 0 };
|
|
399
483
|
scrollListener;
|
|
400
484
|
resizeListener;
|
|
485
|
+
/**
|
|
486
|
+
* Every document that hosts a candidate list (the source document, plus any same-origin iframe
|
|
487
|
+
* such as a page-builder canvas) needs its own scroll/resize listeners: `scroll` does not cross
|
|
488
|
+
* the iframe boundary, so scrolling the canvas never reaches a listener on the parent page.
|
|
489
|
+
*/
|
|
490
|
+
documentListeners = new Map();
|
|
491
|
+
/** viewport → local matrix of the dragged element, computed once per free drag. */
|
|
492
|
+
freeDragInverse;
|
|
401
493
|
init() {
|
|
402
494
|
this.previousTransform =
|
|
403
495
|
this.el.ownerDocument.defaultView?.getComputedStyle(this.el).transform ?? '';
|
|
@@ -426,6 +518,7 @@ class DragRef {
|
|
|
426
518
|
this.moveDx = 0;
|
|
427
519
|
this.moveDy = 0;
|
|
428
520
|
this.scrollCompensation = { x: 0, y: 0 };
|
|
521
|
+
this.freeDragInverse = undefined;
|
|
429
522
|
// Only draggables that are direct children of their drop list take part in sorting.
|
|
430
523
|
// Everything else is a plain free drag.
|
|
431
524
|
const list = this.dropList && this.dropList.el === this.el.parentElement ? this.dropList : null;
|
|
@@ -617,7 +710,11 @@ class DragRef {
|
|
|
617
710
|
: '';
|
|
618
711
|
const tx = this.moveDx + this.scrollCompensation.x;
|
|
619
712
|
const ty = this.moveDy + this.scrollCompensation.y;
|
|
620
|
-
|
|
713
|
+
if (this.freeDragInverse === undefined) {
|
|
714
|
+
// Includes the element's own (pre-drag) transform: the translate is appended after it.
|
|
715
|
+
this.freeDragInverse = invertLinear(getLocalToViewportMatrix(this.el, true));
|
|
716
|
+
}
|
|
717
|
+
const local = applyLinear(this.freeDragInverse, { x: tx, y: ty });
|
|
621
718
|
this.el.style.transform = `${base}translate3d(${local.x}px, ${local.y}px, 0)`;
|
|
622
719
|
}
|
|
623
720
|
// -------------------------------------------------------------------------------------
|
|
@@ -646,9 +743,40 @@ class DragRef {
|
|
|
646
743
|
l._refresh();
|
|
647
744
|
this.updateDropTarget(this.lastEffective);
|
|
648
745
|
};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
746
|
+
this.syncDocumentListeners();
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Keeps scroll/resize listeners attached to every document that currently hosts a candidate
|
|
750
|
+
* list. Capture: `scroll` does not bubble, and any ancestor (or the page) may be scrolling.
|
|
751
|
+
*/
|
|
752
|
+
syncDocumentListeners() {
|
|
753
|
+
if (!this.scrollListener || !this.resizeListener)
|
|
754
|
+
return;
|
|
755
|
+
const scroll = this.scrollListener;
|
|
756
|
+
const resize = this.resizeListener;
|
|
757
|
+
const wanted = new Set([this.el.ownerDocument]);
|
|
758
|
+
for (const list of this.cachedLists) {
|
|
759
|
+
const doc = list.el?.ownerDocument;
|
|
760
|
+
if (doc)
|
|
761
|
+
wanted.add(doc);
|
|
762
|
+
}
|
|
763
|
+
for (const [doc, remove] of this.documentListeners) {
|
|
764
|
+
if (!wanted.has(doc)) {
|
|
765
|
+
remove();
|
|
766
|
+
this.documentListeners.delete(doc);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const doc of wanted) {
|
|
770
|
+
const win = doc.defaultView;
|
|
771
|
+
if (!win || this.documentListeners.has(doc))
|
|
772
|
+
continue;
|
|
773
|
+
doc.addEventListener('scroll', scroll, { capture: true, passive: true });
|
|
774
|
+
win.addEventListener('resize', resize, { passive: true });
|
|
775
|
+
this.documentListeners.set(doc, () => {
|
|
776
|
+
doc.removeEventListener('scroll', scroll, { capture: true });
|
|
777
|
+
win.removeEventListener('resize', resize);
|
|
778
|
+
});
|
|
779
|
+
}
|
|
652
780
|
}
|
|
653
781
|
/**
|
|
654
782
|
* Recomputes which registered lists are valid targets for this drag — connected to the
|
|
@@ -677,6 +805,7 @@ class DragRef {
|
|
|
677
805
|
}
|
|
678
806
|
this.dropCandidates = next;
|
|
679
807
|
this.cachedLists = next.map((c) => c.list);
|
|
808
|
+
this.syncDocumentListeners();
|
|
680
809
|
}
|
|
681
810
|
/**
|
|
682
811
|
* Decide which list is under the pointer, switch if needed, then re-sort in the active one.
|
|
@@ -751,11 +880,9 @@ class DragRef {
|
|
|
751
880
|
* regardless of how the drag ended. Only then is the drop event emitted.
|
|
752
881
|
*/
|
|
753
882
|
finishListDrag(commit) {
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (this.resizeListener)
|
|
758
|
-
doc.defaultView?.removeEventListener('resize', this.resizeListener);
|
|
883
|
+
for (const remove of this.documentListeners.values())
|
|
884
|
+
remove();
|
|
885
|
+
this.documentListeners.clear();
|
|
759
886
|
this.scrollListener = this.resizeListener = undefined;
|
|
760
887
|
const target = commit ? this.activeDropList : null;
|
|
761
888
|
const event = target?._finish(this, true) ?? null;
|
|
@@ -1284,6 +1411,13 @@ class SortSession {
|
|
|
1284
1411
|
/** undefined = not created yet, null = this layout has no whitespace separators. */
|
|
1285
1412
|
spacer;
|
|
1286
1413
|
onOffsetChange;
|
|
1414
|
+
/**
|
|
1415
|
+
* viewport → local-CSS-pixel matrix of the container's coordinate space. Every slot is a direct
|
|
1416
|
+
* child of the container, so ONE matrix serves all of them. It is computed lazily and kept for
|
|
1417
|
+
* the whole session: walking the ancestor chain with `getComputedStyle` on every displacement
|
|
1418
|
+
* would put style reads on the hot path (see the "zero style reads per move" test).
|
|
1419
|
+
*/
|
|
1420
|
+
inverse;
|
|
1287
1421
|
constructor(init) {
|
|
1288
1422
|
this.spacer = init.spacer;
|
|
1289
1423
|
this.onOffsetChange = init.onOffsetChange;
|
|
@@ -1356,6 +1490,7 @@ class SortSession {
|
|
|
1356
1490
|
return;
|
|
1357
1491
|
const target = this.targetIdx;
|
|
1358
1492
|
this.clearDisplacement();
|
|
1493
|
+
this.inverse = undefined; // the viewport may have changed the ancestors' transforms
|
|
1359
1494
|
this.snapshot();
|
|
1360
1495
|
this.frame = new ScrollFrame(this.container, true);
|
|
1361
1496
|
if (this.mode === 'transform') {
|
|
@@ -1455,11 +1590,17 @@ class SortSession {
|
|
|
1455
1590
|
}
|
|
1456
1591
|
else {
|
|
1457
1592
|
this.applied.set(e.el, off);
|
|
1458
|
-
const local =
|
|
1593
|
+
const local = this.toLocal(off);
|
|
1459
1594
|
e.el.style.transform = `translate3d(${local.x}px, ${local.y}px, 0)${e.base}`;
|
|
1460
1595
|
}
|
|
1461
1596
|
this.onOffsetChange?.(e.el, off);
|
|
1462
1597
|
}
|
|
1598
|
+
toLocal(delta) {
|
|
1599
|
+
if (this.inverse === undefined) {
|
|
1600
|
+
this.inverse = invertLinear(getLocalToViewportMatrix(this.container, true));
|
|
1601
|
+
}
|
|
1602
|
+
return applyLinear(this.inverse, delta);
|
|
1603
|
+
}
|
|
1463
1604
|
// ---------------------------------------------------------------------------------------
|
|
1464
1605
|
// flow strategy (single placeholder move + FLIP transforms)
|
|
1465
1606
|
// ---------------------------------------------------------------------------------------
|
|
@@ -1481,7 +1622,7 @@ class SortSession {
|
|
|
1481
1622
|
const dx = first[i].left - last[i].left;
|
|
1482
1623
|
const dy = first[i].top - last[i].top;
|
|
1483
1624
|
if (this.animationMs > 0 && (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5)) {
|
|
1484
|
-
const local =
|
|
1625
|
+
const local = this.toLocal({ x: dx, y: dy });
|
|
1485
1626
|
e.el.style.transform = `translate3d(${local.x}px, ${local.y}px, 0)${e.base}`;
|
|
1486
1627
|
moved.push(e);
|
|
1487
1628
|
}
|
|
@@ -1551,6 +1692,14 @@ class DropListRef {
|
|
|
1551
1692
|
dropListGroup;
|
|
1552
1693
|
/** Duration (ms) of the sibling slide animation. `0` disables animation. */
|
|
1553
1694
|
sortAnimationDuration = 200;
|
|
1695
|
+
/**
|
|
1696
|
+
* How the auto-generated placeholder is sized.
|
|
1697
|
+
* - `'item'` (default): exactly the dragged item's footprint.
|
|
1698
|
+
* - `'stretch'`: only the MAIN axis follows the item; the cross axis fills the list. This is
|
|
1699
|
+
* what a page-builder canvas wants when a small palette tile is dragged into a full-width
|
|
1700
|
+
* column of blocks (the tile's own width means nothing there).
|
|
1701
|
+
*/
|
|
1702
|
+
placeholderFit = 'item';
|
|
1554
1703
|
_draggables = new Set();
|
|
1555
1704
|
onDrop = new EventEmitter();
|
|
1556
1705
|
/**
|
|
@@ -2012,14 +2161,35 @@ class DropListRef {
|
|
|
2012
2161
|
if (ph.custom)
|
|
2013
2162
|
return;
|
|
2014
2163
|
placeholder.style.display = drag.originalDisplay;
|
|
2164
|
+
// `rect` is in the VIEWPORT pixels of the dragged item's document. CSS sizes are local to the
|
|
2165
|
+
// list: convert across an (optionally scaled) iframe boundary first, then through the
|
|
2166
|
+
// list's own scaled/zoomed ancestors. Without this a placeholder inside a `scale(.5)` canvas
|
|
2167
|
+
// would be drawn at half the size it reserves.
|
|
2168
|
+
const inTarget = convertSizeBetweenDocuments({ x: rect.width, y: rect.height }, drag.el.ownerDocument, this.el.ownerDocument) ?? { x: rect.width, y: rect.height };
|
|
2169
|
+
const scale = getLocalScale(this.el);
|
|
2170
|
+
const width = Math.abs(inTarget.x) / scale.x;
|
|
2171
|
+
const height = Math.abs(inTarget.y) / scale.y;
|
|
2015
2172
|
// Occupies exactly the dragged item's footprint, so the gap reserved in the list — and the
|
|
2016
2173
|
// sibling-displacement math derived from it — matches the item's real size.
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2174
|
+
const axis = this.placeholderFit === 'stretch' ? fallbackAxis(this.el) : null;
|
|
2175
|
+
if (axis === 'y') {
|
|
2176
|
+
// vertical stack: the width is whatever the list gives its children
|
|
2177
|
+
set('width', 'auto');
|
|
2178
|
+
set('max-width', 'none');
|
|
2179
|
+
if (placeholder.style.display === 'inline' || placeholder.style.display === 'inline-block') {
|
|
2180
|
+
placeholder.style.display = 'block';
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
else {
|
|
2184
|
+
set('width', `${width}px`);
|
|
2185
|
+
}
|
|
2186
|
+
if (axis === 'x') {
|
|
2187
|
+
set('height', 'auto');
|
|
2188
|
+
set('max-height', 'none');
|
|
2189
|
+
}
|
|
2190
|
+
else {
|
|
2191
|
+
set('height', `${height}px`);
|
|
2192
|
+
}
|
|
2023
2193
|
set('visibility', 'visible');
|
|
2024
2194
|
set('opacity', '0.16');
|
|
2025
2195
|
set('pointer-events', 'none');
|
|
@@ -2047,6 +2217,24 @@ function measureHit(el) {
|
|
|
2047
2217
|
return null;
|
|
2048
2218
|
const clips = [];
|
|
2049
2219
|
const root = el.ownerDocument.documentElement;
|
|
2220
|
+
// The visible window of the document itself. Matters for a list inside an <iframe>: a tall
|
|
2221
|
+
// canvas extends far below the iframe's viewport, and a pointer that is already outside the
|
|
2222
|
+
// frame (toolbar, footer) must not count as "over the list".
|
|
2223
|
+
const ownerWin = el.ownerDocument.defaultView;
|
|
2224
|
+
if (ownerWin?.frameElement) {
|
|
2225
|
+
// In quirks mode (an iframe written without <!doctype>) `documentElement.clientHeight` is the
|
|
2226
|
+
// height of the CONTENT, and the viewport size lives on <body> instead.
|
|
2227
|
+
const sizer = el.ownerDocument.compatMode === 'CSS1Compat' ? root : (el.ownerDocument.body ?? root);
|
|
2228
|
+
clips.push({
|
|
2229
|
+
box: {
|
|
2230
|
+
left: 0,
|
|
2231
|
+
top: 0,
|
|
2232
|
+
right: sizer.clientWidth || ownerWin.innerWidth,
|
|
2233
|
+
bottom: sizer.clientHeight || ownerWin.innerHeight,
|
|
2234
|
+
},
|
|
2235
|
+
frame: new ScrollFrame(null, false, null), // the window clip never moves with scrolling
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2050
2238
|
for (let node = el.parentElement; node && node !== root; node = node.parentElement) {
|
|
2051
2239
|
const s = node.ownerDocument.defaultView?.getComputedStyle(node);
|
|
2052
2240
|
if (!s)
|
|
@@ -2250,6 +2438,8 @@ class AutoScroller {
|
|
|
2250
2438
|
raf = 0;
|
|
2251
2439
|
container;
|
|
2252
2440
|
win;
|
|
2441
|
+
/** Document the pointer coordinates (`update(x, y)`) are expressed in. */
|
|
2442
|
+
pointerDoc;
|
|
2253
2443
|
speed = { x: 0, y: 0 };
|
|
2254
2444
|
onTick;
|
|
2255
2445
|
edge;
|
|
@@ -2261,6 +2451,7 @@ class AutoScroller {
|
|
|
2261
2451
|
/** onTick اجرا میشود بعد از هر گام اسکرول، تا با آخرین موقعیت pointer، sort/placeholder بهروز شود. */
|
|
2262
2452
|
start(fromEl, onTick) {
|
|
2263
2453
|
this.win = fromEl.ownerDocument.defaultView ?? undefined;
|
|
2454
|
+
this.pointerDoc = fromEl.ownerDocument;
|
|
2264
2455
|
this.container = findScrollableAncestor(fromEl);
|
|
2265
2456
|
this.onTick = onTick;
|
|
2266
2457
|
}
|
|
@@ -2269,10 +2460,24 @@ class AutoScroller {
|
|
|
2269
2460
|
this.container = findScrollableAncestor(fromEl);
|
|
2270
2461
|
}
|
|
2271
2462
|
update(clientX, clientY) {
|
|
2463
|
+
// The scroll container may live in another document (a page-builder iframe). Its rect is in
|
|
2464
|
+
// THAT document's viewport pixels, so bring the pointer into the same space first.
|
|
2465
|
+
let x = clientX;
|
|
2466
|
+
let y = clientY;
|
|
2467
|
+
const containerDoc = documentOf(this.container);
|
|
2468
|
+
if (this.pointerDoc && containerDoc && containerDoc !== this.pointerDoc) {
|
|
2469
|
+
const p = convertPointBetweenDocuments({ x, y }, this.pointerDoc, containerDoc);
|
|
2470
|
+
if (!p) {
|
|
2471
|
+
this.speed = { x: 0, y: 0 };
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
x = p.x;
|
|
2475
|
+
y = p.y;
|
|
2476
|
+
}
|
|
2272
2477
|
const rect = getScrollRect(this.container);
|
|
2273
2478
|
this.speed = {
|
|
2274
|
-
x: edgeSpeed(
|
|
2275
|
-
y: edgeSpeed(
|
|
2479
|
+
x: edgeSpeed(x, rect.left, rect.right, this.edge, this.maxSpeed),
|
|
2480
|
+
y: edgeSpeed(y, rect.top, rect.bottom, this.edge, this.maxSpeed),
|
|
2276
2481
|
};
|
|
2277
2482
|
if ((this.speed.x || this.speed.y) && !this.raf) {
|
|
2278
2483
|
this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
|
|
@@ -2285,6 +2490,7 @@ class AutoScroller {
|
|
|
2285
2490
|
this.speed = { x: 0, y: 0 };
|
|
2286
2491
|
this.onTick = undefined;
|
|
2287
2492
|
this.win = undefined;
|
|
2493
|
+
this.pointerDoc = undefined;
|
|
2288
2494
|
}
|
|
2289
2495
|
tick = () => {
|
|
2290
2496
|
if (!this.speed.x && !this.speed.y) {
|
|
@@ -2318,7 +2524,21 @@ function getScrollRect(container) {
|
|
|
2318
2524
|
if (isWindow(container)) {
|
|
2319
2525
|
return { left: 0, top: 0, right: container.innerWidth, bottom: container.innerHeight };
|
|
2320
2526
|
}
|
|
2321
|
-
|
|
2527
|
+
// A scroller taller than the screen (or partly outside it) is only "edge-sensitive" at the
|
|
2528
|
+
// edges the user can actually see: clip its rect to the document viewport.
|
|
2529
|
+
const r = container.getBoundingClientRect();
|
|
2530
|
+
const win = container.ownerDocument.defaultView;
|
|
2531
|
+
if (!win)
|
|
2532
|
+
return r;
|
|
2533
|
+
return {
|
|
2534
|
+
left: Math.max(r.left, 0),
|
|
2535
|
+
top: Math.max(r.top, 0),
|
|
2536
|
+
right: Math.min(r.right, win.innerWidth),
|
|
2537
|
+
bottom: Math.min(r.bottom, win.innerHeight),
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
function documentOf(container) {
|
|
2541
|
+
return isWindow(container) ? container.document : container.ownerDocument;
|
|
2322
2542
|
}
|
|
2323
2543
|
function findScrollableAncestor(el) {
|
|
2324
2544
|
let node = el;
|
|
@@ -2328,7 +2548,12 @@ function findScrollableAncestor(el) {
|
|
|
2328
2548
|
break;
|
|
2329
2549
|
const style = win.getComputedStyle(node);
|
|
2330
2550
|
const scrollable = /(auto|scroll|overlay)/.test(style.overflowY + style.overflowX);
|
|
2331
|
-
if (scrollable && node.scrollHeight > node.clientHeight) {
|
|
2551
|
+
if (scrollable && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)) {
|
|
2552
|
+
// <html>/<body> scroll the whole document: their own rect spans the full (tall) page, so
|
|
2553
|
+
// edge detection must use the window's viewport instead.
|
|
2554
|
+
const doc = node.ownerDocument;
|
|
2555
|
+
if (node === doc.documentElement || node === doc.body)
|
|
2556
|
+
return win;
|
|
2332
2557
|
return node;
|
|
2333
2558
|
}
|
|
2334
2559
|
node = node.parentElement;
|
|
@@ -2403,6 +2628,19 @@ class NgxDraggable {
|
|
|
2403
2628
|
/** Auto-scroll the nearest scrollable ancestor while dragging near its edge. */
|
|
2404
2629
|
autoScroll = input(true, /* @ts-ignore */
|
|
2405
2630
|
...(ngDevMode ? [{ debugName: "autoScroll" }] : /* istanbul ignore next */ []));
|
|
2631
|
+
/**
|
|
2632
|
+
* Presses that start inside an element matching this selector never start a drag (the element
|
|
2633
|
+
* stays fully usable: text caret, clicks, selection). Handy for editable regions of a block.
|
|
2634
|
+
*/
|
|
2635
|
+
noDragSelector = input('[data-no-drag]', /* @ts-ignore */
|
|
2636
|
+
...(ngDevMode ? [{ debugName: "noDragSelector" }] : /* istanbul ignore next */ []));
|
|
2637
|
+
/**
|
|
2638
|
+
* When `true`, presses on native interactive controls (`button, a, input, textarea, select`,
|
|
2639
|
+
* `[contenteditable]`) do not start a drag either. Off by default so that a page-builder canvas
|
|
2640
|
+
* block can be grabbed by its own button/link, which is exactly what you want while editing.
|
|
2641
|
+
*/
|
|
2642
|
+
ignoreInteractive = input(false, /* @ts-ignore */
|
|
2643
|
+
...(ngDevMode ? [{ debugName: "ignoreInteractive" }] : /* istanbul ignore next */ []));
|
|
2406
2644
|
set data(v) {
|
|
2407
2645
|
this._ref.data = v;
|
|
2408
2646
|
}
|
|
@@ -2431,6 +2669,8 @@ class NgxDraggable {
|
|
|
2431
2669
|
lastWindowScroll = { left: 0, top: 0 };
|
|
2432
2670
|
removeContainerScroll;
|
|
2433
2671
|
removeWindowScroll;
|
|
2672
|
+
removeDocKeydown;
|
|
2673
|
+
keyboardDrag = false;
|
|
2434
2674
|
renderer = inject(Renderer2);
|
|
2435
2675
|
constructor(host) {
|
|
2436
2676
|
this.host = host;
|
|
@@ -2474,6 +2714,7 @@ class NgxDraggable {
|
|
|
2474
2714
|
this.removeEscape?.();
|
|
2475
2715
|
this.removeCancel?.();
|
|
2476
2716
|
this.removeLostCapture?.();
|
|
2717
|
+
this.removeDocKeydown?.();
|
|
2477
2718
|
this.scroller.stop();
|
|
2478
2719
|
this.stopScrollTracking();
|
|
2479
2720
|
// Destroyed mid-drag (e.g. the model changed underneath us): never leave a body clone behind.
|
|
@@ -2484,7 +2725,7 @@ class NgxDraggable {
|
|
|
2484
2725
|
pointerDown(e) {
|
|
2485
2726
|
if (this.disabled() ||
|
|
2486
2727
|
e.button !== 0 ||
|
|
2487
|
-
|
|
2728
|
+
this.isExcluded(e.target) ||
|
|
2488
2729
|
!this.isOnHandle(e.target))
|
|
2489
2730
|
return;
|
|
2490
2731
|
// Nested draggables: pointerdown bubbles, so without this an ancestor draggable would start
|
|
@@ -2494,15 +2735,27 @@ class NgxDraggable {
|
|
|
2494
2735
|
claimedPointerEvents.add(e);
|
|
2495
2736
|
e.preventDefault();
|
|
2496
2737
|
this.down = true;
|
|
2738
|
+
this.keyboardDrag = false;
|
|
2497
2739
|
this.pointerId = e.pointerId;
|
|
2498
|
-
this
|
|
2740
|
+
// Pointer capture keeps move/up events flowing to this document even while the pointer is
|
|
2741
|
+
// over an <iframe> (the page-builder canvas). It can throw (pointer already gone, synthetic
|
|
2742
|
+
// events from automation tools) — in that case we simply fall back to plain document events.
|
|
2743
|
+
try {
|
|
2744
|
+
this._ref.el.setPointerCapture(this.pointerId);
|
|
2745
|
+
}
|
|
2746
|
+
catch {
|
|
2747
|
+
/* not capturable: document level listeners below still work inside this document */
|
|
2748
|
+
}
|
|
2499
2749
|
this.start = { x: e.clientX, y: e.clientY };
|
|
2500
2750
|
this._ref.boundary = this.boundary();
|
|
2501
2751
|
this._ref.lockAxis = this.lockAxis();
|
|
2502
2752
|
this._ref.pointerDown(this.start);
|
|
2503
|
-
this.removeMove = this.renderer.listen(this._ref.el, 'pointermove', (ev) => this.pointerMove(ev));
|
|
2504
|
-
this.removeUp = this.renderer.listen(this._ref.el, 'pointerup', (ev) => this.pointerUp(ev));
|
|
2505
2753
|
const doc = this._ref.el.ownerDocument;
|
|
2754
|
+
// Listen on the DOCUMENT (captured events bubble up to it): this keeps working when the
|
|
2755
|
+
// dragged element is re-rendered / detached by the framework mid-drag, and it is the same
|
|
2756
|
+
// for real captured events and for events dispatched by test tooling.
|
|
2757
|
+
this.removeMove = this.renderer.listen(doc, 'pointermove', (ev) => this.pointerMove(ev));
|
|
2758
|
+
this.removeUp = this.renderer.listen(doc, 'pointerup', (ev) => this.pointerUp(ev));
|
|
2506
2759
|
this.removeEscape = this.renderer.listen(doc, 'keydown', (ev) => {
|
|
2507
2760
|
if (ev.key === 'Escape' && this.dragging())
|
|
2508
2761
|
this.cancel();
|
|
@@ -2587,9 +2840,7 @@ class NgxDraggable {
|
|
|
2587
2840
|
pointerUp(e) {
|
|
2588
2841
|
if (!this.down || e.pointerId !== this.pointerId)
|
|
2589
2842
|
return;
|
|
2590
|
-
|
|
2591
|
-
this._ref.el.releasePointerCapture(this.pointerId);
|
|
2592
|
-
}
|
|
2843
|
+
this.releaseCapture();
|
|
2593
2844
|
if (this.dragging()) {
|
|
2594
2845
|
// Also when released OUTSIDE every list: endDrag() rolls the drag back and cleans up.
|
|
2595
2846
|
// The release position is authoritative for list sorting (a fast flick may end with
|
|
@@ -2601,6 +2852,9 @@ class NgxDraggable {
|
|
|
2601
2852
|
// endDrag must run for free drags too, otherwise their lifecycle never completes.
|
|
2602
2853
|
this._ref.endDrag();
|
|
2603
2854
|
this.service.end(this._ref);
|
|
2855
|
+
// The browser still fires a `click` on the captured element after the button is released.
|
|
2856
|
+
// After a real drag that click must not select the block / follow its link.
|
|
2857
|
+
this.swallowNextClick();
|
|
2604
2858
|
}
|
|
2605
2859
|
this.finish();
|
|
2606
2860
|
this.dragEnd.emit(this._ref.pointer);
|
|
@@ -2615,15 +2869,27 @@ class NgxDraggable {
|
|
|
2615
2869
|
}
|
|
2616
2870
|
this.pointerUp(e);
|
|
2617
2871
|
}
|
|
2872
|
+
releaseCapture() {
|
|
2873
|
+
if (this.pointerId < 0)
|
|
2874
|
+
return;
|
|
2875
|
+
try {
|
|
2876
|
+
if (this._ref.el.hasPointerCapture?.(this.pointerId)) {
|
|
2877
|
+
this._ref.el.releasePointerCapture(this.pointerId);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
catch {
|
|
2881
|
+
/* element detached or pointer already released */
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2618
2884
|
cancel() {
|
|
2619
2885
|
this._ref.cancelDrag();
|
|
2620
2886
|
this.service.end(this._ref);
|
|
2621
2887
|
this.finish();
|
|
2622
2888
|
}
|
|
2623
2889
|
finish() {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2890
|
+
this.releaseCapture();
|
|
2891
|
+
this.removeDocKeydown?.();
|
|
2892
|
+
this.removeDocKeydown = undefined;
|
|
2627
2893
|
this.dragging.set(false);
|
|
2628
2894
|
this.down = false;
|
|
2629
2895
|
this.scroller.stop();
|
|
@@ -2636,10 +2902,20 @@ class NgxDraggable {
|
|
|
2636
2902
|
this.removeMove = this.removeUp = this.removeEscape = undefined;
|
|
2637
2903
|
this.removeCancel = this.removeLostCapture = undefined;
|
|
2638
2904
|
}
|
|
2905
|
+
/** Keys already processed (the host listener and the document listener may both see one). */
|
|
2906
|
+
handledKeys = new WeakSet();
|
|
2639
2907
|
/** Arrow-key movement for keyboard/assistive-tech users. Shift multiplies the step by 4. */
|
|
2640
2908
|
keyDown(e) {
|
|
2641
|
-
if (this.disabled())
|
|
2909
|
+
if (this.disabled() || this.handledKeys.has(e))
|
|
2910
|
+
return;
|
|
2911
|
+
// Only the draggable ITSELF being focused means "move me". Arrow keys typed into an input,
|
|
2912
|
+
// a textarea or a contenteditable region inside the block belong to that control (caret
|
|
2913
|
+
// movement), and must never start a drag or be swallowed.
|
|
2914
|
+
if (e.target !== this.host.nativeElement && e.target !== this._ref.el && !this.dragging())
|
|
2915
|
+
return;
|
|
2916
|
+
if (this.dragging() && isEditableTarget(e.target))
|
|
2642
2917
|
return;
|
|
2918
|
+
this.handledKeys.add(e);
|
|
2643
2919
|
const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
|
|
2644
2920
|
const deltas = {
|
|
2645
2921
|
ArrowLeft: { x: -step, y: 0 },
|
|
@@ -2665,6 +2941,7 @@ class NgxDraggable {
|
|
|
2665
2941
|
this.dragging.set(true);
|
|
2666
2942
|
this.service.begin(this._ref);
|
|
2667
2943
|
this.startScrollTracking();
|
|
2944
|
+
this.beginKeyboardSession();
|
|
2668
2945
|
this.dragStart.emit(origin);
|
|
2669
2946
|
}
|
|
2670
2947
|
this._ref.nudge(delta.x, delta.y);
|
|
@@ -2677,26 +2954,62 @@ class NgxDraggable {
|
|
|
2677
2954
|
this.service.end(this._ref);
|
|
2678
2955
|
this.dragEnd.emit(this._ref.pointer);
|
|
2679
2956
|
this.finish();
|
|
2957
|
+
this.restoreFocus();
|
|
2680
2958
|
}
|
|
2681
2959
|
else if (e.key === 'Escape' && this.dragging()) {
|
|
2682
2960
|
e.preventDefault();
|
|
2683
2961
|
this._ref.cancelDrag();
|
|
2684
2962
|
this.service.end(this._ref);
|
|
2685
2963
|
this.finish();
|
|
2964
|
+
this.restoreFocus();
|
|
2686
2965
|
}
|
|
2687
2966
|
}
|
|
2967
|
+
/**
|
|
2968
|
+
* A list item is `display:none` while it is being moved, so the browser drops its focus and
|
|
2969
|
+
* the following arrow/Enter/Escape presses would go to <body>. Listen on the document for the
|
|
2970
|
+
* duration of a keyboard drag so the gesture keeps working.
|
|
2971
|
+
*/
|
|
2972
|
+
beginKeyboardSession() {
|
|
2973
|
+
this.keyboardDrag = true;
|
|
2974
|
+
this.removeDocKeydown?.();
|
|
2975
|
+
this.removeDocKeydown = this.renderer.listen(this._ref.el.ownerDocument, 'keydown', (ev) => this.keyDown(ev));
|
|
2976
|
+
}
|
|
2977
|
+
restoreFocus() {
|
|
2978
|
+
this.removeDocKeydown?.();
|
|
2979
|
+
this.removeDocKeydown = undefined;
|
|
2980
|
+
if (this.keyboardDrag) {
|
|
2981
|
+
this.keyboardDrag = false;
|
|
2982
|
+
this._ref.el.focus?.({ preventScroll: true });
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
/** Swallow the single `click` the browser dispatches right after a completed drag. */
|
|
2986
|
+
swallowNextClick() {
|
|
2987
|
+
const doc = this._ref.el.ownerDocument;
|
|
2988
|
+
const stop = (ev) => {
|
|
2989
|
+
ev.stopPropagation();
|
|
2990
|
+
ev.preventDefault();
|
|
2991
|
+
};
|
|
2992
|
+
doc.addEventListener('click', stop, { capture: true, once: true });
|
|
2993
|
+
// If no click follows (pointer released elsewhere), do not eat the user's NEXT real click.
|
|
2994
|
+
doc.defaultView?.setTimeout(() => doc.removeEventListener('click', stop, { capture: true }), 0);
|
|
2995
|
+
}
|
|
2688
2996
|
isOnHandle(target) {
|
|
2689
2997
|
if (!this.dragHandle())
|
|
2690
2998
|
return true;
|
|
2691
2999
|
// TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
|
|
2692
3000
|
return isElementTarget(target) && !!target.closest(this.dragHandle());
|
|
2693
3001
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
3002
|
+
/** Should a press on `target` be left alone (no drag)? */
|
|
3003
|
+
isExcluded(target) {
|
|
3004
|
+
if (!isElementTarget(target))
|
|
3005
|
+
return false;
|
|
3006
|
+
const selector = this.noDragSelector();
|
|
3007
|
+
if (selector && target.closest(selector))
|
|
3008
|
+
return true;
|
|
3009
|
+
return this.ignoreInteractive() && isEditableOrInteractive(target);
|
|
2697
3010
|
}
|
|
2698
3011
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
2699
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxDraggable, isStandalone: true, selector: "[NgxDraggable],[ngxDraggable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, dragRootElement: { classPropertyName: "dragRootElement", publicName: "dragRootElement", isSignal: true, isRequired: false, transformFunction: null }, dragHandle: { classPropertyName: "dragHandle", publicName: "dragHandle", isSignal: true, isRequired: false, transformFunction: null }, lockAxis: { classPropertyName: "lockAxis", publicName: "lockAxis", isSignal: true, isRequired: false, transformFunction: null }, dragThreshold: { classPropertyName: "dragThreshold", publicName: "dragThreshold", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, autoScroll: { classPropertyName: "autoScroll", publicName: "autoScroll", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", dragRootElement: "dragRootElementChange", dragHandle: "dragHandleChange", dragStart: "dragStart", dragMove: "dragMove", dragEnd: "dragEnd" }, host: { properties: { "style.touch-action": "\"none\"", "attr.tabindex": "disabled() ? null : 0", "attr.aria-grabbed": "dragging()", "class.disable-drag": "disabled()" }, classAttribute: "ngx-draggable" }, providers: [{ provide: NGX_DRAGGABLE, useExisting: NgxDraggable }], queries: [{ propertyName: "customPreview", first: true, predicate: NgxDragPreview, descendants: true }], ngImport: i0 });
|
|
3012
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.4", type: NgxDraggable, isStandalone: true, selector: "[NgxDraggable],[ngxDraggable]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, boundary: { classPropertyName: "boundary", publicName: "boundary", isSignal: true, isRequired: false, transformFunction: null }, dragRootElement: { classPropertyName: "dragRootElement", publicName: "dragRootElement", isSignal: true, isRequired: false, transformFunction: null }, dragHandle: { classPropertyName: "dragHandle", publicName: "dragHandle", isSignal: true, isRequired: false, transformFunction: null }, lockAxis: { classPropertyName: "lockAxis", publicName: "lockAxis", isSignal: true, isRequired: false, transformFunction: null }, dragThreshold: { classPropertyName: "dragThreshold", publicName: "dragThreshold", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, autoScroll: { classPropertyName: "autoScroll", publicName: "autoScroll", isSignal: true, isRequired: false, transformFunction: null }, noDragSelector: { classPropertyName: "noDragSelector", publicName: "noDragSelector", isSignal: true, isRequired: false, transformFunction: null }, ignoreInteractive: { classPropertyName: "ignoreInteractive", publicName: "ignoreInteractive", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", dragRootElement: "dragRootElementChange", dragHandle: "dragHandleChange", dragStart: "dragStart", dragMove: "dragMove", dragEnd: "dragEnd" }, host: { properties: { "style.touch-action": "\"none\"", "attr.tabindex": "disabled() ? null : 0", "attr.aria-grabbed": "dragging()", "class.disable-drag": "disabled()" }, classAttribute: "ngx-draggable" }, providers: [{ provide: NGX_DRAGGABLE, useExisting: NgxDraggable }], queries: [{ propertyName: "customPreview", first: true, predicate: NgxDragPreview, descendants: true }], ngImport: i0 });
|
|
2700
3013
|
}
|
|
2701
3014
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, decorators: [{
|
|
2702
3015
|
type: Directive,
|
|
@@ -2711,13 +3024,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
2711
3024
|
'[class.disable-drag]': 'disabled()',
|
|
2712
3025
|
},
|
|
2713
3026
|
}]
|
|
2714
|
-
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], boundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "boundary", required: false }] }], dragRootElement: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragRootElement", required: false }] }, { type: i0.Output, args: ["dragRootElementChange"] }], dragHandle: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragHandle", required: false }] }, { type: i0.Output, args: ["dragHandleChange"] }], lockAxis: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockAxis", required: false }] }], dragThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragThreshold", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], autoScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScroll", required: false }] }], data: [{
|
|
3027
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], boundary: [{ type: i0.Input, args: [{ isSignal: true, alias: "boundary", required: false }] }], dragRootElement: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragRootElement", required: false }] }, { type: i0.Output, args: ["dragRootElementChange"] }], dragHandle: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragHandle", required: false }] }, { type: i0.Output, args: ["dragHandleChange"] }], lockAxis: [{ type: i0.Input, args: [{ isSignal: true, alias: "lockAxis", required: false }] }], dragThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragThreshold", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], autoScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScroll", required: false }] }], noDragSelector: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDragSelector", required: false }] }], ignoreInteractive: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreInteractive", required: false }] }], data: [{
|
|
2715
3028
|
type: Input,
|
|
2716
3029
|
args: ['data']
|
|
2717
3030
|
}], customPreview: [{
|
|
2718
3031
|
type: ContentChild,
|
|
2719
3032
|
args: [NgxDragPreview]
|
|
2720
3033
|
}], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }] } });
|
|
3034
|
+
function isEditableOrInteractive(el) {
|
|
3035
|
+
return !!el.closest('button,a[href],input,textarea,select,[contenteditable]:not([contenteditable="false"])');
|
|
3036
|
+
}
|
|
3037
|
+
function isEditableTarget(target) {
|
|
3038
|
+
if (!isElementTarget(target))
|
|
3039
|
+
return false;
|
|
3040
|
+
return !!target.closest('input,textarea,select,[contenteditable]:not([contenteditable="false"])');
|
|
3041
|
+
}
|
|
2721
3042
|
function isElementTarget(target) {
|
|
2722
3043
|
return (!!target &&
|
|
2723
3044
|
typeof target === 'object' &&
|
|
@@ -2816,7 +3137,12 @@ class NgxResizable {
|
|
|
2816
3137
|
this.beforeTransform = element.style.transform;
|
|
2817
3138
|
this.resizing.set(true);
|
|
2818
3139
|
element.style.transition = 'none';
|
|
2819
|
-
|
|
3140
|
+
try {
|
|
3141
|
+
element.setPointerCapture?.(e.pointerId);
|
|
3142
|
+
}
|
|
3143
|
+
catch {
|
|
3144
|
+
/* synthetic / already-ended pointer: document listeners below still receive the events */
|
|
3145
|
+
}
|
|
2820
3146
|
const doc = element.ownerDocument;
|
|
2821
3147
|
this.removeDocumentListeners = [
|
|
2822
3148
|
this.renderer.listen(doc, 'pointermove', (event) => this.onPointerMove(event)),
|
|
@@ -2873,6 +3199,11 @@ class NgxResizable {
|
|
|
2873
3199
|
if (this.disabled()) {
|
|
2874
3200
|
return;
|
|
2875
3201
|
}
|
|
3202
|
+
// Only when the resizable element itself is focused: arrow keys pressed inside an input /
|
|
3203
|
+
// contenteditable that lives in the block are caret movement, not a resize request.
|
|
3204
|
+
if (e.target !== this.el.nativeElement) {
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
2876
3207
|
const rtl = isRtl(this.el.nativeElement);
|
|
2877
3208
|
const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
|
|
2878
3209
|
let dw = 0;
|
|
@@ -2896,8 +3227,9 @@ class NgxResizable {
|
|
|
2896
3227
|
const rect = this.el.nativeElement.getBoundingClientRect();
|
|
2897
3228
|
const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), this.el.nativeElement.offsetWidth + dw)), 'x');
|
|
2898
3229
|
const height = this.clampGrid(Math.max(this.minHeight(), Math.min(this.maxHeight(), this.el.nativeElement.offsetHeight + dh)), 'y');
|
|
2899
|
-
this.
|
|
2900
|
-
this.el.nativeElement.style.
|
|
3230
|
+
const styled = this.toStyleSize(width, height);
|
|
3231
|
+
this.el.nativeElement.style.width = `${styled.width}px`;
|
|
3232
|
+
this.el.nativeElement.style.height = `${styled.height}px`;
|
|
2901
3233
|
const out = {
|
|
2902
3234
|
width,
|
|
2903
3235
|
height,
|
|
@@ -2967,8 +3299,9 @@ class NgxResizable {
|
|
|
2967
3299
|
/**
|
|
2968
3300
|
* Boundary constraint.
|
|
2969
3301
|
*/
|
|
2970
|
-
|
|
2971
|
-
|
|
3302
|
+
const boundaryEl = this.boundary();
|
|
3303
|
+
if (boundaryEl) {
|
|
3304
|
+
const clamped = clampResizeWithinBoundary(boundaryEl.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
|
|
2972
3305
|
width = clamped.width / scaleX;
|
|
2973
3306
|
height = clamped.height / scaleY;
|
|
2974
3307
|
moveLeft = clamped.left;
|
|
@@ -3001,8 +3334,9 @@ class NgxResizable {
|
|
|
3001
3334
|
* Changing width/height may cause the browser to re-layout
|
|
3002
3335
|
* the element, especially for relative/flex/grid/static layouts.
|
|
3003
3336
|
*/
|
|
3004
|
-
|
|
3005
|
-
element.style.
|
|
3337
|
+
const style = this.toStyleSize(out.width, out.height);
|
|
3338
|
+
element.style.width = `${style.width}px`;
|
|
3339
|
+
element.style.height = `${style.height}px`;
|
|
3006
3340
|
/**
|
|
3007
3341
|
* Force/read layout after changing the size.
|
|
3008
3342
|
*/
|
|
@@ -3037,9 +3371,29 @@ class NgxResizable {
|
|
|
3037
3371
|
element.style.transform =
|
|
3038
3372
|
`${baseTransform}translate3d(` + `${localCorrection.x}px, ` + `${localCorrection.y}px, 0)`;
|
|
3039
3373
|
}
|
|
3374
|
+
/**
|
|
3375
|
+
* `offsetWidth/Height` (what we measure and report) are BORDER-box sizes. The inline
|
|
3376
|
+
* `width/height` we write are interpreted according to `box-sizing`: for the CSS default
|
|
3377
|
+
* (`content-box`) writing the border-box number would make a padded/bordered block jump by
|
|
3378
|
+
* its padding+border the instant a resize starts. Convert so the box is exactly the size
|
|
3379
|
+
* reported in `IResizableOutput`.
|
|
3380
|
+
*/
|
|
3381
|
+
toStyleSize(width, height) {
|
|
3382
|
+
const el = this.el.nativeElement;
|
|
3383
|
+
const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
|
|
3384
|
+
if (!cs || cs.boxSizing === 'border-box')
|
|
3385
|
+
return { width, height };
|
|
3386
|
+
const px = (v) => Number.parseFloat(v) || 0;
|
|
3387
|
+
const extraX = px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
|
|
3388
|
+
const extraY = px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
|
|
3389
|
+
return { width: Math.max(0, width - extraX), height: Math.max(0, height - extraY) };
|
|
3390
|
+
}
|
|
3040
3391
|
clampGrid(value, axis) {
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3392
|
+
const g = this.grid();
|
|
3393
|
+
// NOTE: the previous code returned the input *signal function* for a numeric grid, so every
|
|
3394
|
+
// snapped size became NaN and the resize silently stopped working.
|
|
3395
|
+
const grid = typeof g === 'number' ? g : g?.[axis];
|
|
3396
|
+
if (!grid || !Number.isFinite(grid) || grid <= 0) {
|
|
3043
3397
|
return value;
|
|
3044
3398
|
}
|
|
3045
3399
|
return Math.round(value / grid) * grid;
|
|
@@ -3052,8 +3406,13 @@ class NgxResizable {
|
|
|
3052
3406
|
for (const remove of this.removeDocumentListeners)
|
|
3053
3407
|
remove();
|
|
3054
3408
|
this.removeDocumentListeners = [];
|
|
3055
|
-
|
|
3056
|
-
this.el.nativeElement.
|
|
3409
|
+
try {
|
|
3410
|
+
if (pointerId >= 0 && this.el.nativeElement.hasPointerCapture?.(pointerId)) {
|
|
3411
|
+
this.el.nativeElement.releasePointerCapture(pointerId);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
catch {
|
|
3415
|
+
/* already released */
|
|
3057
3416
|
}
|
|
3058
3417
|
this.beforeTransform = '';
|
|
3059
3418
|
}
|
|
@@ -3194,6 +3553,13 @@ class NgxDropList {
|
|
|
3194
3553
|
set disableSort(value) {
|
|
3195
3554
|
this._ref.disableSort = value === true;
|
|
3196
3555
|
}
|
|
3556
|
+
/** `'stretch'`: the placeholder only copies the item's size along the list's main axis. */
|
|
3557
|
+
set placeholderFit(value) {
|
|
3558
|
+
this._ref.placeholderFit = value === 'stretch' ? 'stretch' : 'item';
|
|
3559
|
+
}
|
|
3560
|
+
set sortAnimationDuration(value) {
|
|
3561
|
+
this._ref.sortAnimationDuration = value == null ? 200 : Math.max(0, value);
|
|
3562
|
+
}
|
|
3197
3563
|
drop = new EventEmitter();
|
|
3198
3564
|
customPlaceholder;
|
|
3199
3565
|
sub;
|
|
@@ -3225,7 +3591,7 @@ class NgxDropList {
|
|
|
3225
3591
|
this.service.removeDropList(this._ref);
|
|
3226
3592
|
}
|
|
3227
3593
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
3228
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: NgxDropList, isStandalone: true, selector: "[NgxDropList],[ngxDropList]", inputs: { data: "data", connectedTo: "connectedTo", disableSort: "disableSort" }, outputs: { drop: "drop" }, host: { classAttribute: "ngx-drop-list" }, providers: [{ provide: NGX_DROPLIST, useExisting: NgxDropList }], queries: [{ propertyName: "customPlaceholder", first: true, predicate: NgxPlaceholder, descendants: true }], ngImport: i0 });
|
|
3594
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.4", type: NgxDropList, isStandalone: true, selector: "[NgxDropList],[ngxDropList]", inputs: { data: "data", connectedTo: "connectedTo", disableSort: "disableSort", placeholderFit: "placeholderFit", sortAnimationDuration: "sortAnimationDuration" }, outputs: { drop: "drop" }, host: { classAttribute: "ngx-drop-list" }, providers: [{ provide: NGX_DROPLIST, useExisting: NgxDropList }], queries: [{ propertyName: "customPlaceholder", first: true, predicate: NgxPlaceholder, descendants: true }], ngImport: i0 });
|
|
3229
3595
|
}
|
|
3230
3596
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, decorators: [{
|
|
3231
3597
|
type: Directive,
|
|
@@ -3241,6 +3607,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
3241
3607
|
type: Input
|
|
3242
3608
|
}], disableSort: [{
|
|
3243
3609
|
type: Input
|
|
3610
|
+
}], placeholderFit: [{
|
|
3611
|
+
type: Input
|
|
3612
|
+
}], sortAnimationDuration: [{
|
|
3613
|
+
type: Input
|
|
3244
3614
|
}], drop: [{
|
|
3245
3615
|
type: Output
|
|
3246
3616
|
}], customPlaceholder: [{
|
|
@@ -3313,9 +3683,54 @@ function clamp(v, max) {
|
|
|
3313
3683
|
return Math.max(0, Math.min(max, v));
|
|
3314
3684
|
}
|
|
3315
3685
|
|
|
3686
|
+
/**
|
|
3687
|
+
* Helpers for a page builder's "export HTML" step.
|
|
3688
|
+
*
|
|
3689
|
+
* While you edit, the directives decorate the canvas DOM: `ngx-*` classes, `tabindex="0"`,
|
|
3690
|
+
* `aria-grabbed`, `touch-action: none`, and — if an export happens mid-drag — a placeholder or a
|
|
3691
|
+
* preview clone. None of that belongs in the page a visitor receives. Run the canvas through
|
|
3692
|
+
* `getCleanHtml()` (or `stripEditorArtifacts()` on a clone you already have) before saving.
|
|
3693
|
+
*/
|
|
3694
|
+
const TRANSIENT_NODES = '.ngx-drag-placeholder, .ngx-drag-preview';
|
|
3695
|
+
/** Mutates `root` (and its subtree) in place. Pass a CLONE of live DOM, never the live canvas. */
|
|
3696
|
+
function stripEditorArtifacts(root, options = {}) {
|
|
3697
|
+
const prefix = options.classPrefix ?? 'ngx-';
|
|
3698
|
+
const dropClasses = new Set(['disable-drag', ...(options.extraClasses ?? [])]);
|
|
3699
|
+
const dropAttrs = ['aria-grabbed', ...(options.extraAttributes ?? [])];
|
|
3700
|
+
root.querySelectorAll(TRANSIENT_NODES).forEach((n) => n.remove());
|
|
3701
|
+
const all = [root, ...Array.from(root.querySelectorAll('*'))];
|
|
3702
|
+
for (const el of all) {
|
|
3703
|
+
// `tabindex="0"` is only ours on draggables (a real focusable of the page keeps its own).
|
|
3704
|
+
if (el.classList.contains('ngx-draggable') && el.getAttribute('tabindex') === '0') {
|
|
3705
|
+
el.removeAttribute('tabindex');
|
|
3706
|
+
}
|
|
3707
|
+
for (const attr of dropAttrs)
|
|
3708
|
+
el.removeAttribute(attr);
|
|
3709
|
+
for (const cls of Array.from(el.classList)) {
|
|
3710
|
+
if (cls.startsWith(prefix) || dropClasses.has(cls))
|
|
3711
|
+
el.classList.remove(cls);
|
|
3712
|
+
}
|
|
3713
|
+
if (!el.classList.length)
|
|
3714
|
+
el.removeAttribute('class');
|
|
3715
|
+
const style = el.style;
|
|
3716
|
+
if (style) {
|
|
3717
|
+
if (style.touchAction === 'none')
|
|
3718
|
+
style.removeProperty('touch-action');
|
|
3719
|
+
if (!el.getAttribute('style')?.trim())
|
|
3720
|
+
el.removeAttribute('style');
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
/** Serialised, editor-free HTML of `el`'s children (the canvas content). */
|
|
3725
|
+
function getCleanHtml(el, options) {
|
|
3726
|
+
const clone = el.cloneNode(true);
|
|
3727
|
+
stripEditorArtifacts(clone, options);
|
|
3728
|
+
return clone.innerHTML;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3316
3731
|
/**
|
|
3317
3732
|
* Generated bundle index. Do not edit.
|
|
3318
3733
|
*/
|
|
3319
3734
|
|
|
3320
|
-
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, getPageZoom, getScrollPosition, isRtl, moveItemInArray, resolveLogicalDirection, transferArrayItem };
|
|
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 };
|
|
3321
3736
|
//# sourceMappingURL=ngx-kit-drag-resize.mjs.map
|