ngx-virtual-dnd 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -824,13 +824,10 @@ class ElementCloneService {
|
|
|
824
824
|
placeholder.style.cssText = `
|
|
825
825
|
width: 100%;
|
|
826
826
|
height: 100%;
|
|
827
|
-
background: #333;
|
|
828
827
|
display: flex;
|
|
829
828
|
align-items: center;
|
|
830
829
|
justify-content: center;
|
|
831
|
-
color: #666;
|
|
832
830
|
`;
|
|
833
|
-
placeholder.textContent = 'Video';
|
|
834
831
|
video.replaceWith(placeholder);
|
|
835
832
|
}
|
|
836
833
|
});
|
|
@@ -841,13 +838,9 @@ class ElementCloneService {
|
|
|
841
838
|
const iframeStyles = window.getComputedStyle(iframe);
|
|
842
839
|
placeholder.style.width = iframeStyles.width;
|
|
843
840
|
placeholder.style.height = iframeStyles.height;
|
|
844
|
-
placeholder.style.background = '#f0f0f0';
|
|
845
|
-
placeholder.style.border = '1px solid #ddd';
|
|
846
841
|
placeholder.style.display = 'flex';
|
|
847
842
|
placeholder.style.alignItems = 'center';
|
|
848
843
|
placeholder.style.justifyContent = 'center';
|
|
849
|
-
placeholder.style.color = '#999';
|
|
850
|
-
placeholder.textContent = 'Embedded content';
|
|
851
844
|
iframe.replaceWith(placeholder);
|
|
852
845
|
});
|
|
853
846
|
}
|
|
@@ -1020,6 +1013,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1020
1013
|
}]
|
|
1021
1014
|
}] });
|
|
1022
1015
|
|
|
1016
|
+
/**
|
|
1017
|
+
* Service that manages a shared overlay container appended to `document.body`.
|
|
1018
|
+
*
|
|
1019
|
+
* Elements placed inside the overlay container escape any ancestor CSS `transform`,
|
|
1020
|
+
* `perspective`, or `filter` that would create a new containing block for
|
|
1021
|
+
* `position: fixed` children. This ensures viewport-relative positioning works
|
|
1022
|
+
* correctly regardless of where the consuming component sits in the DOM tree.
|
|
1023
|
+
*
|
|
1024
|
+
* Mirrors the strategy used by Angular CDK's `OverlayContainer`.
|
|
1025
|
+
*/
|
|
1026
|
+
class OverlayContainerService {
|
|
1027
|
+
#containerElement = null;
|
|
1028
|
+
/**
|
|
1029
|
+
* Returns the shared overlay container element, lazily creating it on first access.
|
|
1030
|
+
* Returns `null` in non-browser environments (SSR).
|
|
1031
|
+
*/
|
|
1032
|
+
getContainerElement() {
|
|
1033
|
+
if (typeof document === 'undefined') {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
if (!this.#containerElement) {
|
|
1037
|
+
this.#containerElement = document.createElement('div');
|
|
1038
|
+
this.#containerElement.classList.add('vdnd-overlay-container');
|
|
1039
|
+
document.body.appendChild(this.#containerElement);
|
|
1040
|
+
}
|
|
1041
|
+
return this.#containerElement;
|
|
1042
|
+
}
|
|
1043
|
+
ngOnDestroy() {
|
|
1044
|
+
this.#containerElement?.remove();
|
|
1045
|
+
this.#containerElement = null;
|
|
1046
|
+
}
|
|
1047
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: OverlayContainerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1048
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: OverlayContainerService, providedIn: 'root' });
|
|
1049
|
+
}
|
|
1050
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: OverlayContainerService, decorators: [{
|
|
1051
|
+
type: Injectable,
|
|
1052
|
+
args: [{
|
|
1053
|
+
providedIn: 'root',
|
|
1054
|
+
}]
|
|
1055
|
+
}] });
|
|
1056
|
+
|
|
1023
1057
|
/**
|
|
1024
1058
|
* Renders an empty placeholder that takes up space in the document flow.
|
|
1025
1059
|
*
|
|
@@ -1605,8 +1639,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1605
1639
|
/**
|
|
1606
1640
|
* Renders a preview of the dragged item that follows the cursor.
|
|
1607
1641
|
*
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1642
|
+
* The component automatically teleports itself into a body-level overlay container,
|
|
1643
|
+
* so it works correctly even inside ancestors with CSS `transform` (e.g. Ionic pages).
|
|
1644
|
+
* It can be placed anywhere in the component tree.
|
|
1610
1645
|
*
|
|
1611
1646
|
* @example
|
|
1612
1647
|
* ```html
|
|
@@ -1619,6 +1654,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1619
1654
|
*/
|
|
1620
1655
|
class DragPreviewComponent {
|
|
1621
1656
|
dragState = inject(DragStateService);
|
|
1657
|
+
#overlayContainer = inject(OverlayContainerService);
|
|
1658
|
+
#elementRef = inject((ElementRef));
|
|
1622
1659
|
/** Optional custom template for the preview */
|
|
1623
1660
|
previewTemplate = input(...(ngDevMode ? [undefined, { debugName: "previewTemplate" }] : []));
|
|
1624
1661
|
/** Offset from cursor to preview (to avoid cursor being on top of preview) */
|
|
@@ -1633,6 +1670,14 @@ class DragPreviewComponent {
|
|
|
1633
1670
|
return this.dragState.draggedItem()?.clonedElement ?? null;
|
|
1634
1671
|
}, ...(ngDevMode ? [{ debugName: "clonedElement" }] : []));
|
|
1635
1672
|
constructor() {
|
|
1673
|
+
// Teleport host element into the body-level overlay container after first render.
|
|
1674
|
+
// This escapes any ancestor CSS transforms that would break position: fixed.
|
|
1675
|
+
afterNextRender(() => {
|
|
1676
|
+
const container = this.#overlayContainer.getContainerElement();
|
|
1677
|
+
if (container) {
|
|
1678
|
+
container.appendChild(this.#elementRef.nativeElement);
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1636
1681
|
// Effect to insert the cloned element into the container
|
|
1637
1682
|
effect(() => {
|
|
1638
1683
|
const container = this.cloneContainer()?.nativeElement;
|
|
@@ -1648,6 +1693,9 @@ class DragPreviewComponent {
|
|
|
1648
1693
|
}
|
|
1649
1694
|
});
|
|
1650
1695
|
}
|
|
1696
|
+
ngOnDestroy() {
|
|
1697
|
+
this.#elementRef.nativeElement.remove();
|
|
1698
|
+
}
|
|
1651
1699
|
/** Whether the preview is visible */
|
|
1652
1700
|
isVisible = computed(() => {
|
|
1653
1701
|
return this.dragState.isDragging() && this.dragState.cursorPosition() !== null;
|
|
@@ -1709,6 +1757,7 @@ class DragPreviewComponent {
|
|
|
1709
1757
|
@if (isVisible()) {
|
|
1710
1758
|
<div
|
|
1711
1759
|
class="vdnd-drag-preview"
|
|
1760
|
+
data-testid="vdnd-drag-preview"
|
|
1712
1761
|
[style.position]="'fixed'"
|
|
1713
1762
|
[style.left.px]="0"
|
|
1714
1763
|
[style.top.px]="0"
|
|
@@ -1718,7 +1767,6 @@ class DragPreviewComponent {
|
|
|
1718
1767
|
[style.height.px]="dimensions().height"
|
|
1719
1768
|
[style.pointer-events]="'none'"
|
|
1720
1769
|
[style.z-index]="1000"
|
|
1721
|
-
[style.opacity]="0.9"
|
|
1722
1770
|
>
|
|
1723
1771
|
@if (previewTemplate()) {
|
|
1724
1772
|
<ng-container *ngTemplateOutlet="previewTemplate()!; context: templateContext()">
|
|
@@ -1732,7 +1780,7 @@ class DragPreviewComponent {
|
|
|
1732
1780
|
}
|
|
1733
1781
|
</div>
|
|
1734
1782
|
}
|
|
1735
|
-
`, isInline: true, styles: [".vdnd-drag-preview{box-sizing:border-box}.vdnd-drag-preview-clone{width:100%;height:100%;
|
|
1783
|
+
`, isInline: true, styles: [".vdnd-drag-preview{box-sizing:border-box}.vdnd-drag-preview-clone{width:100%;height:100%;overflow:hidden}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1736
1784
|
}
|
|
1737
1785
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DragPreviewComponent, decorators: [{
|
|
1738
1786
|
type: Component,
|
|
@@ -1740,6 +1788,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1740
1788
|
@if (isVisible()) {
|
|
1741
1789
|
<div
|
|
1742
1790
|
class="vdnd-drag-preview"
|
|
1791
|
+
data-testid="vdnd-drag-preview"
|
|
1743
1792
|
[style.position]="'fixed'"
|
|
1744
1793
|
[style.left.px]="0"
|
|
1745
1794
|
[style.top.px]="0"
|
|
@@ -1749,7 +1798,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1749
1798
|
[style.height.px]="dimensions().height"
|
|
1750
1799
|
[style.pointer-events]="'none'"
|
|
1751
1800
|
[style.z-index]="1000"
|
|
1752
|
-
[style.opacity]="0.9"
|
|
1753
1801
|
>
|
|
1754
1802
|
@if (previewTemplate()) {
|
|
1755
1803
|
<ng-container *ngTemplateOutlet="previewTemplate()!; context: templateContext()">
|
|
@@ -1763,7 +1811,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1763
1811
|
}
|
|
1764
1812
|
</div>
|
|
1765
1813
|
}
|
|
1766
|
-
`, styles: [".vdnd-drag-preview{box-sizing:border-box}.vdnd-drag-preview-clone{width:100%;height:100%;
|
|
1814
|
+
`, styles: [".vdnd-drag-preview{box-sizing:border-box}.vdnd-drag-preview-clone{width:100%;height:100%;overflow:hidden}\n"] }]
|
|
1767
1815
|
}], ctorParameters: () => [], propDecorators: { previewTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewTemplate", required: false }] }], cursorOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "cursorOffset", required: false }] }], cloneContainer: [{ type: i0.ViewChild, args: ['cloneContainer', { isSignal: true }] }] } });
|
|
1768
1816
|
|
|
1769
1817
|
/**
|
|
@@ -1796,7 +1844,8 @@ class PlaceholderComponent {
|
|
|
1796
1844
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: PlaceholderComponent, isStandalone: true, selector: "vdnd-placeholder", inputs: { height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, template: { classPropertyName: "template", publicName: "template", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.height.px": "height()", "attr.data-draggable-id": "\"placeholder\"" }, classAttribute: "vdnd-placeholder" }, ngImport: i0, template: `
|
|
1797
1845
|
@if (template()) {
|
|
1798
1846
|
<ng-container
|
|
1799
|
-
*ngTemplateOutlet="template()!; context: { $implicit: height(), height: height() }"
|
|
1847
|
+
*ngTemplateOutlet="template()!; context: { $implicit: height(), height: height() }"
|
|
1848
|
+
>
|
|
1800
1849
|
</ng-container>
|
|
1801
1850
|
}
|
|
1802
1851
|
`, isInline: true, styles: [":host{display:block;box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
@@ -1810,7 +1859,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1810
1859
|
}, template: `
|
|
1811
1860
|
@if (template()) {
|
|
1812
1861
|
<ng-container
|
|
1813
|
-
*ngTemplateOutlet="template()!; context: { $implicit: height(), height: height() }"
|
|
1862
|
+
*ngTemplateOutlet="template()!; context: { $implicit: height(), height: height() }"
|
|
1863
|
+
>
|
|
1814
1864
|
</ng-container>
|
|
1815
1865
|
}
|
|
1816
1866
|
`, styles: [":host{display:block;box-sizing:border-box}\n"] }]
|
|
@@ -1867,6 +1917,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1867
1917
|
}]
|
|
1868
1918
|
}], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "vdndGroup", required: true }] }] } });
|
|
1869
1919
|
|
|
1920
|
+
/**
|
|
1921
|
+
* Creates a computed signal that resolves the effective group name for a draggable or droppable.
|
|
1922
|
+
*
|
|
1923
|
+
* Resolution order:
|
|
1924
|
+
* 1. Explicit group input (vdndDraggableGroup or vdndDroppableGroup)
|
|
1925
|
+
* 2. Inherited group from parent vdndGroup directive
|
|
1926
|
+
* 3. null (with dev-mode warning)
|
|
1927
|
+
*
|
|
1928
|
+
* @example
|
|
1929
|
+
* ```typescript
|
|
1930
|
+
* readonly #effectiveGroup = createEffectiveGroupSignal({
|
|
1931
|
+
* explicitGroup: this.vdndDraggableGroup,
|
|
1932
|
+
* parentGroup: this.#parentGroup,
|
|
1933
|
+
* elementId: this.vdndDraggable,
|
|
1934
|
+
* elementType: 'draggable',
|
|
1935
|
+
* });
|
|
1936
|
+
* ```
|
|
1937
|
+
*/
|
|
1938
|
+
function createEffectiveGroupSignal(options) {
|
|
1939
|
+
const { explicitGroup, parentGroup, elementId, elementType } = options;
|
|
1940
|
+
// State object to track whether we've warned (mutable closure state)
|
|
1941
|
+
const state = { hasWarnedMissingGroup: false };
|
|
1942
|
+
return computed(() => {
|
|
1943
|
+
const explicit = explicitGroup();
|
|
1944
|
+
if (explicit)
|
|
1945
|
+
return explicit;
|
|
1946
|
+
const inherited = parentGroup?.group();
|
|
1947
|
+
if (inherited)
|
|
1948
|
+
return inherited;
|
|
1949
|
+
if (isDevMode() && !state.hasWarnedMissingGroup) {
|
|
1950
|
+
const directive = elementType === 'draggable' ? 'vdndDraggable' : 'vdndDroppable';
|
|
1951
|
+
const groupInput = elementType === 'draggable' ? 'vdndDraggableGroup' : 'vdndDroppableGroup';
|
|
1952
|
+
const action = elementType === 'draggable' ? 'Drag' : 'Dropping';
|
|
1953
|
+
console.warn(`[ngx-virtual-dnd] [${directive}="${elementId()}"] requires a group. ` +
|
|
1954
|
+
`Either set ${groupInput} or wrap in a vdndGroup directive. ` +
|
|
1955
|
+
`${action} will be disabled for this element.`);
|
|
1956
|
+
state.hasWarnedMissingGroup = true;
|
|
1957
|
+
}
|
|
1958
|
+
return null;
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1870
1962
|
/**
|
|
1871
1963
|
* Marks an element as a valid drop target within the virtual scroll drag-and-drop system.
|
|
1872
1964
|
*
|
|
@@ -1904,22 +1996,12 @@ class DroppableDirective {
|
|
|
1904
1996
|
* Resolved group name - uses explicit input or falls back to parent group.
|
|
1905
1997
|
* Returns null (and disables dropping) if neither is available.
|
|
1906
1998
|
*/
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
if (inherited)
|
|
1914
|
-
return inherited;
|
|
1915
|
-
if (isDevMode() && !this.#hasWarnedMissingGroup) {
|
|
1916
|
-
console.warn(`[ngx-virtual-dnd] [vdndDroppable="${this.vdndDroppable()}"] requires a group. ` +
|
|
1917
|
-
'Either set vdndDroppableGroup or wrap in a vdndGroup directive. ' +
|
|
1918
|
-
'Dropping will be disabled for this element.');
|
|
1919
|
-
this.#hasWarnedMissingGroup = true;
|
|
1920
|
-
}
|
|
1921
|
-
return null;
|
|
1922
|
-
}, ...(ngDevMode ? [{ debugName: "effectiveGroup" }] : []));
|
|
1999
|
+
effectiveGroup = createEffectiveGroupSignal({
|
|
2000
|
+
explicitGroup: this.vdndDroppableGroup,
|
|
2001
|
+
parentGroup: this.#parentGroup,
|
|
2002
|
+
elementId: this.vdndDroppable,
|
|
2003
|
+
elementType: 'droppable',
|
|
2004
|
+
});
|
|
1923
2005
|
/** Optional data associated with this droppable */
|
|
1924
2006
|
vdndDroppableData = input(...(ngDevMode ? [undefined, { debugName: "vdndDroppableData" }] : []));
|
|
1925
2007
|
/** Whether this droppable is disabled */
|
|
@@ -2945,22 +3027,12 @@ class DraggableDirective {
|
|
|
2945
3027
|
* Resolved group name - uses explicit input or falls back to parent group.
|
|
2946
3028
|
* Returns null (and disables drag) if neither is available.
|
|
2947
3029
|
*/
|
|
2948
|
-
#
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
if (inherited)
|
|
2955
|
-
return inherited;
|
|
2956
|
-
if (isDevMode() && !this.#hasWarnedMissingGroup) {
|
|
2957
|
-
console.warn(`[ngx-virtual-dnd] [vdndDraggable="${this.vdndDraggable()}"] requires a group. ` +
|
|
2958
|
-
'Either set vdndDraggableGroup or wrap in a vdndGroup directive. ' +
|
|
2959
|
-
'Drag will be disabled for this element.');
|
|
2960
|
-
this.#hasWarnedMissingGroup = true;
|
|
2961
|
-
}
|
|
2962
|
-
return null;
|
|
2963
|
-
}, ...(ngDevMode ? [{ debugName: "#effectiveGroup" }] : []));
|
|
3030
|
+
#effectiveGroup = createEffectiveGroupSignal({
|
|
3031
|
+
explicitGroup: this.vdndDraggableGroup,
|
|
3032
|
+
parentGroup: this.#parentGroup,
|
|
3033
|
+
elementId: this.vdndDraggable,
|
|
3034
|
+
elementType: 'draggable',
|
|
3035
|
+
});
|
|
2964
3036
|
/** Optional data associated with this draggable */
|
|
2965
3037
|
vdndDraggableData = input(...(ngDevMode ? [undefined, { debugName: "vdndDraggableData" }] : []));
|
|
2966
3038
|
/** Whether this draggable is disabled */
|
|
@@ -4067,7 +4139,6 @@ class VirtualForDirective {
|
|
|
4067
4139
|
#updateViews() {
|
|
4068
4140
|
const items = this.vdndVirtualForOf();
|
|
4069
4141
|
const { start, end } = this.#renderRange();
|
|
4070
|
-
const trackByFn = this.vdndVirtualForTrackBy();
|
|
4071
4142
|
const itemHeight = this.vdndVirtualForItemHeight();
|
|
4072
4143
|
const placeholderIndex = this.#placeholderIndex();
|
|
4073
4144
|
const showPlaceholder = this.#shouldShowPlaceholder();
|
|
@@ -4079,18 +4150,45 @@ class VirtualForDirective {
|
|
|
4079
4150
|
draggedIndex >= 0 &&
|
|
4080
4151
|
isSourceList &&
|
|
4081
4152
|
draggedIndex < items.length;
|
|
4082
|
-
// Notify viewport of render start index for wrapper positioning
|
|
4083
|
-
|
|
4084
|
-
//
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4153
|
+
// Notify viewport of render start index for wrapper positioning
|
|
4154
|
+
this.#notifyViewportRenderStart(start, isSourceList, draggedIndex);
|
|
4155
|
+
// 1. Build the list of items to render
|
|
4156
|
+
const itemsToRender = this.#calculateItemsToRender({
|
|
4157
|
+
items,
|
|
4158
|
+
start,
|
|
4159
|
+
end,
|
|
4160
|
+
showPlaceholder,
|
|
4161
|
+
placeholderIndex,
|
|
4162
|
+
shouldKeepDragged,
|
|
4163
|
+
draggedIndex,
|
|
4164
|
+
});
|
|
4165
|
+
// 2. Reconcile views with the DOM
|
|
4166
|
+
const placeholderDomPosition = this.#reconcileViews(itemsToRender, showPlaceholder, itemHeight);
|
|
4167
|
+
// 3. Position placeholder in DOM
|
|
4168
|
+
this.#positionPlaceholder(showPlaceholder, placeholderDomPosition, itemHeight);
|
|
4169
|
+
// 4. Trim view pool to prevent memory bloat
|
|
4170
|
+
this.#trimViewPool();
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* Notify viewport of render start index for wrapper positioning.
|
|
4174
|
+
* Adjusts when the dragged item is above the rendered range.
|
|
4175
|
+
*/
|
|
4176
|
+
#notifyViewportRenderStart(start, isSourceList, draggedIndex) {
|
|
4177
|
+
if (!this.#useViewportPositioning)
|
|
4178
|
+
return;
|
|
4179
|
+
const shouldAdjustRenderStart = this.#dragState.isDragging() && isSourceList && draggedIndex >= 0 && draggedIndex < start;
|
|
4090
4180
|
const renderStartIndex = shouldAdjustRenderStart ? Math.max(0, start - 1) : start;
|
|
4091
4181
|
this.#viewport?.setRenderStartIndex(renderStartIndex);
|
|
4092
|
-
|
|
4182
|
+
}
|
|
4183
|
+
/**
|
|
4184
|
+
* Calculate the list of items to render, including placeholder positioning
|
|
4185
|
+
* and keeping the dragged item alive when scrolled out of range.
|
|
4186
|
+
*/
|
|
4187
|
+
#calculateItemsToRender(params) {
|
|
4188
|
+
const { items, start, end, showPlaceholder, placeholderIndex, shouldKeepDragged, draggedIndex, } = params;
|
|
4189
|
+
const trackByFn = this.vdndVirtualForTrackBy();
|
|
4093
4190
|
const itemsToRender = [];
|
|
4191
|
+
// Build render list for visible range
|
|
4094
4192
|
for (let i = start; i <= end && i < items.length; i++) {
|
|
4095
4193
|
// Insert placeholder before item at placeholderIndex
|
|
4096
4194
|
if (showPlaceholder &&
|
|
@@ -4117,7 +4215,7 @@ class VirtualForDirective {
|
|
|
4117
4215
|
visualIndex: i,
|
|
4118
4216
|
});
|
|
4119
4217
|
}
|
|
4120
|
-
//
|
|
4218
|
+
// Add placeholder at end if needed
|
|
4121
4219
|
if (showPlaceholder &&
|
|
4122
4220
|
placeholderIndex >= items.length &&
|
|
4123
4221
|
!itemsToRender.some((r) => r.type === 'placeholder')) {
|
|
@@ -4128,8 +4226,7 @@ class VirtualForDirective {
|
|
|
4128
4226
|
visualIndex: placeholderIndex,
|
|
4129
4227
|
});
|
|
4130
4228
|
}
|
|
4131
|
-
// Keep
|
|
4132
|
-
// This prevents the draggable directive from being recycled during long autoscrolls.
|
|
4229
|
+
// Keep dragged item view alive when scrolled out of range
|
|
4133
4230
|
if (shouldKeepDragged && (draggedIndex < start || draggedIndex > end)) {
|
|
4134
4231
|
const draggedItem = items[draggedIndex];
|
|
4135
4232
|
const draggedKey = trackByFn(draggedIndex, draggedItem);
|
|
@@ -4149,8 +4246,17 @@ class VirtualForDirective {
|
|
|
4149
4246
|
});
|
|
4150
4247
|
}
|
|
4151
4248
|
}
|
|
4249
|
+
return itemsToRender;
|
|
4250
|
+
}
|
|
4251
|
+
/**
|
|
4252
|
+
* Reconcile views with the calculated items to render.
|
|
4253
|
+
* Moves unused views to pool, updates existing views, creates new views as needed.
|
|
4254
|
+
* Returns the DOM position where placeholder should be inserted.
|
|
4255
|
+
*/
|
|
4256
|
+
#reconcileViews(itemsToRender, showPlaceholder, itemHeight) {
|
|
4257
|
+
// Determine which keys we need
|
|
4152
4258
|
const neededKeys = new Set(itemsToRender.filter((r) => r.type === 'item').map((item) => item.key));
|
|
4153
|
-
//
|
|
4259
|
+
// Move unused views to pool
|
|
4154
4260
|
for (const [key, view] of this.#activeViews) {
|
|
4155
4261
|
if (!neededKeys.has(key)) {
|
|
4156
4262
|
const index = this.#viewContainer.indexOf(view);
|
|
@@ -4166,36 +4272,16 @@ class VirtualForDirective {
|
|
|
4166
4272
|
this.#placeholder.remove();
|
|
4167
4273
|
this.#placeholderInDom = false;
|
|
4168
4274
|
}
|
|
4169
|
-
//
|
|
4275
|
+
// Process items and track placeholder position
|
|
4170
4276
|
let viewContainerIndex = 0;
|
|
4171
|
-
let placeholderDomPosition = -1;
|
|
4277
|
+
let placeholderDomPosition = -1;
|
|
4172
4278
|
for (const entry of itemsToRender) {
|
|
4173
4279
|
if (entry.type === 'placeholder') {
|
|
4174
|
-
// Remember the DOM position for placeholder (based on how many items rendered before it)
|
|
4175
4280
|
placeholderDomPosition = viewContainerIndex;
|
|
4176
4281
|
continue;
|
|
4177
4282
|
}
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
let view = this.#activeViews.get(key);
|
|
4181
|
-
if (view) {
|
|
4182
|
-
// View exists - update context in place (no DOM manipulation needed)
|
|
4183
|
-
Object.assign(view.context, context);
|
|
4184
|
-
view.markForCheck();
|
|
4185
|
-
}
|
|
4186
|
-
else {
|
|
4187
|
-
// Need a new view - try pool first, then create
|
|
4188
|
-
view = this.#viewPool.pop();
|
|
4189
|
-
if (view) {
|
|
4190
|
-
Object.assign(view.context, context);
|
|
4191
|
-
view.markForCheck();
|
|
4192
|
-
}
|
|
4193
|
-
else {
|
|
4194
|
-
view = this.#templateRef.createEmbeddedView(context);
|
|
4195
|
-
}
|
|
4196
|
-
this.#activeViews.set(key, view);
|
|
4197
|
-
}
|
|
4198
|
-
// Ensure view is in ViewContainerRef at correct position
|
|
4283
|
+
const view = this.#getOrCreateView(entry.key, entry.context);
|
|
4284
|
+
// Ensure view is at correct position in ViewContainerRef
|
|
4199
4285
|
const currentIndex = this.#viewContainer.indexOf(view);
|
|
4200
4286
|
if (currentIndex !== viewContainerIndex) {
|
|
4201
4287
|
if (currentIndex >= 0) {
|
|
@@ -4205,60 +4291,99 @@ class VirtualForDirective {
|
|
|
4205
4291
|
this.#viewContainer.insert(view, viewContainerIndex);
|
|
4206
4292
|
}
|
|
4207
4293
|
}
|
|
4208
|
-
// Apply absolute positioning
|
|
4209
|
-
// (viewport provides wrapper-based transform positioning)
|
|
4294
|
+
// Apply absolute positioning when not using viewport wrapper
|
|
4210
4295
|
if (!this.#useViewportPositioning) {
|
|
4211
|
-
|
|
4212
|
-
for (const node of view.rootNodes) {
|
|
4213
|
-
if (node instanceof HTMLElement) {
|
|
4214
|
-
node.style.position = 'absolute';
|
|
4215
|
-
node.style.top = `${topOffset}px`;
|
|
4216
|
-
node.style.left = '0';
|
|
4217
|
-
node.style.right = '0';
|
|
4218
|
-
}
|
|
4219
|
-
}
|
|
4296
|
+
this.#applyAbsolutePositioning(view, entry.visualIndex * itemHeight);
|
|
4220
4297
|
}
|
|
4221
4298
|
viewContainerIndex++;
|
|
4222
4299
|
}
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4300
|
+
return placeholderDomPosition;
|
|
4301
|
+
}
|
|
4302
|
+
/**
|
|
4303
|
+
* Get an existing view or create/recycle one from the pool.
|
|
4304
|
+
*/
|
|
4305
|
+
#getOrCreateView(key, context) {
|
|
4306
|
+
let view = this.#activeViews.get(key);
|
|
4307
|
+
if (view) {
|
|
4308
|
+
// Update existing view context
|
|
4309
|
+
Object.assign(view.context, context);
|
|
4310
|
+
view.markForCheck();
|
|
4311
|
+
}
|
|
4312
|
+
else {
|
|
4313
|
+
// Try pool first, then create new
|
|
4314
|
+
view = this.#viewPool.pop();
|
|
4315
|
+
if (view) {
|
|
4316
|
+
Object.assign(view.context, context);
|
|
4317
|
+
view.markForCheck();
|
|
4318
|
+
}
|
|
4319
|
+
else {
|
|
4320
|
+
view = this.#templateRef.createEmbeddedView(context);
|
|
4321
|
+
}
|
|
4322
|
+
this.#activeViews.set(key, view);
|
|
4323
|
+
}
|
|
4324
|
+
return view;
|
|
4325
|
+
}
|
|
4326
|
+
/**
|
|
4327
|
+
* Apply absolute positioning styles to a view's root nodes.
|
|
4328
|
+
*/
|
|
4329
|
+
#applyAbsolutePositioning(view, topOffset) {
|
|
4330
|
+
for (const node of view.rootNodes) {
|
|
4331
|
+
if (node instanceof HTMLElement) {
|
|
4332
|
+
node.style.position = 'absolute';
|
|
4333
|
+
node.style.top = `${topOffset}px`;
|
|
4334
|
+
node.style.left = '0';
|
|
4335
|
+
node.style.right = '0';
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
/**
|
|
4340
|
+
* Position the placeholder element in the DOM at the correct index.
|
|
4341
|
+
*/
|
|
4342
|
+
#positionPlaceholder(showPlaceholder, placeholderDomPosition, itemHeight) {
|
|
4343
|
+
if (!showPlaceholder || !this.#placeholder || placeholderDomPosition < 0) {
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4346
|
+
this.#placeholder.style.height = `${itemHeight}px`;
|
|
4347
|
+
const container = this.#viewContainer.element.nativeElement.parentElement;
|
|
4348
|
+
if (!container)
|
|
4349
|
+
return;
|
|
4350
|
+
// Find children excluding spacers and placeholder itself
|
|
4351
|
+
const children = Array.from(container.children).filter((el) => {
|
|
4352
|
+
const element = el;
|
|
4353
|
+
return (!element.classList.contains('vdnd-drag-placeholder') &&
|
|
4354
|
+
!element.classList.contains('vdnd-virtual-for-spacer') &&
|
|
4355
|
+
!element.classList.contains('vdnd-content-spacer'));
|
|
4356
|
+
});
|
|
4357
|
+
const insertBeforeEl = children[placeholderDomPosition] ?? null;
|
|
4358
|
+
if (!this.#placeholderInDom) {
|
|
4359
|
+
// First insertion
|
|
4360
|
+
if (insertBeforeEl) {
|
|
4361
|
+
container.insertBefore(this.#placeholder, insertBeforeEl);
|
|
4362
|
+
}
|
|
4363
|
+
else {
|
|
4364
|
+
container.appendChild(this.#placeholder);
|
|
4365
|
+
}
|
|
4366
|
+
this.#placeholderInDom = true;
|
|
4367
|
+
}
|
|
4368
|
+
else {
|
|
4369
|
+
// Move to correct position if needed
|
|
4370
|
+
const currentNextSibling = this.#placeholder.nextElementSibling;
|
|
4371
|
+
if (insertBeforeEl !== currentNextSibling) {
|
|
4372
|
+
if (insertBeforeEl) {
|
|
4373
|
+
container.insertBefore(this.#placeholder, insertBeforeEl);
|
|
4245
4374
|
}
|
|
4246
4375
|
else {
|
|
4247
|
-
|
|
4248
|
-
const currentNextSibling = this.#placeholder.nextElementSibling;
|
|
4249
|
-
if (insertBeforeEl !== currentNextSibling) {
|
|
4250
|
-
if (insertBeforeEl) {
|
|
4251
|
-
container.insertBefore(this.#placeholder, insertBeforeEl);
|
|
4252
|
-
}
|
|
4253
|
-
else {
|
|
4254
|
-
container.appendChild(this.#placeholder);
|
|
4255
|
-
}
|
|
4256
|
-
}
|
|
4376
|
+
container.appendChild(this.#placeholder);
|
|
4257
4377
|
}
|
|
4258
4378
|
}
|
|
4259
4379
|
}
|
|
4260
|
-
|
|
4261
|
-
|
|
4380
|
+
}
|
|
4381
|
+
/**
|
|
4382
|
+
* Trim the view pool to prevent memory bloat, keeping a reasonable buffer.
|
|
4383
|
+
*/
|
|
4384
|
+
#trimViewPool() {
|
|
4385
|
+
const maxPoolSize = 10;
|
|
4386
|
+
while (this.#viewPool.length > maxPoolSize) {
|
|
4262
4387
|
const view = this.#viewPool.pop();
|
|
4263
4388
|
view?.destroy();
|
|
4264
4389
|
}
|
|
@@ -4471,5 +4596,5 @@ function removeAt(list, index) {
|
|
|
4471
4596
|
* Generated bundle index. Do not edit.
|
|
4472
4597
|
*/
|
|
4473
4598
|
|
|
4474
|
-
export { AutoScrollService, DragPlaceholderComponent, DragPreviewComponent, DragStateService, DraggableDirective, DroppableDirective, DroppableGroupDirective, END_OF_LIST, ElementCloneService, INITIAL_DRAG_STATE, KeyboardDragService, PlaceholderComponent, PositionCalculatorService, ScrollableDirective, VDND_GROUP_TOKEN, VDND_SCROLL_CONTAINER, VDND_VIRTUAL_VIEWPORT, VirtualContentComponent, VirtualForDirective, VirtualScrollContainerComponent, VirtualSortableListComponent, VirtualViewportComponent, applyMove, insertAt, isNoOpDrop, moveItem, removeAt, reorderItems };
|
|
4599
|
+
export { AutoScrollService, DragPlaceholderComponent, DragPreviewComponent, DragStateService, DraggableDirective, DroppableDirective, DroppableGroupDirective, END_OF_LIST, ElementCloneService, INITIAL_DRAG_STATE, KeyboardDragService, OverlayContainerService, PlaceholderComponent, PositionCalculatorService, ScrollableDirective, VDND_GROUP_TOKEN, VDND_SCROLL_CONTAINER, VDND_VIRTUAL_VIEWPORT, VirtualContentComponent, VirtualForDirective, VirtualScrollContainerComponent, VirtualSortableListComponent, VirtualViewportComponent, applyMove, insertAt, isNoOpDrop, moveItem, removeAt, reorderItems };
|
|
4475
4600
|
//# sourceMappingURL=ngx-virtual-dnd.mjs.map
|