ngx-kit 9.1.0 → 9.2.1
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 +492 -68
- 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 +87 -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,7 +2438,10 @@ 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 };
|
|
2444
|
+
carry = { x: 0, y: 0 };
|
|
2254
2445
|
onTick;
|
|
2255
2446
|
edge;
|
|
2256
2447
|
maxSpeed;
|
|
@@ -2261,6 +2452,7 @@ class AutoScroller {
|
|
|
2261
2452
|
/** onTick اجرا میشود بعد از هر گام اسکرول، تا با آخرین موقعیت pointer، sort/placeholder بهروز شود. */
|
|
2262
2453
|
start(fromEl, onTick) {
|
|
2263
2454
|
this.win = fromEl.ownerDocument.defaultView ?? undefined;
|
|
2455
|
+
this.pointerDoc = fromEl.ownerDocument;
|
|
2264
2456
|
this.container = findScrollableAncestor(fromEl);
|
|
2265
2457
|
this.onTick = onTick;
|
|
2266
2458
|
}
|
|
@@ -2269,10 +2461,24 @@ class AutoScroller {
|
|
|
2269
2461
|
this.container = findScrollableAncestor(fromEl);
|
|
2270
2462
|
}
|
|
2271
2463
|
update(clientX, clientY) {
|
|
2464
|
+
// The scroll container may live in another document (a page-builder iframe). Its rect is in
|
|
2465
|
+
// THAT document's viewport pixels, so bring the pointer into the same space first.
|
|
2466
|
+
let x = clientX;
|
|
2467
|
+
let y = clientY;
|
|
2468
|
+
const containerDoc = documentOf(this.container);
|
|
2469
|
+
if (this.pointerDoc && containerDoc && containerDoc !== this.pointerDoc) {
|
|
2470
|
+
const p = convertPointBetweenDocuments({ x, y }, this.pointerDoc, containerDoc);
|
|
2471
|
+
if (!p) {
|
|
2472
|
+
this.speed = { x: 0, y: 0 };
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
x = p.x;
|
|
2476
|
+
y = p.y;
|
|
2477
|
+
}
|
|
2272
2478
|
const rect = getScrollRect(this.container);
|
|
2273
2479
|
this.speed = {
|
|
2274
|
-
x: edgeSpeed(
|
|
2275
|
-
y: edgeSpeed(
|
|
2480
|
+
x: edgeSpeed(x, rect.left, rect.right, this.edge, this.maxSpeed),
|
|
2481
|
+
y: edgeSpeed(y, rect.top, rect.bottom, this.edge, this.maxSpeed),
|
|
2276
2482
|
};
|
|
2277
2483
|
if ((this.speed.x || this.speed.y) && !this.raf) {
|
|
2278
2484
|
this.raf = this.win?.requestAnimationFrame(this.tick) ?? 0;
|
|
@@ -2283,8 +2489,10 @@ class AutoScroller {
|
|
|
2283
2489
|
this.win?.cancelAnimationFrame(this.raf);
|
|
2284
2490
|
this.raf = 0;
|
|
2285
2491
|
this.speed = { x: 0, y: 0 };
|
|
2492
|
+
this.carry = { x: 0, y: 0 };
|
|
2286
2493
|
this.onTick = undefined;
|
|
2287
2494
|
this.win = undefined;
|
|
2495
|
+
this.pointerDoc = undefined;
|
|
2288
2496
|
}
|
|
2289
2497
|
tick = () => {
|
|
2290
2498
|
if (!this.speed.x && !this.speed.y) {
|
|
@@ -2292,14 +2500,21 @@ class AutoScroller {
|
|
|
2292
2500
|
return;
|
|
2293
2501
|
}
|
|
2294
2502
|
const container = this.container;
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2503
|
+
// Edge speed is measured in viewport pixels; an element's scrollLeft/Top are local to it.
|
|
2504
|
+
const local = isWindow(container) ? this.speed : viewportDeltaToLocal(container, this.speed);
|
|
2505
|
+
// Browsers snap scroll offsets to whole pixels, so a slow speed (< 1px/frame, i.e. when the
|
|
2506
|
+
// pointer has just entered the edge zone) would never move anything. Carry the remainder.
|
|
2507
|
+
this.carry.x += local.x;
|
|
2508
|
+
this.carry.y += local.y;
|
|
2509
|
+
const dx = Math.trunc(this.carry.x);
|
|
2510
|
+
const dy = Math.trunc(this.carry.y);
|
|
2511
|
+
this.carry.x -= dx;
|
|
2512
|
+
this.carry.y -= dy;
|
|
2513
|
+
if (dx || dy) {
|
|
2514
|
+
// `behavior: 'instant'` matters: with `scroll-behavior: smooth` on <html> or on the scroller
|
|
2515
|
+
// (very common in apps) every per-frame scroll would start a new animation that cancels the
|
|
2516
|
+
// previous one, and the list would not move at all.
|
|
2517
|
+
container.scrollBy({ left: dx, top: dy, behavior: 'instant' });
|
|
2303
2518
|
}
|
|
2304
2519
|
// Pointer ثابت مانده ولی محتوا اسکرول شده؛
|
|
2305
2520
|
// بنابراین موقعیت placeholder/sort را دوباره محاسبه کن.
|
|
@@ -2318,7 +2533,21 @@ function getScrollRect(container) {
|
|
|
2318
2533
|
if (isWindow(container)) {
|
|
2319
2534
|
return { left: 0, top: 0, right: container.innerWidth, bottom: container.innerHeight };
|
|
2320
2535
|
}
|
|
2321
|
-
|
|
2536
|
+
// A scroller taller than the screen (or partly outside it) is only "edge-sensitive" at the
|
|
2537
|
+
// edges the user can actually see: clip its rect to the document viewport.
|
|
2538
|
+
const r = container.getBoundingClientRect();
|
|
2539
|
+
const win = container.ownerDocument.defaultView;
|
|
2540
|
+
if (!win)
|
|
2541
|
+
return r;
|
|
2542
|
+
return {
|
|
2543
|
+
left: Math.max(r.left, 0),
|
|
2544
|
+
top: Math.max(r.top, 0),
|
|
2545
|
+
right: Math.min(r.right, win.innerWidth),
|
|
2546
|
+
bottom: Math.min(r.bottom, win.innerHeight),
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
function documentOf(container) {
|
|
2550
|
+
return isWindow(container) ? container.document : container.ownerDocument;
|
|
2322
2551
|
}
|
|
2323
2552
|
function findScrollableAncestor(el) {
|
|
2324
2553
|
let node = el;
|
|
@@ -2328,7 +2557,12 @@ function findScrollableAncestor(el) {
|
|
|
2328
2557
|
break;
|
|
2329
2558
|
const style = win.getComputedStyle(node);
|
|
2330
2559
|
const scrollable = /(auto|scroll|overlay)/.test(style.overflowY + style.overflowX);
|
|
2331
|
-
if (scrollable && node.scrollHeight > node.clientHeight) {
|
|
2560
|
+
if (scrollable && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth)) {
|
|
2561
|
+
// <html>/<body> scroll the whole document: their own rect spans the full (tall) page, so
|
|
2562
|
+
// edge detection must use the window's viewport instead.
|
|
2563
|
+
const doc = node.ownerDocument;
|
|
2564
|
+
if (node === doc.documentElement || node === doc.body)
|
|
2565
|
+
return win;
|
|
2332
2566
|
return node;
|
|
2333
2567
|
}
|
|
2334
2568
|
node = node.parentElement;
|
|
@@ -2403,6 +2637,19 @@ class NgxDraggable {
|
|
|
2403
2637
|
/** Auto-scroll the nearest scrollable ancestor while dragging near its edge. */
|
|
2404
2638
|
autoScroll = input(true, /* @ts-ignore */
|
|
2405
2639
|
...(ngDevMode ? [{ debugName: "autoScroll" }] : /* istanbul ignore next */ []));
|
|
2640
|
+
/**
|
|
2641
|
+
* Presses that start inside an element matching this selector never start a drag (the element
|
|
2642
|
+
* stays fully usable: text caret, clicks, selection). Handy for editable regions of a block.
|
|
2643
|
+
*/
|
|
2644
|
+
noDragSelector = input('[data-no-drag]', /* @ts-ignore */
|
|
2645
|
+
...(ngDevMode ? [{ debugName: "noDragSelector" }] : /* istanbul ignore next */ []));
|
|
2646
|
+
/**
|
|
2647
|
+
* When `true`, presses on native interactive controls (`button, a, input, textarea, select`,
|
|
2648
|
+
* `[contenteditable]`) do not start a drag either. Off by default so that a page-builder canvas
|
|
2649
|
+
* block can be grabbed by its own button/link, which is exactly what you want while editing.
|
|
2650
|
+
*/
|
|
2651
|
+
ignoreInteractive = input(false, /* @ts-ignore */
|
|
2652
|
+
...(ngDevMode ? [{ debugName: "ignoreInteractive" }] : /* istanbul ignore next */ []));
|
|
2406
2653
|
set data(v) {
|
|
2407
2654
|
this._ref.data = v;
|
|
2408
2655
|
}
|
|
@@ -2431,6 +2678,8 @@ class NgxDraggable {
|
|
|
2431
2678
|
lastWindowScroll = { left: 0, top: 0 };
|
|
2432
2679
|
removeContainerScroll;
|
|
2433
2680
|
removeWindowScroll;
|
|
2681
|
+
removeDocKeydown;
|
|
2682
|
+
keyboardDrag = false;
|
|
2434
2683
|
renderer = inject(Renderer2);
|
|
2435
2684
|
constructor(host) {
|
|
2436
2685
|
this.host = host;
|
|
@@ -2474,6 +2723,7 @@ class NgxDraggable {
|
|
|
2474
2723
|
this.removeEscape?.();
|
|
2475
2724
|
this.removeCancel?.();
|
|
2476
2725
|
this.removeLostCapture?.();
|
|
2726
|
+
this.removeDocKeydown?.();
|
|
2477
2727
|
this.scroller.stop();
|
|
2478
2728
|
this.stopScrollTracking();
|
|
2479
2729
|
// Destroyed mid-drag (e.g. the model changed underneath us): never leave a body clone behind.
|
|
@@ -2484,7 +2734,7 @@ class NgxDraggable {
|
|
|
2484
2734
|
pointerDown(e) {
|
|
2485
2735
|
if (this.disabled() ||
|
|
2486
2736
|
e.button !== 0 ||
|
|
2487
|
-
|
|
2737
|
+
this.isExcluded(e.target) ||
|
|
2488
2738
|
!this.isOnHandle(e.target))
|
|
2489
2739
|
return;
|
|
2490
2740
|
// Nested draggables: pointerdown bubbles, so without this an ancestor draggable would start
|
|
@@ -2494,15 +2744,27 @@ class NgxDraggable {
|
|
|
2494
2744
|
claimedPointerEvents.add(e);
|
|
2495
2745
|
e.preventDefault();
|
|
2496
2746
|
this.down = true;
|
|
2747
|
+
this.keyboardDrag = false;
|
|
2497
2748
|
this.pointerId = e.pointerId;
|
|
2498
|
-
this
|
|
2749
|
+
// Pointer capture keeps move/up events flowing to this document even while the pointer is
|
|
2750
|
+
// over an <iframe> (the page-builder canvas). It can throw (pointer already gone, synthetic
|
|
2751
|
+
// events from automation tools) — in that case we simply fall back to plain document events.
|
|
2752
|
+
try {
|
|
2753
|
+
this._ref.el.setPointerCapture(this.pointerId);
|
|
2754
|
+
}
|
|
2755
|
+
catch {
|
|
2756
|
+
/* not capturable: document level listeners below still work inside this document */
|
|
2757
|
+
}
|
|
2499
2758
|
this.start = { x: e.clientX, y: e.clientY };
|
|
2500
2759
|
this._ref.boundary = this.boundary();
|
|
2501
2760
|
this._ref.lockAxis = this.lockAxis();
|
|
2502
2761
|
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
2762
|
const doc = this._ref.el.ownerDocument;
|
|
2763
|
+
// Listen on the DOCUMENT (captured events bubble up to it): this keeps working when the
|
|
2764
|
+
// dragged element is re-rendered / detached by the framework mid-drag, and it is the same
|
|
2765
|
+
// for real captured events and for events dispatched by test tooling.
|
|
2766
|
+
this.removeMove = this.renderer.listen(doc, 'pointermove', (ev) => this.pointerMove(ev));
|
|
2767
|
+
this.removeUp = this.renderer.listen(doc, 'pointerup', (ev) => this.pointerUp(ev));
|
|
2506
2768
|
this.removeEscape = this.renderer.listen(doc, 'keydown', (ev) => {
|
|
2507
2769
|
if (ev.key === 'Escape' && this.dragging())
|
|
2508
2770
|
this.cancel();
|
|
@@ -2587,9 +2849,7 @@ class NgxDraggable {
|
|
|
2587
2849
|
pointerUp(e) {
|
|
2588
2850
|
if (!this.down || e.pointerId !== this.pointerId)
|
|
2589
2851
|
return;
|
|
2590
|
-
|
|
2591
|
-
this._ref.el.releasePointerCapture(this.pointerId);
|
|
2592
|
-
}
|
|
2852
|
+
this.releaseCapture();
|
|
2593
2853
|
if (this.dragging()) {
|
|
2594
2854
|
// Also when released OUTSIDE every list: endDrag() rolls the drag back and cleans up.
|
|
2595
2855
|
// The release position is authoritative for list sorting (a fast flick may end with
|
|
@@ -2601,6 +2861,9 @@ class NgxDraggable {
|
|
|
2601
2861
|
// endDrag must run for free drags too, otherwise their lifecycle never completes.
|
|
2602
2862
|
this._ref.endDrag();
|
|
2603
2863
|
this.service.end(this._ref);
|
|
2864
|
+
// The browser still fires a `click` on the captured element after the button is released.
|
|
2865
|
+
// After a real drag that click must not select the block / follow its link.
|
|
2866
|
+
this.swallowNextClick();
|
|
2604
2867
|
}
|
|
2605
2868
|
this.finish();
|
|
2606
2869
|
this.dragEnd.emit(this._ref.pointer);
|
|
@@ -2615,15 +2878,27 @@ class NgxDraggable {
|
|
|
2615
2878
|
}
|
|
2616
2879
|
this.pointerUp(e);
|
|
2617
2880
|
}
|
|
2881
|
+
releaseCapture() {
|
|
2882
|
+
if (this.pointerId < 0)
|
|
2883
|
+
return;
|
|
2884
|
+
try {
|
|
2885
|
+
if (this._ref.el.hasPointerCapture?.(this.pointerId)) {
|
|
2886
|
+
this._ref.el.releasePointerCapture(this.pointerId);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
catch {
|
|
2890
|
+
/* element detached or pointer already released */
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2618
2893
|
cancel() {
|
|
2619
2894
|
this._ref.cancelDrag();
|
|
2620
2895
|
this.service.end(this._ref);
|
|
2621
2896
|
this.finish();
|
|
2622
2897
|
}
|
|
2623
2898
|
finish() {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2899
|
+
this.releaseCapture();
|
|
2900
|
+
this.removeDocKeydown?.();
|
|
2901
|
+
this.removeDocKeydown = undefined;
|
|
2627
2902
|
this.dragging.set(false);
|
|
2628
2903
|
this.down = false;
|
|
2629
2904
|
this.scroller.stop();
|
|
@@ -2636,10 +2911,20 @@ class NgxDraggable {
|
|
|
2636
2911
|
this.removeMove = this.removeUp = this.removeEscape = undefined;
|
|
2637
2912
|
this.removeCancel = this.removeLostCapture = undefined;
|
|
2638
2913
|
}
|
|
2914
|
+
/** Keys already processed (the host listener and the document listener may both see one). */
|
|
2915
|
+
handledKeys = new WeakSet();
|
|
2639
2916
|
/** Arrow-key movement for keyboard/assistive-tech users. Shift multiplies the step by 4. */
|
|
2640
2917
|
keyDown(e) {
|
|
2641
|
-
if (this.disabled())
|
|
2918
|
+
if (this.disabled() || this.handledKeys.has(e))
|
|
2919
|
+
return;
|
|
2920
|
+
// Only the draggable ITSELF being focused means "move me". Arrow keys typed into an input,
|
|
2921
|
+
// a textarea or a contenteditable region inside the block belong to that control (caret
|
|
2922
|
+
// movement), and must never start a drag or be swallowed.
|
|
2923
|
+
if (e.target !== this.host.nativeElement && e.target !== this._ref.el && !this.dragging())
|
|
2642
2924
|
return;
|
|
2925
|
+
if (this.dragging() && isEditableTarget(e.target))
|
|
2926
|
+
return;
|
|
2927
|
+
this.handledKeys.add(e);
|
|
2643
2928
|
const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
|
|
2644
2929
|
const deltas = {
|
|
2645
2930
|
ArrowLeft: { x: -step, y: 0 },
|
|
@@ -2665,6 +2950,7 @@ class NgxDraggable {
|
|
|
2665
2950
|
this.dragging.set(true);
|
|
2666
2951
|
this.service.begin(this._ref);
|
|
2667
2952
|
this.startScrollTracking();
|
|
2953
|
+
this.beginKeyboardSession();
|
|
2668
2954
|
this.dragStart.emit(origin);
|
|
2669
2955
|
}
|
|
2670
2956
|
this._ref.nudge(delta.x, delta.y);
|
|
@@ -2677,26 +2963,62 @@ class NgxDraggable {
|
|
|
2677
2963
|
this.service.end(this._ref);
|
|
2678
2964
|
this.dragEnd.emit(this._ref.pointer);
|
|
2679
2965
|
this.finish();
|
|
2966
|
+
this.restoreFocus();
|
|
2680
2967
|
}
|
|
2681
2968
|
else if (e.key === 'Escape' && this.dragging()) {
|
|
2682
2969
|
e.preventDefault();
|
|
2683
2970
|
this._ref.cancelDrag();
|
|
2684
2971
|
this.service.end(this._ref);
|
|
2685
2972
|
this.finish();
|
|
2973
|
+
this.restoreFocus();
|
|
2686
2974
|
}
|
|
2687
2975
|
}
|
|
2976
|
+
/**
|
|
2977
|
+
* A list item is `display:none` while it is being moved, so the browser drops its focus and
|
|
2978
|
+
* the following arrow/Enter/Escape presses would go to <body>. Listen on the document for the
|
|
2979
|
+
* duration of a keyboard drag so the gesture keeps working.
|
|
2980
|
+
*/
|
|
2981
|
+
beginKeyboardSession() {
|
|
2982
|
+
this.keyboardDrag = true;
|
|
2983
|
+
this.removeDocKeydown?.();
|
|
2984
|
+
this.removeDocKeydown = this.renderer.listen(this._ref.el.ownerDocument, 'keydown', (ev) => this.keyDown(ev));
|
|
2985
|
+
}
|
|
2986
|
+
restoreFocus() {
|
|
2987
|
+
this.removeDocKeydown?.();
|
|
2988
|
+
this.removeDocKeydown = undefined;
|
|
2989
|
+
if (this.keyboardDrag) {
|
|
2990
|
+
this.keyboardDrag = false;
|
|
2991
|
+
this._ref.el.focus?.({ preventScroll: true });
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
/** Swallow the single `click` the browser dispatches right after a completed drag. */
|
|
2995
|
+
swallowNextClick() {
|
|
2996
|
+
const doc = this._ref.el.ownerDocument;
|
|
2997
|
+
const stop = (ev) => {
|
|
2998
|
+
ev.stopPropagation();
|
|
2999
|
+
ev.preventDefault();
|
|
3000
|
+
};
|
|
3001
|
+
doc.addEventListener('click', stop, { capture: true, once: true });
|
|
3002
|
+
// If no click follows (pointer released elsewhere), do not eat the user's NEXT real click.
|
|
3003
|
+
doc.defaultView?.setTimeout(() => doc.removeEventListener('click', stop, { capture: true }), 0);
|
|
3004
|
+
}
|
|
2688
3005
|
isOnHandle(target) {
|
|
2689
3006
|
if (!this.dragHandle())
|
|
2690
3007
|
return true;
|
|
2691
3008
|
// TODO notsafe instanseof: node.nodeType === Node.ELEMENT_NODE
|
|
2692
3009
|
return isElementTarget(target) && !!target.closest(this.dragHandle());
|
|
2693
3010
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
3011
|
+
/** Should a press on `target` be left alone (no drag)? */
|
|
3012
|
+
isExcluded(target) {
|
|
3013
|
+
if (!isElementTarget(target))
|
|
3014
|
+
return false;
|
|
3015
|
+
const selector = this.noDragSelector();
|
|
3016
|
+
if (selector && target.closest(selector))
|
|
3017
|
+
return true;
|
|
3018
|
+
return this.ignoreInteractive() && isEditableOrInteractive(target);
|
|
2697
3019
|
}
|
|
2698
3020
|
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 });
|
|
3021
|
+
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
3022
|
}
|
|
2701
3023
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDraggable, decorators: [{
|
|
2702
3024
|
type: Directive,
|
|
@@ -2711,13 +3033,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
2711
3033
|
'[class.disable-drag]': 'disabled()',
|
|
2712
3034
|
},
|
|
2713
3035
|
}]
|
|
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: [{
|
|
3036
|
+
}], 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
3037
|
type: Input,
|
|
2716
3038
|
args: ['data']
|
|
2717
3039
|
}], customPreview: [{
|
|
2718
3040
|
type: ContentChild,
|
|
2719
3041
|
args: [NgxDragPreview]
|
|
2720
3042
|
}], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }] } });
|
|
3043
|
+
function isEditableOrInteractive(el) {
|
|
3044
|
+
return !!el.closest('button,a[href],input,textarea,select,[contenteditable]:not([contenteditable="false"])');
|
|
3045
|
+
}
|
|
3046
|
+
function isEditableTarget(target) {
|
|
3047
|
+
if (!isElementTarget(target))
|
|
3048
|
+
return false;
|
|
3049
|
+
return !!target.closest('input,textarea,select,[contenteditable]:not([contenteditable="false"])');
|
|
3050
|
+
}
|
|
2721
3051
|
function isElementTarget(target) {
|
|
2722
3052
|
return (!!target &&
|
|
2723
3053
|
typeof target === 'object' &&
|
|
@@ -2816,7 +3146,12 @@ class NgxResizable {
|
|
|
2816
3146
|
this.beforeTransform = element.style.transform;
|
|
2817
3147
|
this.resizing.set(true);
|
|
2818
3148
|
element.style.transition = 'none';
|
|
2819
|
-
|
|
3149
|
+
try {
|
|
3150
|
+
element.setPointerCapture?.(e.pointerId);
|
|
3151
|
+
}
|
|
3152
|
+
catch {
|
|
3153
|
+
/* synthetic / already-ended pointer: document listeners below still receive the events */
|
|
3154
|
+
}
|
|
2820
3155
|
const doc = element.ownerDocument;
|
|
2821
3156
|
this.removeDocumentListeners = [
|
|
2822
3157
|
this.renderer.listen(doc, 'pointermove', (event) => this.onPointerMove(event)),
|
|
@@ -2873,6 +3208,11 @@ class NgxResizable {
|
|
|
2873
3208
|
if (this.disabled()) {
|
|
2874
3209
|
return;
|
|
2875
3210
|
}
|
|
3211
|
+
// Only when the resizable element itself is focused: arrow keys pressed inside an input /
|
|
3212
|
+
// contenteditable that lives in the block are caret movement, not a resize request.
|
|
3213
|
+
if (e.target !== this.el.nativeElement) {
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
2876
3216
|
const rtl = isRtl(this.el.nativeElement);
|
|
2877
3217
|
const step = this.keyboardStep() * (e.shiftKey ? 4 : 1);
|
|
2878
3218
|
let dw = 0;
|
|
@@ -2896,8 +3236,9 @@ class NgxResizable {
|
|
|
2896
3236
|
const rect = this.el.nativeElement.getBoundingClientRect();
|
|
2897
3237
|
const width = this.clampGrid(Math.max(this.minWidth(), Math.min(this.maxWidth(), this.el.nativeElement.offsetWidth + dw)), 'x');
|
|
2898
3238
|
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.
|
|
3239
|
+
const styled = this.toStyleSize(width, height);
|
|
3240
|
+
this.el.nativeElement.style.width = `${styled.width}px`;
|
|
3241
|
+
this.el.nativeElement.style.height = `${styled.height}px`;
|
|
2901
3242
|
const out = {
|
|
2902
3243
|
width,
|
|
2903
3244
|
height,
|
|
@@ -2967,8 +3308,9 @@ class NgxResizable {
|
|
|
2967
3308
|
/**
|
|
2968
3309
|
* Boundary constraint.
|
|
2969
3310
|
*/
|
|
2970
|
-
|
|
2971
|
-
|
|
3311
|
+
const boundaryEl = this.boundary();
|
|
3312
|
+
if (boundaryEl) {
|
|
3313
|
+
const clamped = clampResizeWithinBoundary(boundaryEl.getBoundingClientRect(), this.start, width * scaleX, height * scaleY, moveLeft, moveTop);
|
|
2972
3314
|
width = clamped.width / scaleX;
|
|
2973
3315
|
height = clamped.height / scaleY;
|
|
2974
3316
|
moveLeft = clamped.left;
|
|
@@ -3001,8 +3343,9 @@ class NgxResizable {
|
|
|
3001
3343
|
* Changing width/height may cause the browser to re-layout
|
|
3002
3344
|
* the element, especially for relative/flex/grid/static layouts.
|
|
3003
3345
|
*/
|
|
3004
|
-
|
|
3005
|
-
element.style.
|
|
3346
|
+
const style = this.toStyleSize(out.width, out.height);
|
|
3347
|
+
element.style.width = `${style.width}px`;
|
|
3348
|
+
element.style.height = `${style.height}px`;
|
|
3006
3349
|
/**
|
|
3007
3350
|
* Force/read layout after changing the size.
|
|
3008
3351
|
*/
|
|
@@ -3037,9 +3380,29 @@ class NgxResizable {
|
|
|
3037
3380
|
element.style.transform =
|
|
3038
3381
|
`${baseTransform}translate3d(` + `${localCorrection.x}px, ` + `${localCorrection.y}px, 0)`;
|
|
3039
3382
|
}
|
|
3383
|
+
/**
|
|
3384
|
+
* `offsetWidth/Height` (what we measure and report) are BORDER-box sizes. The inline
|
|
3385
|
+
* `width/height` we write are interpreted according to `box-sizing`: for the CSS default
|
|
3386
|
+
* (`content-box`) writing the border-box number would make a padded/bordered block jump by
|
|
3387
|
+
* its padding+border the instant a resize starts. Convert so the box is exactly the size
|
|
3388
|
+
* reported in `IResizableOutput`.
|
|
3389
|
+
*/
|
|
3390
|
+
toStyleSize(width, height) {
|
|
3391
|
+
const el = this.el.nativeElement;
|
|
3392
|
+
const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
|
|
3393
|
+
if (!cs || cs.boxSizing === 'border-box')
|
|
3394
|
+
return { width, height };
|
|
3395
|
+
const px = (v) => Number.parseFloat(v) || 0;
|
|
3396
|
+
const extraX = px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
|
|
3397
|
+
const extraY = px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
|
|
3398
|
+
return { width: Math.max(0, width - extraX), height: Math.max(0, height - extraY) };
|
|
3399
|
+
}
|
|
3040
3400
|
clampGrid(value, axis) {
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3401
|
+
const g = this.grid();
|
|
3402
|
+
// NOTE: the previous code returned the input *signal function* for a numeric grid, so every
|
|
3403
|
+
// snapped size became NaN and the resize silently stopped working.
|
|
3404
|
+
const grid = typeof g === 'number' ? g : g?.[axis];
|
|
3405
|
+
if (!grid || !Number.isFinite(grid) || grid <= 0) {
|
|
3043
3406
|
return value;
|
|
3044
3407
|
}
|
|
3045
3408
|
return Math.round(value / grid) * grid;
|
|
@@ -3052,8 +3415,13 @@ class NgxResizable {
|
|
|
3052
3415
|
for (const remove of this.removeDocumentListeners)
|
|
3053
3416
|
remove();
|
|
3054
3417
|
this.removeDocumentListeners = [];
|
|
3055
|
-
|
|
3056
|
-
this.el.nativeElement.
|
|
3418
|
+
try {
|
|
3419
|
+
if (pointerId >= 0 && this.el.nativeElement.hasPointerCapture?.(pointerId)) {
|
|
3420
|
+
this.el.nativeElement.releasePointerCapture(pointerId);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
catch {
|
|
3424
|
+
/* already released */
|
|
3057
3425
|
}
|
|
3058
3426
|
this.beforeTransform = '';
|
|
3059
3427
|
}
|
|
@@ -3194,6 +3562,13 @@ class NgxDropList {
|
|
|
3194
3562
|
set disableSort(value) {
|
|
3195
3563
|
this._ref.disableSort = value === true;
|
|
3196
3564
|
}
|
|
3565
|
+
/** `'stretch'`: the placeholder only copies the item's size along the list's main axis. */
|
|
3566
|
+
set placeholderFit(value) {
|
|
3567
|
+
this._ref.placeholderFit = value === 'stretch' ? 'stretch' : 'item';
|
|
3568
|
+
}
|
|
3569
|
+
set sortAnimationDuration(value) {
|
|
3570
|
+
this._ref.sortAnimationDuration = value == null ? 200 : Math.max(0, value);
|
|
3571
|
+
}
|
|
3197
3572
|
drop = new EventEmitter();
|
|
3198
3573
|
customPlaceholder;
|
|
3199
3574
|
sub;
|
|
@@ -3225,7 +3600,7 @@ class NgxDropList {
|
|
|
3225
3600
|
this.service.removeDropList(this._ref);
|
|
3226
3601
|
}
|
|
3227
3602
|
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 });
|
|
3603
|
+
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
3604
|
}
|
|
3230
3605
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: NgxDropList, decorators: [{
|
|
3231
3606
|
type: Directive,
|
|
@@ -3241,6 +3616,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
|
|
|
3241
3616
|
type: Input
|
|
3242
3617
|
}], disableSort: [{
|
|
3243
3618
|
type: Input
|
|
3619
|
+
}], placeholderFit: [{
|
|
3620
|
+
type: Input
|
|
3621
|
+
}], sortAnimationDuration: [{
|
|
3622
|
+
type: Input
|
|
3244
3623
|
}], drop: [{
|
|
3245
3624
|
type: Output
|
|
3246
3625
|
}], customPlaceholder: [{
|
|
@@ -3313,9 +3692,54 @@ function clamp(v, max) {
|
|
|
3313
3692
|
return Math.max(0, Math.min(max, v));
|
|
3314
3693
|
}
|
|
3315
3694
|
|
|
3695
|
+
/**
|
|
3696
|
+
* Helpers for a page builder's "export HTML" step.
|
|
3697
|
+
*
|
|
3698
|
+
* While you edit, the directives decorate the canvas DOM: `ngx-*` classes, `tabindex="0"`,
|
|
3699
|
+
* `aria-grabbed`, `touch-action: none`, and — if an export happens mid-drag — a placeholder or a
|
|
3700
|
+
* preview clone. None of that belongs in the page a visitor receives. Run the canvas through
|
|
3701
|
+
* `getCleanHtml()` (or `stripEditorArtifacts()` on a clone you already have) before saving.
|
|
3702
|
+
*/
|
|
3703
|
+
const TRANSIENT_NODES = '.ngx-drag-placeholder, .ngx-drag-preview';
|
|
3704
|
+
/** Mutates `root` (and its subtree) in place. Pass a CLONE of live DOM, never the live canvas. */
|
|
3705
|
+
function stripEditorArtifacts(root, options = {}) {
|
|
3706
|
+
const prefix = options.classPrefix ?? 'ngx-';
|
|
3707
|
+
const dropClasses = new Set(['disable-drag', ...(options.extraClasses ?? [])]);
|
|
3708
|
+
const dropAttrs = ['aria-grabbed', ...(options.extraAttributes ?? [])];
|
|
3709
|
+
root.querySelectorAll(TRANSIENT_NODES).forEach((n) => n.remove());
|
|
3710
|
+
const all = [root, ...Array.from(root.querySelectorAll('*'))];
|
|
3711
|
+
for (const el of all) {
|
|
3712
|
+
// `tabindex="0"` is only ours on draggables (a real focusable of the page keeps its own).
|
|
3713
|
+
if (el.classList.contains('ngx-draggable') && el.getAttribute('tabindex') === '0') {
|
|
3714
|
+
el.removeAttribute('tabindex');
|
|
3715
|
+
}
|
|
3716
|
+
for (const attr of dropAttrs)
|
|
3717
|
+
el.removeAttribute(attr);
|
|
3718
|
+
for (const cls of Array.from(el.classList)) {
|
|
3719
|
+
if (cls.startsWith(prefix) || dropClasses.has(cls))
|
|
3720
|
+
el.classList.remove(cls);
|
|
3721
|
+
}
|
|
3722
|
+
if (!el.classList.length)
|
|
3723
|
+
el.removeAttribute('class');
|
|
3724
|
+
const style = el.style;
|
|
3725
|
+
if (style) {
|
|
3726
|
+
if (style.touchAction === 'none')
|
|
3727
|
+
style.removeProperty('touch-action');
|
|
3728
|
+
if (!el.getAttribute('style')?.trim())
|
|
3729
|
+
el.removeAttribute('style');
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
/** Serialised, editor-free HTML of `el`'s children (the canvas content). */
|
|
3734
|
+
function getCleanHtml(el, options) {
|
|
3735
|
+
const clone = el.cloneNode(true);
|
|
3736
|
+
stripEditorArtifacts(clone, options);
|
|
3737
|
+
return clone.innerHTML;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3316
3740
|
/**
|
|
3317
3741
|
* Generated bundle index. Do not edit.
|
|
3318
3742
|
*/
|
|
3319
3743
|
|
|
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 };
|
|
3744
|
+
export { AutoScroller, DragDropService, DragRef, DropListGroupRef, DropListRef, NGX_DRAGGABLE, NGX_DRAG_PREVIEW, NGX_DROPLIST, NGX_DROPLIST_GROUP, NGX_PLACEHOLDER, NgxDragPreview, NgxDraggable, NgxDropList, NgxDropListGroup, NgxPlaceholder, NgxResizable, PlaceHolderRef, adjustForZoom, checkBoundX, checkBoundY, clampResizeWithinBoundary, copyArrayItem, findScrollableAncestor, getCleanHtml, getPageZoom, getScrollPosition, isRtl, moveItemInArray, resolveLogicalDirection, stripEditorArtifacts, transferArrayItem };
|
|
3321
3745
|
//# sourceMappingURL=ngx-kit-drag-resize.mjs.map
|