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
- * This component should be placed at the root of your application (or at least
1609
- * outside of any scrollable containers) to ensure the preview is always visible.
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%;box-shadow:0 4px 12px #00000026;border-radius:4px;overflow:hidden}.vdnd-drag-preview-default{padding:8px;background:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 2px 8px #00000026}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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%;box-shadow:0 4px 12px #00000026;border-radius:4px;overflow:hidden}.vdnd-drag-preview-default{padding:8px;background:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 2px 8px #00000026}\n"] }]
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
- #hasWarnedMissingGroup = false;
1908
- effectiveGroup = computed(() => {
1909
- const explicit = this.vdndDroppableGroup();
1910
- if (explicit)
1911
- return explicit;
1912
- const inherited = this.#parentGroup?.group();
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
- #hasWarnedMissingGroup = false;
2949
- #effectiveGroup = computed(() => {
2950
- const explicit = this.vdndDraggableGroup();
2951
- if (explicit)
2952
- return explicit;
2953
- const inherited = this.#parentGroup?.group();
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
- // Adjust when the dragged item is above the rendered range so the wrapper
4084
- // stays aligned with the collapsed list (dragged item is display:none).
4085
- const shouldAdjustRenderStart = this.#useViewportPositioning &&
4086
- this.#dragState.isDragging() &&
4087
- isSourceList &&
4088
- draggedIndex >= 0 &&
4089
- draggedIndex < start;
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
- // 1. Calculate which keys we need and build the ordered list of items to render
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
- // If placeholder is at the end (after all rendered items), add it
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 the dragged item view alive when it scrolls out of range.
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
- // 2. Remove views we no longer need (move to pool)
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
- // 3. First, process all item views (placeholder is handled separately via DOM)
4275
+ // Process items and track placeholder position
4170
4276
  let viewContainerIndex = 0;
4171
- let placeholderDomPosition = -1; // Track where placeholder should go in DOM
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
- // Handle regular item
4179
- const { key, context } = entry;
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 only when NOT inside a viewport component
4209
- // (viewport provides wrapper-based transform positioning)
4294
+ // Apply absolute positioning when not using viewport wrapper
4210
4295
  if (!this.#useViewportPositioning) {
4211
- const topOffset = entry.visualIndex * itemHeight;
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
- // 4. Handle placeholder DOM insertion (separate from ViewContainerRef)
4224
- if (showPlaceholder && this.#placeholder && placeholderDomPosition >= 0) {
4225
- this.#placeholder.style.height = `${itemHeight}px`;
4226
- // Get the container element (parent of views)
4227
- const container = this.#viewContainer.element.nativeElement.parentElement;
4228
- if (container) {
4229
- // Find the element to insert before (nth child, excluding spacers and placeholder itself)
4230
- const children = Array.from(container.children).filter((el) => {
4231
- const element = el;
4232
- return (!element.classList.contains('vdnd-drag-placeholder') &&
4233
- !element.classList.contains('vdnd-virtual-for-spacer') &&
4234
- !element.classList.contains('vdnd-content-spacer'));
4235
- });
4236
- const insertBeforeEl = children[placeholderDomPosition] ?? null;
4237
- if (!this.#placeholderInDom) {
4238
- if (insertBeforeEl) {
4239
- container.insertBefore(this.#placeholder, insertBeforeEl);
4240
- }
4241
- else {
4242
- container.appendChild(this.#placeholder);
4243
- }
4244
- this.#placeholderInDom = true;
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
- // Move placeholder to correct position if needed
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
- // 4. Destroy unused views in pool (keep some for reuse)
4261
- while (this.#viewPool.length > 10) {
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