@syncfusion/ej2-navigations 34.1.31 → 34.2.2

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.
@@ -30,6 +30,7 @@ import { ListBase } from '@syncfusion/ej2-lists';
30
30
  import { createCheckBox, rippleMouseHandler } from '@syncfusion/ej2-buttons';
31
31
  import { Input } from '@syncfusion/ej2-inputs';
32
32
  import { createSpinner, showSpinner, hideSpinner } from '@syncfusion/ej2-popups';
33
+ import { Skeleton } from '@syncfusion/ej2-notifications';
33
34
  var ROOT = 'e-treeview';
34
35
  var CONTROL = 'e-control';
35
36
  var COLLAPSIBLE = 'e-icon-collapsible';
@@ -82,6 +83,9 @@ var CHILD = 'e-has-child';
82
83
  var ITEM_ANIMATION_ACTIVE = 'e-animation-active';
83
84
  var DISABLED = 'e-disabled';
84
85
  var PREVENTSELECT = 'e-prevent';
86
+ var VIRTUALIZATION = 'e-virtualization';
87
+ var VIRTUAL_MASK_PARENT = 'e-mask-parent';
88
+ var VIRTUAL_MASK_ITEM = 'e-mask-item';
85
89
  var treeAriaAttr = {
86
90
  treeRole: 'group',
87
91
  itemRole: 'treeitem',
@@ -224,6 +228,55 @@ var TreeView = /** @class */ (function (_super) {
224
228
  _this.isDropIn = false;
225
229
  _this.OldCheckedData = [];
226
230
  _this.isHiddenItem = false;
231
+ // ── Virtualization state ──────────────────────────────────────────────────────────────
232
+ /** Flat ordered list of all currently-visible (non-collapsed) tree nodes. */
233
+ _this.virtualFlatNodes = [];
234
+ /** Maps node id → index in virtualFlatNodes for O(1) lookups. */
235
+ _this.virtualNodeMap = new Map();
236
+ /** Set of expanded node IDs for O(1) lookup performance during flattening (avoids array indexOf). */
237
+ _this.virtualExpandedNodesSet = new Set();
238
+ /** Set of checked node IDs for O(1) lookup performance during flattening. */
239
+ _this.virtualCheckedNodesSet = new Set();
240
+ /** Set of indeterminate node IDs — persists across flattenVisibleNodes() rebuilds. */
241
+ _this.virtualIndeterminateNodesSet = new Set();
242
+ /** Set of selected node IDs for O(1) lookup performance during flattening. */
243
+ _this.virtualSelectedNodesSet = new Set();
244
+ /** Row height in pixels, measured from the first rendered item or set to 40 as a default. */
245
+ _this.virtualItemHeight = 40;
246
+ /** Height of the scrollable container in pixels. */
247
+ _this.virtualViewportHeight = 0;
248
+ /** scrollTop captured during the previous scroll handler invocation. */
249
+ _this.virtualPrevScrollTop = 0;
250
+ /** Index of the first rendered row in the current window. */
251
+ _this.virtualStartIndex = 0;
252
+ /** Index one past the last rendered row in the current window. */
253
+ _this.virtualEndIndex = 0;
254
+ /** Extra rows rendered above and below the visible window to smooth scrolling. */
255
+ _this.virtualOverscanCount = 3;
256
+ /** RequestAnimationFrame handle used for throttling scroll events. */
257
+ _this.virtualScrollRAF = null;
258
+ /** The `.e-mask-parent` overlay element shown during fast scroll. */
259
+ _this.virtualMaskElement = null;
260
+ /** Bound scroll handler stored for later removeEventListener. */
261
+ _this.virtualScrollBound = null;
262
+ /** Skeleton component instances inside the mask overlay. */
263
+ _this.skeletonInstances = [];
264
+ /** Set to true whenever the flat node list must be rebuilt on next render. */
265
+ _this.virtualTreeDirty = false;
266
+ /** The data-uid of the currently keyboard-focused node in virtual mode (persists across re-renders). */
267
+ _this.virtualFocusedId = null;
268
+ /**
269
+ * Set to true by the document-level `keydown` handler when the user presses
270
+ * the Tab key. The browser will auto-scroll the focused element into view
271
+ * immediately after focus, which fires a `scroll` event on the TreeView
272
+ * container. We want to skip the re-render that the virtual scroll handler
273
+ * would otherwise trigger, because the element being focused is already
274
+ * rendered (it is the single `<li tabindex="0">` of the current window) and
275
+ * re-rendering would tear down the DOM element that is currently holding
276
+ * focus. The flag is consumed (reset to `false`) inside `onVirtualScroll`
277
+ * after the auto-scroll has been handled.
278
+ */
279
+ _this.virtualSkipNextScrollRender = false;
227
280
  return _this;
228
281
  }
229
282
  TreeView_1 = TreeView;
@@ -525,6 +578,24 @@ var TreeView = /** @class */ (function (_super) {
525
578
  }
526
579
  };
527
580
  TreeView.prototype.renderItems = function (isSorted) {
581
+ // In virtual mode (with a fixed height) create only the container <ul> —
582
+ // initVirtualization will populate the items directly, avoiding the O(n)
583
+ // ListBase.createList cost.
584
+ // updateCheckedStateFromDS / updateParentCheckState are intentionally skipped here:
585
+ // they call getCheckedNodeDetails which traverses the DOM (no DOM exists yet in
586
+ // virtual mode) and would throw on empty recursion results. The initial checked
587
+ // state is derived from the raw data inside flattenNodeRecursive instead.
588
+ //
589
+ // When enableVirtualization is true but height is NOT provided, we fall through
590
+ // to the normal (non-virtualized) rendering path so the tree is still visible.
591
+ if (this.enableVirtualization && !isNOU(this.height)) {
592
+ this.ulElement = this.createElement('ul', {
593
+ className: PARENTITEM + ' ' + 'e-ul',
594
+ attrs: { role: 'group' }
595
+ });
596
+ this.element.appendChild(this.ulElement);
597
+ return;
598
+ }
528
599
  this.listBaseOption.ariaAttributes.level = 1;
529
600
  var sortedData = this.getSortedData(this.rootData);
530
601
  this.ulElement = ListBase.createList(this.createElement, isSorted ? this.rootData : sortedData, this.listBaseOption);
@@ -1571,7 +1642,7 @@ var TreeView = /** @class */ (function (_super) {
1571
1642
  else if (state === 'indeterminate' && this.autoCheck) {
1572
1643
  frameSpan.classList.remove(CHECK);
1573
1644
  frameSpan.classList.add(INDETERMINATE);
1574
- this.removeCheck(currLi);
1645
+ this.removeCheck(currLi, true);
1575
1646
  ariaState = 'mixed';
1576
1647
  }
1577
1648
  ariaState = state === 'check' ? 'true' : state === 'uncheck' ? 'false' : ariaState;
@@ -1602,12 +1673,110 @@ var TreeView = /** @class */ (function (_super) {
1602
1673
  if (!isNOU(id) && this.checkedNodes.indexOf(id) === -1) {
1603
1674
  this.checkDisabledState(id);
1604
1675
  }
1676
+ if (this.enableVirtualization) {
1677
+ // Clear any indeterminate state — node is now fully checked
1678
+ this.virtualIndeterminateNodesSet.delete(id);
1679
+ var actualIndex = this.virtualNodeMap.get(id);
1680
+ if (!isNOU(actualIndex) && actualIndex > -1) {
1681
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = true;
1682
+ if (this.autoCheck && this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].hasChildren) {
1683
+ this.setVirtualChildCheckState(id, true);
1684
+ }
1685
+ }
1686
+ }
1605
1687
  };
1606
- TreeView.prototype.removeCheck = function (liEle) {
1607
- var index = this.checkedNodes.indexOf(liEle.getAttribute('data-uid'));
1688
+ TreeView.prototype.removeCheck = function (liEle, isIndeterminate) {
1689
+ var id = liEle.getAttribute('data-uid');
1690
+ var index = this.checkedNodes.indexOf(id);
1608
1691
  if (index > -1) {
1609
1692
  this.checkedNodes.splice(index, 1);
1610
1693
  }
1694
+ if (this.enableVirtualization) {
1695
+ var actualIndex = this.virtualNodeMap.get(id);
1696
+ if (!isNOU(actualIndex) && actualIndex > -1) {
1697
+ if (isIndeterminate) {
1698
+ // Parent is transitioning to indeterminate — preserve children states
1699
+ // and mark this node as 'indeterminate' so scroll/collapse re-renders correctly.
1700
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = 'indeterminate';
1701
+ // Also record it in the persistent indeterminate set so flattenVisibleNodes()
1702
+ // can restore this state after a full rebuild (e.g. on collapse).
1703
+ this.virtualIndeterminateNodesSet.add(id);
1704
+ }
1705
+ else {
1706
+ // Node is being fully unchecked — clear indeterminate tracking too
1707
+ this.virtualIndeterminateNodesSet.delete(id);
1708
+ this.virtualCheckedNodesSet.delete(id);
1709
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = false;
1710
+ this.treeData[parseInt(id.toString(), 10) - 1].isChecked = false;
1711
+ if (this.autoCheck && this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].hasChildren) {
1712
+ this.setVirtualChildCheckState(id, false);
1713
+ }
1714
+ }
1715
+ }
1716
+ }
1717
+ };
1718
+ /**
1719
+ * Recursively sets the `isChecked` state of all descendants of the node with
1720
+ * `parentId` in `virtualFlatNodes`, keeping `checkedNodes` and
1721
+ * `virtualCheckedNodesSet` in sync. Used in virtual mode to honor the
1722
+ * `autoCheck` cascade when a parent checkbox is toggled.
1723
+ *
1724
+ * @param {string} parentId - The `data-uid` of the parent whose descendants should be updated.
1725
+ * @param {boolean} isChecked - The new state to apply to every descendant.
1726
+ * @returns {void}
1727
+ * @private
1728
+ */
1729
+ TreeView.prototype.setVirtualChildCheckState = function (parentId, isChecked) {
1730
+ if (!this.virtualFlatNodes || this.virtualFlatNodes.length === 0) {
1731
+ return;
1732
+ }
1733
+ for (var i = 0; i < this.virtualFlatNodes.length; i++) {
1734
+ var node = this.virtualFlatNodes[parseInt(i.toString(), 10)];
1735
+ if (node.parentId === parentId) {
1736
+ node.isChecked = isChecked;
1737
+ if (isChecked) {
1738
+ this.virtualCheckedNodesSet.add(node.id);
1739
+ if (this.checkedNodes && this.checkedNodes.indexOf(node.id) === -1) {
1740
+ this.checkedNodes.push(node.id);
1741
+ }
1742
+ }
1743
+ else {
1744
+ this.virtualCheckedNodesSet.delete(node.id);
1745
+ var idx = this.checkedNodes ? this.checkedNodes.indexOf(node.id) : -1;
1746
+ if (idx !== -1) {
1747
+ this.checkedNodes.splice(idx, 1);
1748
+ }
1749
+ }
1750
+ if (node.hasChildren) {
1751
+ this.setVirtualChildCheckState(node.id, isChecked);
1752
+ }
1753
+ }
1754
+ }
1755
+ // Collapsed children are not present in virtualFlatNodes, so walk the raw data
1756
+ // to keep this.treeData and checkedNodes in sync for hidden descendants too.
1757
+ var collapsedChildren = this.getChildNodes(this.treeData, parentId);
1758
+ if (collapsedChildren) {
1759
+ for (var i = 0; i < collapsedChildren.length; i++) {
1760
+ var childId = collapsedChildren[parseInt(i.toString(), 10)][this.fields.id]
1761
+ ? collapsedChildren[parseInt(i.toString(), 10)][this.fields.id].toString()
1762
+ : null;
1763
+ if (childId && (!this.virtualNodeMap || !this.virtualNodeMap.has(childId))) {
1764
+ this.updateField(this.treeData, this.fields, childId, this.fields.isChecked, isChecked);
1765
+ if (isChecked) {
1766
+ if (this.checkedNodes && this.checkedNodes.indexOf(childId) === -1) {
1767
+ this.checkedNodes.push(childId);
1768
+ }
1769
+ }
1770
+ else {
1771
+ var idx = this.checkedNodes ? this.checkedNodes.indexOf(childId) : -1;
1772
+ if (idx !== -1) {
1773
+ this.checkedNodes.splice(idx, 1);
1774
+ }
1775
+ }
1776
+ this.setVirtualChildCheckState(childId, isChecked);
1777
+ }
1778
+ }
1779
+ }
1611
1780
  };
1612
1781
  TreeView.prototype.getCheckEvent = function (currLi, action, e) {
1613
1782
  this.checkActionNodes.push(this.getNodeData(currLi));
@@ -1639,6 +1808,9 @@ var TreeView = /** @class */ (function (_super) {
1639
1808
  return;
1640
1809
  }
1641
1810
  var liEle = ulEle ? selectAll('.' + LISTITEM, ulEle) : this.liList;
1811
+ if (!liEle) {
1812
+ return;
1813
+ }
1642
1814
  var length = liEle.length;
1643
1815
  for (var i = 0; i < length; i++) {
1644
1816
  this.calculateWrap(liEle[parseInt(i.toString(), 10)]);
@@ -1727,6 +1899,10 @@ var TreeView = /** @class */ (function (_super) {
1727
1899
  }
1728
1900
  this.isLoaded = true;
1729
1901
  this.isNodeDropped = false;
1902
+ // Initialise (or re-initialise) virtual mode after data binding has settled
1903
+ if (this.enableVirtualization && !isNOU(this.height)) {
1904
+ this.initVirtualization();
1905
+ }
1730
1906
  };
1731
1907
  TreeView.prototype.doSelectionAction = function () {
1732
1908
  var sNodes = selectAll('.' + SELECTED, this.element);
@@ -1861,8 +2037,18 @@ var TreeView = /** @class */ (function (_super) {
1861
2037
  };
1862
2038
  this.trigger('nodeClicked', eventArgs);
1863
2039
  };
1864
- TreeView.prototype.expandNode = function (currLi, icon, loaded) {
2040
+ TreeView.prototype.expandNode = function (currLi, icon, loaded, isKeyboardNav) {
1865
2041
  var _this = this;
2042
+ // Virtual mode fast-path: rebuild the flat list and re-render the window.
2043
+ // Only engage when height is also set; without a fixed height the tree falls
2044
+ // back to normal rendering and the standard expand path must be used.
2045
+ if (this.enableVirtualization && !isNOU(this.height)) {
2046
+ var id = currLi ? currLi.getAttribute('data-uid') : null;
2047
+ if (id) {
2048
+ this.handleVirtualExpand(id, isKeyboardNav);
2049
+ }
2050
+ return;
2051
+ }
1866
2052
  this.renderReactTemplates();
1867
2053
  if (icon.classList.contains(LOAD)) {
1868
2054
  this.hideSpinner(icon);
@@ -1970,16 +2156,30 @@ var TreeView = /** @class */ (function (_super) {
1970
2156
  removeClass([icon], PROCESS);
1971
2157
  }
1972
2158
  else {
1973
- _this.nodeCollapseAction(currLi, icon, observedArgs);
2159
+ // Detect if event is keyboard navigation by checking for 'action' property
2160
+ var isKeyboardNav = e && e.action !== undefined;
2161
+ _this.nodeCollapseAction(currLi, icon, observedArgs, isKeyboardNav);
1974
2162
  }
1975
2163
  });
1976
2164
  }
1977
2165
  else {
1978
- this.nodeCollapseAction(currLi, icon, colArgs);
2166
+ // Detect if event is keyboard navigation by checking for 'action' property
2167
+ var isKeyboardNav = e && e.action !== undefined;
2168
+ this.nodeCollapseAction(currLi, icon, colArgs, isKeyboardNav);
1979
2169
  }
1980
2170
  };
1981
- TreeView.prototype.nodeCollapseAction = function (currLi, icon, colArgs) {
2171
+ TreeView.prototype.nodeCollapseAction = function (currLi, icon, colArgs, isKeyboardNav) {
1982
2172
  var _this = this;
2173
+ // Virtual mode fast-path: rebuild the flat list and re-render the window.
2174
+ // Only engage when height is also set; without a fixed height the tree falls
2175
+ // back to normal rendering and the standard collapse path must be used.
2176
+ if (this.enableVirtualization && !isNOU(this.height)) {
2177
+ var id = currLi ? currLi.getAttribute('data-uid') : null;
2178
+ if (id) {
2179
+ this.handleVirtualCollapse(id, isKeyboardNav);
2180
+ }
2181
+ return;
2182
+ }
1983
2183
  removeClass([icon], COLLAPSIBLE);
1984
2184
  addClass([icon], EXPANDABLE);
1985
2185
  var start = 0;
@@ -2443,6 +2643,10 @@ var TreeView = /** @class */ (function (_super) {
2443
2643
  li.setAttribute('tabindex', '0');
2444
2644
  EventHandler.add(li, 'blur', this.focusOut, this);
2445
2645
  this.updateIdAttr(focusedNode, li);
2646
+ // Track focused id for virtual mode re-render restoration
2647
+ if (this.enableVirtualization) {
2648
+ this.virtualFocusedId = li.getAttribute('data-uid');
2649
+ }
2446
2650
  }
2447
2651
  };
2448
2652
  TreeView.prototype.addSelect = function (liEle) {
@@ -2501,6 +2705,9 @@ var TreeView = /** @class */ (function (_super) {
2501
2705
  }
2502
2706
  };
2503
2707
  TreeView.prototype.expandCollapseAction = function (currLi, e) {
2708
+ if (this.enableVirtualization && !isNOU(this.height) && currLi.classList.contains(EDITING)) {
2709
+ return;
2710
+ }
2504
2711
  var icon = select('div.' + ICON, currLi);
2505
2712
  if (!icon || icon.classList.contains(PROCESS)) {
2506
2713
  return;
@@ -2531,15 +2738,29 @@ var TreeView = /** @class */ (function (_super) {
2531
2738
  removeClass([icon], PROCESS);
2532
2739
  }
2533
2740
  else {
2534
- _this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll);
2741
+ // Detect if event is keyboard navigation by checking for 'action' property
2742
+ var isKeyboardNav = e && e.action !== undefined;
2743
+ _this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav);
2535
2744
  }
2536
2745
  });
2537
2746
  }
2538
2747
  else {
2539
- this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll);
2748
+ // Detect if event is keyboard navigation by checking for 'action' property
2749
+ var isKeyboardNav = e && e.action !== undefined;
2750
+ this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav);
2751
+ }
2752
+ };
2753
+ TreeView.prototype.nodeExpandAction = function (currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav) {
2754
+ // In virtual mode (with a fixed height) the flat <li> never has a child <ul>
2755
+ // in the DOM, so the PARENTITEM check below would always fail and fall into
2756
+ // renderChildNodes(), which corrupts the virtual DOM.
2757
+ // Delegate directly to expandNode() which has the handleVirtualExpand fast-path.
2758
+ // When height is not provided, fall through to normal expand logic below since
2759
+ // the tree is rendered with a full nested DOM structure.
2760
+ if (this.enableVirtualization && !isNOU(this.height)) {
2761
+ this.expandNode(currLi, icon, undefined, isKeyboardNav);
2762
+ return;
2540
2763
  }
2541
- };
2542
- TreeView.prototype.nodeExpandAction = function (currLi, icon, expandChild, callback, isFromExpandAll) {
2543
2764
  var ul = select('.' + PARENTITEM, currLi);
2544
2765
  if (ul && ul.nodeName === 'UL') {
2545
2766
  this.expandNode(currLi, icon);
@@ -2653,6 +2874,19 @@ var TreeView = /** @class */ (function (_super) {
2653
2874
  });
2654
2875
  };
2655
2876
  TreeView.prototype.navigateToFocus = function (isUp) {
2877
+ // In virtual mode (with a fixed height) the element itself is the scroll
2878
+ // container; use ensureVirtualNodeVisible. When height is not provided,
2879
+ // the tree uses normal rendering, so fall through to the standard scroll logic.
2880
+ if (this.enableVirtualization && !isNOU(this.height)) {
2881
+ var focusedLi = this.getFocusedNode();
2882
+ if (focusedLi) {
2883
+ var id = focusedLi.getAttribute('data-uid');
2884
+ if (id) {
2885
+ this.ensureVirtualNodeVisible(id);
2886
+ }
2887
+ }
2888
+ return;
2889
+ }
2656
2890
  var focusNode = this.getFocusedNode().querySelector('.' + TEXTWRAP);
2657
2891
  var pos = focusNode.getBoundingClientRect();
2658
2892
  var parent = this.getScrollParent(this.element);
@@ -3073,6 +3307,46 @@ var TreeView = /** @class */ (function (_super) {
3073
3307
  return isNOU(fNode) ? (isNOU(selectedItem) ? this.element.firstElementChild : selectedItem) : fNode;
3074
3308
  };
3075
3309
  TreeView.prototype.focusNextNode = function (li, isTowards) {
3310
+ if (this.enableVirtualization && !isNOU(this.height)) {
3311
+ // Determine the id of the node one step in the requested direction
3312
+ var currentId = li ? li.getAttribute('data-uid') : null;
3313
+ var currentIdx = -1;
3314
+ if (!isNOU(currentId)) {
3315
+ var idx = this.virtualNodeMap.get(currentId);
3316
+ currentIdx = !isNOU(idx) ? idx : -1;
3317
+ }
3318
+ var targetIdx = isTowards ? currentIdx + 1 : currentIdx - 1;
3319
+ if (targetIdx < 0 || targetIdx >= this.virtualFlatNodes.length) {
3320
+ return;
3321
+ }
3322
+ var targetId = this.virtualFlatNodes[parseInt(targetIdx.toString(), 10)].id;
3323
+ // ── Fix 1: set virtualFocusedId BEFORE calling ensureVirtualNodeVisible ──
3324
+ // renderVirtualItems() is called synchronously inside ensureVirtualNodeVisible
3325
+ // when a re-render is needed. If virtualFocusedId is still the old value at
3326
+ // that point, focus restoration falls back to the first item. Setting it here
3327
+ // ensures renderVirtualItems always restores focus to the correct target node.
3328
+ this.virtualFocusedId = targetId;
3329
+ // ── Fix 2: clear focus classes from ALL nested list items, not just top-level ones ──
3330
+ // With hierarchical virtual DOM, the currently focused <li> may be nested inside
3331
+ // a child <ul>. Using ':scope > li' only cleans up direct children and leaves
3332
+ // stale e-focused / e-node-focus classes on nested nodes across re-renders.
3333
+ var allLi = this.element.querySelectorAll('.' + LISTITEM + '.' + FOCUS);
3334
+ for (var k = 0; k < allLi.length; k++) {
3335
+ allLi[parseInt(k.toString(), 10)].classList.remove(FOCUS);
3336
+ allLi[parseInt(k.toString(), 10)].classList.remove(FOCUSED);
3337
+ allLi[parseInt(k.toString(), 10)].setAttribute('tabindex', '-1');
3338
+ }
3339
+ // Ensure the target <li> is in the DOM window (may trigger a re-render).
3340
+ // virtualFocusedId is already set above so renderVirtualItems restores focus correctly.
3341
+ this.ensureVirtualNodeVisible(targetId);
3342
+ // Re-query both nodes after the potential re-render so setFocus gets live DOM elements
3343
+ var prevNode = currentId ? this.getElement(currentId) : li;
3344
+ var nextNode_1 = this.getElement(targetId);
3345
+ if (!isNOU(nextNode_1)) {
3346
+ this.setFocus(isNOU(prevNode) ? nextNode_1 : prevNode, nextNode_1);
3347
+ }
3348
+ return;
3349
+ }
3076
3350
  var nextNode = isTowards ? this.getNextNode(li) : this.getPrevNode(li);
3077
3351
  this.setFocus(li, nextNode);
3078
3352
  this.navigateToFocus(!isTowards);
@@ -3142,6 +3416,12 @@ var TreeView = /** @class */ (function (_super) {
3142
3416
  addClass([nextNode], FOCUS);
3143
3417
  addClass([nextNode], FOCUSED);
3144
3418
  nextNode.setAttribute('tabindex', '0');
3419
+ if (this.enableVirtualization && !isNOU(this.height)) {
3420
+ nextNode.focus({ preventScroll: true });
3421
+ }
3422
+ else {
3423
+ nextNode.focus();
3424
+ }
3145
3425
  nextNode.focus();
3146
3426
  EventHandler.add(nextNode, 'blur', this.focusOut, this);
3147
3427
  this.updateIdAttr(preNode, nextNode);
@@ -3160,6 +3440,30 @@ var TreeView = /** @class */ (function (_super) {
3160
3440
  if (!focusedElement) {
3161
3441
  return;
3162
3442
  }
3443
+ // Virtual-mode guard: when the browser moves focus into the TreeView (e.g. via
3444
+ // the Tab key) it always lands on the single `<li tabindex="0">`. In that
3445
+ // case the element is already the focusable one, FOCUS is already applied
3446
+ // and the only thing left to do is the keyboard focus styling. Skip the
3447
+ // redundant setAttribute/EventHandler.add work so we never re-render the
3448
+ // virtual window as a side effect of focusing the existing `tabindex="0"`
3449
+ // item.
3450
+ // ── Fix: Use document.activeElement to detect browser-focused node ──
3451
+ // After renderVirtualItems() restores focus, the browser-focused element is
3452
+ // the authoritative source, not getFocusedNode(). If they don't match, it means
3453
+ // renderVirtualItems already correctly set up the element, so we must not
3454
+ // override its setup by calling setFocus again.
3455
+ var actuallyFocusedElement = document.activeElement;
3456
+ var isAlreadyFocusedInVirtualMode = this.enableVirtualization
3457
+ && actuallyFocusedElement === focusedElement
3458
+ && actuallyFocusedElement.getAttribute('tabindex') === '0'
3459
+ && actuallyFocusedElement.classList.contains(FOCUS);
3460
+ if (isAlreadyFocusedInVirtualMode) {
3461
+ if (this.isKeyboardFocus && !actuallyFocusedElement.classList.contains(FOCUSED)) {
3462
+ addClass([actuallyFocusedElement], FOCUSED);
3463
+ }
3464
+ this.isKeyboardFocus = false;
3465
+ return;
3466
+ }
3163
3467
  if (!this.mouseDownStatus) {
3164
3468
  if (focusedElement.classList.contains('e-disable')) {
3165
3469
  focusedElement.setAttribute('tabindex', '-1');
@@ -3186,6 +3490,9 @@ var TreeView = /** @class */ (function (_super) {
3186
3490
  removeClass([focusedElement], FOCUS);
3187
3491
  removeClass([focusedElement], FOCUSED);
3188
3492
  EventHandler.remove(focusedElement, 'blur', this.focusOut);
3493
+ if (this.enableVirtualization) {
3494
+ this.virtualFocusedId = null;
3495
+ }
3189
3496
  }
3190
3497
  };
3191
3498
  TreeView.prototype.onMouseOver = function (e) {
@@ -3399,6 +3706,19 @@ var TreeView = /** @class */ (function (_super) {
3399
3706
  else {
3400
3707
  txtEle[ListBase.getDomSetter(this.listBaseOption)] = ListBase.getSanitizedText(newText, this.listBaseOption);
3401
3708
  }
3709
+ // ── Virtual mode: sync the flat-node snapshot so re-renders show updated text ──
3710
+ // virtualFlatNodes is rebuilt from this.treeData (via flattenVisibleNodes) only on
3711
+ // expand/collapse/scroll. Between those events the snapshot is the single source of
3712
+ // truth used by createVirtualListItem. setValue() above updates this.editData (which
3713
+ // IS the treeData object reference), but virtualFlatNodes[idx].text still holds the
3714
+ // old value. Update it here so the next renderVirtualItems() call shows the new text.
3715
+ if (this.enableVirtualization) {
3716
+ var id = liEle.getAttribute('data-uid');
3717
+ var idx = this.virtualNodeMap ? this.virtualNodeMap.get(id) : undefined;
3718
+ if (!isNOU(idx) && idx > -1) {
3719
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].text = newText;
3720
+ }
3721
+ }
3402
3722
  if (isInput) {
3403
3723
  removeClass([liEle], EDITING);
3404
3724
  liEle.focus();
@@ -4779,6 +5099,31 @@ var TreeView = /** @class */ (function (_super) {
4779
5099
  TreeView.prototype.keyDownHandler = function (e) {
4780
5100
  if (e.key === 'Tab' || e.keyCode === 9) {
4781
5101
  this.isKeyboardFocus = true;
5102
+ // Mark the upcoming scroll event (if any) as Tab-induced so the
5103
+ // virtual scroll handler skips its re-render path. The browser
5104
+ // calls `Element.scrollIntoView` semantics on focus, which scrolls
5105
+ // a `<li tabindex="0">` in the middle of the tree into view and
5106
+ // therefore fires `scroll` on the container. Without this guard
5107
+ // the scroll handler would tear down the focused `<li>` and move
5108
+ // focus elsewhere.
5109
+ if (this.enableVirtualization) {
5110
+ this.virtualSkipNextScrollRender = true;
5111
+ }
5112
+ }
5113
+ // ── Fix: Also skip scroll render for arrow key navigation ──────────────────
5114
+ // Arrow Down/Up calls focusNextNode() which calls ensureVirtualNodeVisible()
5115
+ // to scroll the target node into view. Without this guard, the scroll event
5116
+ // would trigger onVirtualScroll() which re-renders the window and resets the
5117
+ // focus that focusNextNode() just set. This manifests as tabindex resetting
5118
+ // to the first item instead of staying on the arrow-navigated target.
5119
+ // Arrow Right/Left (expand/collapse) also calls renderVirtualItems() which
5120
+ // calls li.focus() in branch 2; that can trigger a scroll on an edge node,
5121
+ // so we consume it here to prevent a stale RAF re-render.
5122
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.keyCode === 40 || e.keyCode === 38
5123
+ || e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.keyCode === 39 || e.keyCode === 37) {
5124
+ if (this.enableVirtualization) {
5125
+ this.virtualSkipNextScrollRender = true;
5126
+ }
4782
5127
  }
4783
5128
  };
4784
5129
  TreeView.prototype.preventContextMenu = function (e) {
@@ -5135,6 +5480,18 @@ var TreeView = /** @class */ (function (_super) {
5135
5480
  if (this.dataType === 1) {
5136
5481
  this.groupedData = this.getGroupedData(this.treeData, this.fields.parentID);
5137
5482
  }
5483
+ // In virtual mode (with a fixed height), rebuild the flat node list and re-render
5484
+ // after deletion so the removed nodes are correctly reflected in the hierarchical
5485
+ // virtual DOM. When height is not provided, the tree uses normal rendering and
5486
+ // the DOM is already updated by removeNode() above.
5487
+ if (this.enableVirtualization && !isNOU(this.height)) {
5488
+ this.virtualFlatNodes = this.flattenVisibleNodes();
5489
+ this.rebuildVirtualNodeMap();
5490
+ var win = this.getVirtualWindow();
5491
+ this.virtualStartIndex = win.startIndex;
5492
+ this.virtualEndIndex = win.endIndex;
5493
+ this.renderVirtualItems();
5494
+ }
5138
5495
  this.triggerEvent('removeNode', nodeData);
5139
5496
  };
5140
5497
  TreeView.prototype.editSucess = function (target, newText, prevent) {
@@ -5195,6 +5552,1031 @@ var TreeView = /** @class */ (function (_super) {
5195
5552
  }
5196
5553
  return parentsID;
5197
5554
  };
5555
+ // ═══════════════════════════════════════════════════════════════════════════════
5556
+ // VIRTUALIZATION — private helpers
5557
+ // ═══════════════════════════════════════════════════════════════════════════════
5558
+ /**
5559
+ * Parses the `height` property (string like '400px' or number) to a pixel value.
5560
+ * Returns 0 if the value cannot be determined.
5561
+ *
5562
+ * @returns {number} Pixel height.
5563
+ * @private
5564
+ */
5565
+ TreeView.prototype.parseHeightToPixels = function () {
5566
+ var h = this.height;
5567
+ if (isNOU(h)) {
5568
+ return 0;
5569
+ }
5570
+ if (typeof h === 'number') {
5571
+ return h;
5572
+ }
5573
+ var parsed = parseFloat(h);
5574
+ return isNaN(parsed) ? 0 : parsed;
5575
+ };
5576
+ /**
5577
+ * Measures the height of the first rendered list item so subsequent calculations
5578
+ * use real layout values. Falls back to the stored `virtualItemHeight` default (40).
5579
+ *
5580
+ * @returns {number} Row height in pixels.
5581
+ * @private
5582
+ */
5583
+ TreeView.prototype.measureVirtualItemHeight = function () {
5584
+ var firstLi = select('.' + LISTITEM, this.element);
5585
+ if (firstLi && firstLi.offsetHeight > 0) {
5586
+ return firstLi.offsetHeight;
5587
+ }
5588
+ return this.virtualItemHeight;
5589
+ };
5590
+ /**
5591
+ * Rebuilds the cached Sets for checked/selected nodes to enable O(1) lookup
5592
+ * during the flatten operation instead of O(n) array indexOf calls.
5593
+ *
5594
+ * @returns {void}
5595
+ * @private
5596
+ */
5597
+ TreeView.prototype.rebuildVirtualCheckCache = function () {
5598
+ this.virtualExpandedNodesSet.clear();
5599
+ this.virtualCheckedNodesSet.clear();
5600
+ this.virtualSelectedNodesSet.clear();
5601
+ if (this.expandedNodes && Array.isArray(this.expandedNodes)) {
5602
+ for (var i = 0; i < this.expandedNodes.length; i++) {
5603
+ this.virtualExpandedNodesSet.add(this.expandedNodes[i]);
5604
+ }
5605
+ }
5606
+ if (this.checkedNodes && Array.isArray(this.checkedNodes)) {
5607
+ for (var i = 0; i < this.checkedNodes.length; i++) {
5608
+ this.virtualCheckedNodesSet.add(this.checkedNodes[i]);
5609
+ }
5610
+ }
5611
+ if (this.selectedNodes && Array.isArray(this.selectedNodes)) {
5612
+ for (var i = 0; i < this.selectedNodes.length; i++) {
5613
+ this.virtualSelectedNodesSet.add(this.selectedNodes[i]);
5614
+ }
5615
+ }
5616
+ };
5617
+ /**
5618
+ * Recursively walks the treeData (or groupedData for flat lists) and builds
5619
+ * `virtualFlatNodes` — an ordered list of every node that is currently visible
5620
+ * (i.e. its ancestors are all expanded). Uses cached Sets for O(1) state lookups.
5621
+ *
5622
+ * @returns {VirtualNodeData[]} Ordered flat list of visible nodes.
5623
+ * @private
5624
+ */
5625
+ TreeView.prototype.flattenVisibleNodes = function () {
5626
+ // Build cached Sets for fast lookups
5627
+ this.rebuildVirtualCheckCache();
5628
+ var result = [];
5629
+ if (this.dataType === 1) {
5630
+ // Flat data with parentID field — use getChildGroup to find root nodes
5631
+ var rootNodes = [];
5632
+ if (this.groupedData) {
5633
+ rootNodes = this.getChildGroup(this.groupedData, null, true);
5634
+ }
5635
+ if (!rootNodes || rootNodes.length === 0) {
5636
+ rootNodes = this.rootData;
5637
+ }
5638
+ // Apply sortOrder to root-level nodes for flat data (same as hierarchical path below)
5639
+ rootNodes = this.getSortedData(rootNodes);
5640
+ this.flattenNodeRecursive(rootNodes, 1, null, result);
5641
+ }
5642
+ else {
5643
+ // Hierarchical data
5644
+ var sorted = this.getSortedData(this.rootData);
5645
+ this.flattenNodeRecursive(sorted, 1, null, result);
5646
+ }
5647
+ return result;
5648
+ };
5649
+ /**
5650
+ * Recursive helper for `flattenVisibleNodes`.
5651
+ *
5652
+ * @param {Object[]} nodes - Array of raw data objects at this level.
5653
+ * @param {number} level - 1-based depth level.
5654
+ * @param {string | null} parentId - The parent node id, or null for root.
5655
+ * @param {VirtualNodeData[]} result - Accumulator array.
5656
+ * @returns {void}
5657
+ * @private
5658
+ */
5659
+ TreeView.prototype.flattenNodeRecursive = function (nodes, level, parentId, result) {
5660
+ if (!nodes || nodes.length === 0) {
5661
+ return;
5662
+ }
5663
+ // Cache field names and settings outside the loop to avoid repeated conversions
5664
+ var childFieldName = this.fields.child.toString();
5665
+ var expandedFieldName = this.fields.expanded;
5666
+ var isCheckedFieldName = this.fields.isChecked;
5667
+ var iconCssFieldName = this.fields.iconCss;
5668
+ var imageUrlFieldName = this.fields.imageUrl;
5669
+ var idFieldName = this.fields.id;
5670
+ var textFieldName = this.fields.text;
5671
+ var needsSorting = this.sortOrder !== 'None';
5672
+ var isDataTypeFlat = this.dataType === 1;
5673
+ var enableVirtualization = this.enableVirtualization;
5674
+ for (var i = 0; i < nodes.length; i++) {
5675
+ var raw = nodes[i];
5676
+ var id = isNOU(raw[idFieldName]) ? '' : raw[idFieldName].toString();
5677
+ var text = isNOU(raw[textFieldName]) ? '' : raw[textFieldName].toString();
5678
+ // Use expandedNodes as the single source of truth.
5679
+ var inExpandedNodes = this.virtualExpandedNodesSet.has(id);
5680
+ var rawExpanded = !inExpandedNodes && getValue(expandedFieldName, raw) === true;
5681
+ var isExpanded = inExpandedNodes || rawExpanded;
5682
+ if (rawExpanded && id) {
5683
+ this.expandedNodes.push(id);
5684
+ this.virtualExpandedNodesSet.add(id);
5685
+ }
5686
+ // Inline hasChildRecords check to avoid function call (optimization)
5687
+ var hasChildren = false;
5688
+ if (isDataTypeFlat) {
5689
+ if (this.groupedData) {
5690
+ var groupedChild = this.getChildGroup(this.groupedData, id, false);
5691
+ hasChildren = !isNOU(groupedChild) && groupedChild.length > 0;
5692
+ }
5693
+ }
5694
+ else {
5695
+ var childrenObj = getValue(childFieldName, raw);
5696
+ hasChildren = !isNOU(childrenObj) && Array.isArray(childrenObj) && childrenObj.length > 0;
5697
+ }
5698
+ // Virtual-mode only: honour fields.isChecked === true on initial render
5699
+ if (enableVirtualization && id && isCheckedFieldName && !this.virtualCheckedNodesSet.has(id)) {
5700
+ var rawChecked = getValue(isCheckedFieldName, raw);
5701
+ if (!isNOU(rawChecked) && rawChecked.toString() === 'true') {
5702
+ this.virtualCheckedNodesSet.add(id);
5703
+ if (this.checkedNodes && this.checkedNodes.indexOf(id) === -1) {
5704
+ this.checkedNodes.push(id);
5705
+ }
5706
+ }
5707
+ }
5708
+ // Use cached Sets for O(1) lookup
5709
+ var isSelected = this.virtualSelectedNodesSet.has(id);
5710
+ // Resolve checkbox state: indeterminate takes precedence over the checked set,
5711
+ // because a node can be in the indeterminate state without being in checkedNodes.
5712
+ var isChecked = (enableVirtualization && this.virtualIndeterminateNodesSet.has(id))
5713
+ ? 'indeterminate'
5714
+ : this.virtualCheckedNodesSet.has(id);
5715
+ var isDisabled = false; // TODO: Check if any per-node disabled field exists
5716
+ var iconCssVal = raw[iconCssFieldName];
5717
+ var iconCss = isNOU(iconCssVal) ? '' : iconCssVal.toString();
5718
+ var imageUrlVal = raw[imageUrlFieldName];
5719
+ var imageUrl = isNOU(imageUrlVal) ? '' : imageUrlVal.toString();
5720
+ var node = {
5721
+ id: id, text: text, level: level, parentId: parentId, hasChildren: hasChildren, isExpanded: isExpanded,
5722
+ isSelected: isSelected, isChecked: isChecked, isDisabled: isDisabled, iconCss: iconCss, imageUrl: imageUrl, rawData: raw
5723
+ };
5724
+ result.push(node);
5725
+ if (isExpanded && hasChildren) {
5726
+ // Determine children list
5727
+ var children = [];
5728
+ if (isDataTypeFlat) {
5729
+ if (this.groupedData) {
5730
+ children = this.getChildGroup(this.groupedData, id, false);
5731
+ // Apply sortOrder to flat-data children (same as hierarchical path)
5732
+ if (needsSorting) {
5733
+ children = ListBase.getDataSource(children, ListBase.addSorting(this.sortOrder, this.fields.text));
5734
+ }
5735
+ }
5736
+ }
5737
+ else {
5738
+ var rawChildren = getValue(childFieldName, raw);
5739
+ if (!isNOU(rawChildren) && Array.isArray(rawChildren)) {
5740
+ children = rawChildren;
5741
+ // Only sort if needed
5742
+ if (needsSorting) {
5743
+ children = ListBase.getDataSource(children, ListBase.addSorting(this.sortOrder, this.fields.text));
5744
+ }
5745
+ }
5746
+ }
5747
+ this.flattenNodeRecursive(children, level + 1, id, result);
5748
+ }
5749
+ }
5750
+ };
5751
+ /**
5752
+ * Checks whether a raw datasource node has child records.
5753
+ *
5754
+ * @param {Object} raw - Raw datasource object.
5755
+ * @returns {boolean} True if children exist.
5756
+ * @private
5757
+ */
5758
+ TreeView.prototype.hasChildRecords = function (raw) {
5759
+ if (this.dataType === 1) {
5760
+ if (!this.groupedData) {
5761
+ return false;
5762
+ }
5763
+ var rawId = raw[this.fields.id];
5764
+ if (isNOU(rawId)) {
5765
+ return false;
5766
+ }
5767
+ var groupedChild = this.getChildGroup(this.groupedData, rawId.toString(), false);
5768
+ return !isNOU(groupedChild) && groupedChild.length > 0;
5769
+ }
5770
+ var childField = this.fields.child.toString();
5771
+ var children = getValue(childField, raw);
5772
+ return !isNOU(children) && Array.isArray(children) && children.length > 0;
5773
+ };
5774
+ /**
5775
+ * Rebuilds `virtualNodeMap` (id → index) from the current `virtualFlatNodes`.
5776
+ *
5777
+ * @returns {void}
5778
+ * @private
5779
+ */
5780
+ TreeView.prototype.rebuildVirtualNodeMap = function () {
5781
+ this.virtualNodeMap.clear();
5782
+ for (var i = 0; i < this.virtualFlatNodes.length; i++) {
5783
+ this.virtualNodeMap.set(this.virtualFlatNodes[parseInt(i.toString(), 10)].id, i);
5784
+ }
5785
+ };
5786
+ TreeView.prototype.getVirtualWindow = function (scrollTop) {
5787
+ var total = this.virtualFlatNodes.length;
5788
+ if (total === 0) {
5789
+ return { startIndex: 0, endIndex: 0 };
5790
+ }
5791
+ var st = scrollTop !== undefined ? scrollTop : this.element.scrollTop;
5792
+ var itemH = this.virtualItemHeight;
5793
+ var vpH = this.virtualViewportHeight;
5794
+ var rawStart = Math.floor(st / itemH) - this.virtualOverscanCount;
5795
+ var rawEnd = Math.ceil((st + vpH) / itemH) + this.virtualOverscanCount;
5796
+ var startIndex = Math.max(0, rawStart);
5797
+ var endIndex = Math.min(total, rawEnd);
5798
+ return { startIndex: startIndex, endIndex: endIndex };
5799
+ };
5800
+ TreeView.prototype.getVirtualWindowAt = function (scrollTop) {
5801
+ return this.getVirtualWindow(scrollTop);
5802
+ };
5803
+ TreeView.prototype.getVirtualPadding = function () {
5804
+ var total = this.virtualFlatNodes.length;
5805
+ var itemH = this.virtualItemHeight;
5806
+ var start = this.virtualStartIndex;
5807
+ var end = this.virtualEndIndex;
5808
+ var paddingTop = start * itemH;
5809
+ var paddingBottom = Math.max(0, (total - end) * itemH);
5810
+ return { paddingTop: paddingTop, paddingBottom: paddingBottom };
5811
+ };
5812
+ /**
5813
+ * Renders the virtual window into a **nested** `<ul>/<li>` hierarchy identical to
5814
+ * the non-virtualized DOM structure.
5815
+ *
5816
+ * ## Scroll math
5817
+ * The flat-index ↔ pixel mapping stays unchanged: every entry in
5818
+ * `virtualFlatNodes` represents exactly one `itemHeight` row.
5819
+ * `paddingTop = startIndex * itemHeight` and
5820
+ * `paddingBottom = (total - endIndex) * itemHeight` keep the scrollbar
5821
+ * thumb in the correct position.
5822
+ *
5823
+ * ## Ancestor injection
5824
+ * When `startIndex` points at a child node whose ancestor(s) have already
5825
+ * scrolled above the viewport (their flat indices are < startIndex), the
5826
+ * ancestors would normally be missing from the DOM, forcing the children to
5827
+ * fall back to the root `<ul>` as flat siblings — producing wrong HTML.
5828
+ *
5829
+ * To fix this, before rendering the window we walk the ancestor chain of the
5830
+ * first visible node and collect every ancestor that lies **outside** the
5831
+ * window. Those ancestors are injected as real `<li>` containers at the
5832
+ * very top of the root `<ul>`. To compensate for their presence, `paddingTop`
5833
+ * is reduced by `injectedCount * itemHeight` so the total scroll height
5834
+ * remains the same.
5835
+ *
5836
+ * Once the ancestors are in the DOM they are registered in `renderedParentMap`
5837
+ * so the normal window-rendering loop can nest children inside them exactly
5838
+ * as it does when both parent and child are in the same window.
5839
+ *
5840
+ * @returns {void}
5841
+ * @private
5842
+ */
5843
+ TreeView.prototype.renderVirtualItems = function () {
5844
+ if (!this.ulElement) {
5845
+ return;
5846
+ }
5847
+ var ul = this.ulElement;
5848
+ // ── Snapshot focused ID BEFORE clearing the DOM ───────────────────────────
5849
+ // Removing a focused <li> from the DOM triggers a synchronous `blur` event,
5850
+ // which calls focusOut() and sets virtualFocusedId = null. Capturing the id
5851
+ // here (before any removeChild) ensures focus is correctly restored to the
5852
+ // same node after re-render even when keyboard navigation was in progress.
5853
+ var focusedUid = this.virtualFocusedId || null;
5854
+ // ── Clear previous render (direct children only) ─────────────────────────
5855
+ var existingItems = ul.querySelectorAll(':scope > li');
5856
+ for (var i = existingItems.length - 1; i >= 0; i--) {
5857
+ ul.removeChild(existingItems[parseInt(i.toString(), 10)]);
5858
+ }
5859
+ var renderedParentMap = new Map();
5860
+ var itemH = this.virtualItemHeight;
5861
+ var injectedCount = 0;
5862
+ if (this.virtualStartIndex < this.virtualFlatNodes.length) {
5863
+ var firstNode = this.virtualFlatNodes[this.virtualStartIndex];
5864
+ if (firstNode.parentId) {
5865
+ // Build the ancestor chain from root down to the immediate parent
5866
+ var ancestors = [];
5867
+ var searchId = firstNode.parentId;
5868
+ while (searchId) {
5869
+ var ancestorIdx = this.virtualNodeMap.get(searchId);
5870
+ if (!isNOU(ancestorIdx) && ancestorIdx < this.virtualStartIndex) {
5871
+ var ancestor = this.virtualFlatNodes[parseInt(ancestorIdx.toString(), 10)];
5872
+ ancestors.unshift(ancestor); // prepend so root comes first
5873
+ searchId = ancestor.parentId;
5874
+ }
5875
+ else {
5876
+ break;
5877
+ }
5878
+ }
5879
+ // Inject each ancestor as a real <li> into the root <ul>
5880
+ // and register it in renderedParentMap for child-nesting below.
5881
+ for (var a = 0; a < ancestors.length; a++) {
5882
+ var ancestor = ancestors[parseInt(a.toString(), 10)];
5883
+ var ancestorLi = this.createVirtualListItem(ancestor, false);
5884
+ // Find the correct container: root or a previously injected ancestor's <ul>
5885
+ var targetUl = ul;
5886
+ if (ancestor.parentId && renderedParentMap.has(ancestor.parentId)) {
5887
+ var pe = renderedParentMap.get(ancestor.parentId);
5888
+ if (!pe.childUl) {
5889
+ pe.childUl = this.createElement('ul', {
5890
+ className: PARENTITEM + ' e-ul',
5891
+ attrs: { role: 'group' }
5892
+ });
5893
+ pe.li.appendChild(pe.childUl);
5894
+ }
5895
+ targetUl = pe.childUl;
5896
+ }
5897
+ targetUl.appendChild(ancestorLi);
5898
+ renderedParentMap.set(ancestor.id, { li: ancestorLi, childUl: null });
5899
+ injectedCount++;
5900
+ }
5901
+ }
5902
+ }
5903
+ // ── Scroll-space padding (adjusted for injected ancestors) ────────────────
5904
+ var total = this.virtualFlatNodes.length;
5905
+ var start = this.virtualStartIndex;
5906
+ var end = this.virtualEndIndex;
5907
+ var rawPaddingTop = start * itemH;
5908
+ // Subtract the height of injected ancestors so the scroll position is
5909
+ // exact: those ancestor rows are now real DOM elements, not hidden space.
5910
+ var adjustedPaddingTop = Math.max(0, rawPaddingTop - injectedCount * itemH);
5911
+ ul.style.paddingTop = adjustedPaddingTop + 'px';
5912
+ ul.style.paddingBottom = Math.max(0, (total - end) * itemH) + 'px';
5913
+ // ── Render window items (nested via renderedParentMap) ────────────────────
5914
+ var liListBuf = [];
5915
+ for (var i = start; i < end; i++) {
5916
+ var node = this.virtualFlatNodes[parseInt(i.toString(), 10)];
5917
+ var li = this.createVirtualListItem(node, i === start);
5918
+ // Find the correct parent container
5919
+ var targetUl = ul;
5920
+ if (node.parentId && renderedParentMap.has(node.parentId)) {
5921
+ var parentEntry = renderedParentMap.get(node.parentId);
5922
+ // Lazily create the child <ul> the first time a child needs it
5923
+ if (!parentEntry.childUl) {
5924
+ parentEntry.childUl = this.createElement('ul', {
5925
+ className: PARENTITEM + ' e-ul',
5926
+ attrs: { role: 'group' }
5927
+ });
5928
+ parentEntry.li.appendChild(parentEntry.childUl);
5929
+ }
5930
+ targetUl = parentEntry.childUl;
5931
+ }
5932
+ targetUl.appendChild(li);
5933
+ liListBuf.push(li);
5934
+ // Register this node so its children in the same window can nest under it
5935
+ if (node.isExpanded && node.hasChildren) {
5936
+ renderedParentMap.set(node.id, { li: li, childUl: null });
5937
+ }
5938
+ }
5939
+ // ── Restore focus / tabindex ──────────────────────────────────────────────
5940
+ // In virtual mode only the li that carries the 'e-active' class should be
5941
+ // focusable (tabindex='0'). All other rendered li elements must be set
5942
+ // to tabindex='-1' so the tab order remains unambiguous as the window
5943
+ // changes during scrolling.
5944
+ var focusRestored = false;
5945
+ // 1. Prefer the currently active node if it is in the rendered window.
5946
+ for (var k = 0; k < liListBuf.length; k++) {
5947
+ var li = liListBuf[parseInt(k.toString(), 10)];
5948
+ if (li.classList.contains(ACTIVE)) {
5949
+ li.setAttribute('tabindex', '0');
5950
+ // ── Fix: ensure FOCUS class set on active node too ──
5951
+ // This ensures focusIn() guard condition is met: tabindex='0' && FOCUS class
5952
+ addClass([li], FOCUS);
5953
+ focusRestored = true;
5954
+ break;
5955
+ }
5956
+ }
5957
+ // 2. Otherwise fall back to the previously focused node (if it is in window).
5958
+ if (!focusRestored && focusedUid) {
5959
+ for (var k = 0; k < liListBuf.length; k++) {
5960
+ var li = liListBuf[parseInt(k.toString(), 10)];
5961
+ if (li.getAttribute('data-uid') === focusedUid) {
5962
+ li.setAttribute('tabindex', '0');
5963
+ addClass([li], FOCUS);
5964
+ addClass([li], FOCUSED);
5965
+ // ── Fix: restore actual browser focus so keyboard events keep working ──
5966
+ // After renderVirtualItems() replaces all <li> elements the old element
5967
+ // is gone from the DOM and document.activeElement falls back to
5968
+ // document.body. Setting tabindex + classes is not enough — the new
5969
+ // element also needs a programmatic .focus() call so subsequent keyboard
5970
+ // events (Arrow keys, Home, End, etc.) are delivered to the TreeView.
5971
+ // Re-attach the blur→focusOut handler so virtualFocusedId is cleared
5972
+ // correctly when the user tabs away.
5973
+ // ── Use preventScroll:true ────────────────────────────────────────────
5974
+ // Without preventScroll the browser scrolls the container to bring this
5975
+ // node into view during the re-render, changing scrollTop AFTER the
5976
+ // correct paddingTop has been applied. This is the root cause of the
5977
+ // KBN expand position-jump bug: the render is correct but the browser
5978
+ // silently scrolls the viewport, making the parent node appear to jump.
5979
+ // ensureVirtualNodeVisible() handles any legitimate scroll-into-view
5980
+ // needs before renderVirtualItems() is called, so scroll here is wrong.
5981
+ EventHandler.add(li, 'blur', this.focusOut, this);
5982
+ li.focus({ preventScroll: true });
5983
+ focusRestored = true;
5984
+ break;
5985
+ }
5986
+ }
5987
+ }
5988
+ // 3. Finally default to the first item in the window.
5989
+ if (!focusRestored && liListBuf.length > 0) {
5990
+ var firstLi = liListBuf[0];
5991
+ firstLi.setAttribute('tabindex', '0');
5992
+ }
5993
+ // ── Fix: Ensure all other rendered nodes have tabindex="-1" ──
5994
+ // This prevents stale tabindex="0" on multiple nodes which breaks Tab order
5995
+ for (var k = 0; k < liListBuf.length; k++) {
5996
+ var li = liListBuf[parseInt(k.toString(), 10)];
5997
+ if (li.getAttribute('tabindex') !== '0') {
5998
+ li.setAttribute('tabindex', '-1');
5999
+ }
6000
+ }
6001
+ // Keep liList in sync so keyboard-navigation helpers stay functional.
6002
+ this.liList = liListBuf;
6003
+ };
6004
+ /**
6005
+ * Creates a single `<li>` element for the virtual window representing `node`.
6006
+ *
6007
+ * @param {VirtualNodeData} node - The node data.
6008
+ * @param {boolean} _isFirst - True if this is the first rendered item (unused here but kept for potential override).
6009
+ * @returns {HTMLElement} The constructed `<li>` element.
6010
+ * @private
6011
+ */
6012
+ TreeView.prototype.createVirtualListItem = function (node, _isFirst) {
6013
+ var li = this.createElement('li', { className: LISTITEM });
6014
+ // Level class for CSS indentation
6015
+ li.classList.add('e-level-' + node.level);
6016
+ li.setAttribute('data-uid', node.id);
6017
+ li.setAttribute('aria-level', node.level.toString());
6018
+ li.setAttribute('role', 'treeitem');
6019
+ li.setAttribute('tabindex', '-1');
6020
+ // Full-row overlay (required for fullRowSelect mode)
6021
+ if (this.fullRowSelect) {
6022
+ var fullRow = this.createElement('div', { className: FULLROW });
6023
+ li.appendChild(fullRow);
6024
+ }
6025
+ // Text/icon wrapper
6026
+ var textWrap = this.createElement('div', { className: TEXTWRAP });
6027
+ // Expand/collapse icon
6028
+ if (node.hasChildren) {
6029
+ var iconSpan = this.createElement('div', {
6030
+ className: ICON + ' ' + (node.isExpanded ? COLLAPSIBLE : EXPANDABLE)
6031
+ });
6032
+ textWrap.appendChild(iconSpan);
6033
+ }
6034
+ // Checkbox
6035
+ if (this.showCheckBox) {
6036
+ var checkWrap = createCheckBox(this.createElement, false, {
6037
+ checked: node.isChecked === true,
6038
+ cssClass: CHECKBOXWRAP
6039
+ });
6040
+ if (node.isChecked === 'indeterminate') {
6041
+ var frame = checkWrap.querySelector('.' + CHECKBOXFRAME);
6042
+ if (frame) {
6043
+ frame.classList.add(INDETERMINATE);
6044
+ }
6045
+ }
6046
+ textWrap.appendChild(checkWrap);
6047
+ }
6048
+ // Icon
6049
+ if (node.iconCss) {
6050
+ var iconEl = this.createElement('span', {
6051
+ className: ICON + ' ' + node.iconCss
6052
+ });
6053
+ textWrap.appendChild(iconEl);
6054
+ }
6055
+ // Image
6056
+ if (node.imageUrl) {
6057
+ var img = document.createElement('img');
6058
+ img.src = node.imageUrl;
6059
+ img.classList.add(IMAGE);
6060
+ textWrap.appendChild(img);
6061
+ }
6062
+ // Text anchor
6063
+ var anchor = this.createElement('span', {
6064
+ className: 'e-list-text',
6065
+ innerHTML: node.text
6066
+ });
6067
+ textWrap.appendChild(anchor);
6068
+ li.appendChild(textWrap);
6069
+ // State classes
6070
+ if (node.isExpanded) {
6071
+ li.classList.add(EXPANDED);
6072
+ li.setAttribute('aria-expanded', 'true');
6073
+ }
6074
+ else if (node.hasChildren) {
6075
+ li.classList.add(NODECOLLAPSED);
6076
+ li.setAttribute('aria-expanded', 'false');
6077
+ }
6078
+ if (node.isSelected) {
6079
+ li.classList.add(ACTIVE);
6080
+ li.setAttribute('aria-selected', 'true');
6081
+ }
6082
+ if (node.isDisabled) {
6083
+ li.classList.add(DISABLED);
6084
+ li.setAttribute('aria-disabled', 'true');
6085
+ }
6086
+ if (node.hasChildren) {
6087
+ li.classList.add(CHILD);
6088
+ }
6089
+ // Wire the focus event so `focusIn()` fires when the user Tabs into this
6090
+ // item. The non-virtualized path registers this in `beforeNodeCreate()`,
6091
+ // but virtual items never go through that callback, so we must register
6092
+ // it here to keep focus styling (e.g. `e-focused`) consistent between
6093
+ // virtual and non-virtual modes. `unWireEvents()` already removes this
6094
+ // handler during teardown by iterating over the rendered `<li>`s.
6095
+ EventHandler.add(li, 'focus', this.focusIn, this);
6096
+ return li;
6097
+ };
6098
+ /**
6099
+ * Initialises the virtual-scrolling layout:
6100
+ * 1. Adds the `e-virtualization` CSS class and applies a fixed `height` + `overflow-y`.
6101
+ * 2. Flattens all visible nodes into `virtualFlatNodes`.
6102
+ * 3. Measures item height and viewport height.
6103
+ * 4. Computes the initial window and renders it.
6104
+ * 5. Wires the scroll event handler.
6105
+ *
6106
+ * @returns {void}
6107
+ * @private
6108
+ */
6109
+ TreeView.prototype.initVirtualization = function () {
6110
+ addClass([this.element], VIRTUALIZATION);
6111
+ // Apply container height
6112
+ var px = this.parseHeightToPixels();
6113
+ if (px > 0) {
6114
+ this.element.style.height = px + 'px';
6115
+ this.element.style.overflowY = 'auto';
6116
+ }
6117
+ // Ensure the inner UL is ready
6118
+ if (!this.ulElement) {
6119
+ this.ulElement = select('.' + PARENTITEM, this.element);
6120
+ }
6121
+ if (!this.ulElement) {
6122
+ return;
6123
+ }
6124
+ // Make the UL position-relative for the absolute spacers
6125
+ this.ulElement.style.position = 'relative';
6126
+ // Rebuild flat list
6127
+ this.virtualFlatNodes = this.flattenVisibleNodes();
6128
+ this.rebuildVirtualNodeMap();
6129
+ // Measure dimensions
6130
+ this.virtualViewportHeight = px > 0 ? px : this.element.offsetHeight;
6131
+ // Render a single probe item to measure the real item height before the full render
6132
+ if (this.virtualFlatNodes.length > 0) {
6133
+ var probe = this.createVirtualListItem(this.virtualFlatNodes[0], true);
6134
+ this.ulElement.appendChild(probe);
6135
+ this.virtualItemHeight = probe.offsetHeight > 0 ? probe.offsetHeight : this.virtualItemHeight;
6136
+ this.ulElement.removeChild(probe);
6137
+ }
6138
+ // Compute window and render all items in the initial viewport
6139
+ var win = this.getVirtualWindow();
6140
+ this.virtualStartIndex = win.startIndex;
6141
+ this.virtualEndIndex = win.endIndex;
6142
+ this.renderVirtualItems();
6143
+ // Wire scroll
6144
+ this.wireVirtualScrollEvents();
6145
+ };
6146
+ /**
6147
+ * Wires the scroll event listener on the TreeView element.
6148
+ *
6149
+ * @returns {void}
6150
+ * @private
6151
+ */
6152
+ TreeView.prototype.wireVirtualScrollEvents = function () {
6153
+ this.unwireVirtualScrollEvents();
6154
+ this.virtualScrollBound = this.onVirtualScroll.bind(this);
6155
+ this.element.addEventListener('scroll', this.virtualScrollBound, { passive: true });
6156
+ };
6157
+ /**
6158
+ * Removes the scroll event listener previously wired by `wireVirtualScrollEvents`.
6159
+ *
6160
+ * @returns {void}
6161
+ * @private
6162
+ */
6163
+ TreeView.prototype.unwireVirtualScrollEvents = function () {
6164
+ if (this.virtualScrollBound) {
6165
+ this.element.removeEventListener('scroll', this.virtualScrollBound);
6166
+ this.virtualScrollBound = null;
6167
+ }
6168
+ if (this.virtualScrollRAF !== null) {
6169
+ cancelAnimationFrame(this.virtualScrollRAF);
6170
+ this.virtualScrollRAF = null;
6171
+ }
6172
+ };
6173
+ /**
6174
+ * Scroll handler for the virtual TreeView. Throttled via `requestAnimationFrame`.
6175
+ * Shows the loading mask during fast scrolls, re-renders the window, then hides the mask.
6176
+ *
6177
+ * @returns {void}
6178
+ * @private
6179
+ */
6180
+ TreeView.prototype.onVirtualScroll = function () {
6181
+ var _this = this;
6182
+ if (this.virtualScrollRAF !== null) {
6183
+ cancelAnimationFrame(this.virtualScrollRAF);
6184
+ }
6185
+ // ── Tab-induced focus guard ────────────────────────────────────────────────
6186
+ // When the user Tabs into a `<li tabindex="0">` that is in the middle of
6187
+ // the treeview, the browser auto-scrolls the container so the focused
6188
+ // element becomes visible. That fires a `scroll` event here, but the
6189
+ // virtual window already contains the focused element — re-rendering
6190
+ // would destroy the focused `<li>` and break focus. Detect this case
6191
+ // via the flag set in `keyDownHandler` and skip the re-render entirely.
6192
+ if (this.virtualSkipNextScrollRender) {
6193
+ this.virtualSkipNextScrollRender = false;
6194
+ // Update prev scrollTop so a subsequent user-driven scroll still
6195
+ // calculates the correct delta.
6196
+ this.virtualPrevScrollTop = this.element.scrollTop;
6197
+ return;
6198
+ }
6199
+ var currentScrollTop = this.element.scrollTop;
6200
+ var delta = Math.abs(currentScrollTop - this.virtualPrevScrollTop);
6201
+ var fastScrollThreshold = this.virtualViewportHeight * 2;
6202
+ if (delta > fastScrollThreshold) {
6203
+ this.showVirtualMask(currentScrollTop);
6204
+ }
6205
+ this.virtualScrollRAF = requestAnimationFrame(function () {
6206
+ _this.virtualScrollRAF = null;
6207
+ _this.virtualPrevScrollTop = _this.element.scrollTop;
6208
+ var win = _this.getVirtualWindow();
6209
+ _this.virtualStartIndex = win.startIndex;
6210
+ _this.virtualEndIndex = win.endIndex;
6211
+ _this.renderVirtualItems();
6212
+ // Hide the skeleton mask after a short delay so it stays visible long enough
6213
+ // for the user to perceive the loading indicator even on short scrolls.
6214
+ setTimeout(function () {
6215
+ _this.hideVirtualMask();
6216
+ // Keep the (now hidden) mask's coords fresh for the next show.
6217
+ _this.updateMaskPosition();
6218
+ }, 200);
6219
+ });
6220
+ };
6221
+ /**
6222
+ * Creates the skeleton mask overlay used to indicate loading during fast scrolls.
6223
+ * Skeleton instances are stored in `skeletonInstances` for later disposal.
6224
+ *
6225
+ * @returns {void}
6226
+ * @private
6227
+ */
6228
+ TreeView.prototype.createVirtualMask = function () {
6229
+ if (this.virtualMaskElement) {
6230
+ return;
6231
+ }
6232
+ // Use position:fixed so the mask is anchored to the visible viewport (not to the
6233
+ // scrolling container). Width/height/left/top are set/refreshed by
6234
+ // updateMaskPosition() so the mask always covers the TreeView's visible area
6235
+ // regardless of how the page itself is scrolled.
6236
+ var rect = this.element.getBoundingClientRect();
6237
+ var scrollBarWidth = this.element.offsetWidth - this.element.clientWidth;
6238
+ var maskParent = this.createElement('div', {
6239
+ className: VIRTUAL_MASK_PARENT,
6240
+ styles: "position:fixed;top:" + rect.top + "px;left:" + rect.left + "px;width:" + (rect.width - scrollBarWidth) + "px;height:" + rect.height + "px;z-index:10;overflow:hidden;background:#fff;pointer-events:none;display:none;"
6241
+ });
6242
+ var visibleRows = Math.ceil(this.virtualViewportHeight / this.virtualItemHeight) + 1;
6243
+ for (var r = 0; r < visibleRows; r++) {
6244
+ var maskItem = this.createElement('div', {
6245
+ className: VIRTUAL_MASK_ITEM,
6246
+ styles: "display:flex;align-items:center;height:" + this.virtualItemHeight + "px;padding:0 12px;gap:8px;"
6247
+ });
6248
+ // Skeleton circle for icon placeholder
6249
+ // const iconHolder: HTMLElement = this.createElement('span', {
6250
+ // styles: `width:20px;height:20px;`
6251
+ // });
6252
+ // maskItem.appendChild(iconHolder);
6253
+ // const iconSkel: Skeleton = new Skeleton({
6254
+ // shape: 'Circle',
6255
+ // width: '20px',
6256
+ // height: '20px',
6257
+ // cssClass: 'e-virtual-skel-icon'
6258
+ // });
6259
+ // iconSkel.appendTo(iconHolder);
6260
+ // this.skeletonInstances.push(iconSkel);
6261
+ // Skeleton text bar
6262
+ var textHolder = this.createElement('span');
6263
+ textHolder.style.flex = '1';
6264
+ textHolder.style.height = '14px';
6265
+ maskItem.appendChild(textHolder);
6266
+ var textSkel = new Skeleton({
6267
+ shape: 'Text',
6268
+ width: '80%',
6269
+ height: '14px',
6270
+ cssClass: 'e-virtual-skel-text'
6271
+ });
6272
+ textSkel.appendTo(textHolder);
6273
+ this.skeletonInstances.push(textSkel);
6274
+ maskParent.appendChild(maskItem);
6275
+ }
6276
+ this.virtualMaskElement = maskParent;
6277
+ this.element.appendChild(maskParent);
6278
+ };
6279
+ /**
6280
+ * Refreshes the mask position so it always overlays the TreeView's viewport.
6281
+ * Called on every show / scroll, since `position:fixed` is relative to the
6282
+ * page viewport, not to the TreeView scroll container.
6283
+ *
6284
+ * @returns {void}
6285
+ * @private
6286
+ */
6287
+ TreeView.prototype.updateMaskPosition = function () {
6288
+ if (!this.virtualMaskElement) {
6289
+ return;
6290
+ }
6291
+ var rect = this.element.getBoundingClientRect();
6292
+ var scrollBarWidth = this.element.offsetWidth - this.element.clientWidth;
6293
+ this.virtualMaskElement.style.top = rect.top + 'px';
6294
+ this.virtualMaskElement.style.left = rect.left + 'px';
6295
+ this.virtualMaskElement.style.width = (rect.width - scrollBarWidth) + 'px';
6296
+ this.virtualMaskElement.style.height = rect.height + 'px';
6297
+ };
6298
+ /**
6299
+ * Destroys all skeleton instances and removes the mask overlay from the DOM.
6300
+ *
6301
+ * @returns {void}
6302
+ * @private
6303
+ */
6304
+ TreeView.prototype.destroyVirtualMask = function () {
6305
+ for (var i = this.skeletonInstances.length - 1; i >= 0; i--) {
6306
+ this.skeletonInstances[parseInt(i.toString(), 10)].destroy();
6307
+ }
6308
+ this.skeletonInstances = [];
6309
+ if (this.virtualMaskElement) {
6310
+ if (this.virtualMaskElement.parentElement) {
6311
+ this.virtualMaskElement.parentElement.removeChild(this.virtualMaskElement);
6312
+ }
6313
+ this.virtualMaskElement = null;
6314
+ }
6315
+ };
6316
+ /**
6317
+ * Positions and shows the skeleton mask at the current `scrollTop` position.
6318
+ *
6319
+ * @param {number} _scrollTop - Current scrollTop of the container.
6320
+ * @returns {void}
6321
+ * @private
6322
+ */
6323
+ TreeView.prototype.showVirtualMask = function (_scrollTop) {
6324
+ if (!this.virtualMaskElement) {
6325
+ this.createVirtualMask();
6326
+ }
6327
+ if (this.virtualMaskElement) {
6328
+ // Refresh the fixed-position coords in case the TreeView was moved/resized
6329
+ // since the last time the mask was shown.
6330
+ this.updateMaskPosition();
6331
+ this.virtualMaskElement.style.display = 'block';
6332
+ // Visually hide the real list while the skeleton is shown
6333
+ if (this.ulElement) {
6334
+ this.ulElement.style.visibility = 'hidden';
6335
+ }
6336
+ }
6337
+ };
6338
+ /**
6339
+ * Hides the skeleton mask overlay after the virtual window has been rendered.
6340
+ *
6341
+ * @returns {void}
6342
+ * @private
6343
+ */
6344
+ TreeView.prototype.hideVirtualMask = function () {
6345
+ if (this.virtualMaskElement) {
6346
+ this.virtualMaskElement.style.display = 'none';
6347
+ // Restore real list visibility
6348
+ if (this.ulElement) {
6349
+ this.ulElement.style.visibility = '';
6350
+ }
6351
+ }
6352
+ };
6353
+ /**
6354
+ * Ensures the node identified by `id` is scrolled into view inside the virtual container.
6355
+ * If the node falls outside the rendered window, the container's `scrollTop` is adjusted so
6356
+ * that the node becomes visible, the window is re-computed and `renderVirtualItems` is called.
6357
+ *
6358
+ * @param {string} id - The `data-uid` of the target node.
6359
+ * @returns {void}
6360
+ * @private
6361
+ */
6362
+ TreeView.prototype.ensureVirtualNodeVisible = function (id) {
6363
+ var idx = this.virtualNodeMap.get(id);
6364
+ if (isNOU(idx)) {
6365
+ return;
6366
+ }
6367
+ var itemH = this.virtualItemHeight;
6368
+ var vpH = this.virtualViewportHeight;
6369
+ var nodeTop = idx * itemH;
6370
+ var nodeBottom = nodeTop + itemH;
6371
+ var currentTop = this.element.scrollTop;
6372
+ var currentBottom = currentTop + vpH;
6373
+ if (nodeTop < currentTop) {
6374
+ this.element.scrollTop = nodeTop;
6375
+ }
6376
+ else if (nodeBottom > currentBottom) {
6377
+ this.element.scrollTop = nodeBottom - vpH;
6378
+ }
6379
+ else {
6380
+ return; // already visible, no re-render needed
6381
+ }
6382
+ // Re-render the new window synchronously so the <li> is in the DOM
6383
+ var win = this.getVirtualWindow();
6384
+ this.virtualStartIndex = win.startIndex;
6385
+ this.virtualEndIndex = win.endIndex;
6386
+ this.renderVirtualItems();
6387
+ };
6388
+ TreeView.prototype.handleVirtualExpand = function (id, isKeyboardNav) {
6389
+ // ── Cancel any pending scroll RAF ─────────────────────────────────────────
6390
+ // A scroll RAF queued before the expand click would run asynchronously after
6391
+ // this synchronous expand completes, re-rendering the window from a stale
6392
+ // scrollTop and moving the parent node to the wrong position. Cancel it now
6393
+ // so the expand render is authoritative.
6394
+ if (this.virtualScrollRAF !== null) {
6395
+ cancelAnimationFrame(this.virtualScrollRAF);
6396
+ this.virtualScrollRAF = null;
6397
+ }
6398
+ if (this.expandedNodes.indexOf(id) === -1) {
6399
+ this.expandedNodes.push(id);
6400
+ }
6401
+ // Also update virtualExpandedNodesSet to keep it in sync
6402
+ if (this.virtualExpandedNodesSet) {
6403
+ this.virtualExpandedNodesSet.add(id);
6404
+ }
6405
+ if (this.rootData) {
6406
+ var acutalRootData = this.rootData.find(function (item) { return item.id === parseInt(id, 10); });
6407
+ acutalRootData.isExpanded = true;
6408
+ }
6409
+ // ── Fix: Pin focused ID to the expanding node for keyboard navigation ──
6410
+ // When keyboard-triggered, the expanded node IS definitively the focused node
6411
+ // (`id` comes from currLi.getAttribute('data-uid') in expandNode, where currLi
6412
+ // is getFocusedNode()). Set unconditionally — the old conditional guard
6413
+ // `!this.virtualFocusedId` could leave a stale or null value if a previous
6414
+ // RAF render (from arrow-key scroll) hadn't yet completed, causing branch 3
6415
+ // (first item) to fire in renderVirtualItems() instead of branch 2.
6416
+ // For mouse clicks (isKeyboardNav=false), clear virtualFocusedId so that
6417
+ // renderVirtualItems() Branch 2 does NOT apply the FOCUS/FOCUSED classes —
6418
+ // mouse-based expand/collapse should never add the e-focused class.
6419
+ if (isKeyboardNav) {
6420
+ this.virtualFocusedId = id;
6421
+ }
6422
+ else {
6423
+ this.virtualFocusedId = null;
6424
+ }
6425
+ this.virtualFlatNodes = this.flattenVisibleNodes();
6426
+ this.rebuildVirtualNodeMap();
6427
+ // ── Snapshot scrollTop before computing the window ────────────────────────
6428
+ // Read scrollTop once here and pass it into getVirtualWindow so that any
6429
+ // browser scroll-anchoring adjustment that happens when the padding values
6430
+ // change cannot affect which window we render.
6431
+ var scrollTopSnapshot = this.element.scrollTop;
6432
+ var win = this.getVirtualWindowAt(scrollTopSnapshot);
6433
+ this.virtualStartIndex = win.startIndex;
6434
+ this.virtualEndIndex = win.endIndex;
6435
+ // ── Block ALL scroll-triggered re-renders during this expand ─────────────
6436
+ // renderVirtualItems() calls li.focus() in branch 2 (keyboard focus restore).
6437
+ // The post-render focus() call below can also trigger browser scroll. Both
6438
+ // can fire the 'scroll' event and cause onVirtualScroll() to queue a RAF.
6439
+ // We set the skip flag ONCE here; it is consumed by the first scroll event.
6440
+ // To cover the second potential scroll (from the focus() call below) we use
6441
+ // preventScroll:true on that call so it never moves the container at all.
6442
+ this.virtualSkipNextScrollRender = true;
6443
+ this.renderVirtualItems();
6444
+ // renderVirtualItems() replaces all <li> elements so the old icon element that
6445
+ // had the PROCESS class is gone. Remove PROCESS from the newly rendered icon
6446
+ // so the node can be collapsed/expanded again without being blocked.
6447
+ var newLi = this.getElement(id);
6448
+ if (newLi) {
6449
+ var icon = select('.' + ICON, newLi);
6450
+ if (icon) {
6451
+ removeClass([icon], PROCESS);
6452
+ }
6453
+ // ── Use preventScroll:true ────────────────────────────────────────────
6454
+ // Without preventScroll the browser scrolls the container to bring this
6455
+ // node into view, which fires a second 'scroll' event. The first scroll
6456
+ // was already consumed by virtualSkipNextScrollRender (set above), so the
6457
+ // second scroll would queue a new RAF that re-renders from the
6458
+ // focus-adjusted scrollTop — moving the parent node to the wrong position.
6459
+ newLi.focus({ preventScroll: true });
6460
+ }
6461
+ if (this.isLoaded) {
6462
+ var currLi = this.getElement(id);
6463
+ if (currLi) {
6464
+ var expandArgs = this.getExpandEvent(currLi, null);
6465
+ expandArgs.isInteracted = this.isInteracted;
6466
+ this.trigger('nodeExpanded', expandArgs);
6467
+ }
6468
+ }
6469
+ };
6470
+ TreeView.prototype.handleVirtualCollapse = function (id, isKeyboardNav) {
6471
+ // ── Cancel any pending scroll RAF ─────────────────────────────────────────
6472
+ // Mirrors handleVirtualExpand: a queued RAF from a previous scroll event
6473
+ // must not override the collapse render with a stale window.
6474
+ if (this.virtualScrollRAF !== null) {
6475
+ cancelAnimationFrame(this.virtualScrollRAF);
6476
+ this.virtualScrollRAF = null;
6477
+ }
6478
+ var idx = this.expandedNodes.indexOf(id);
6479
+ if (idx !== -1) {
6480
+ this.expandedNodes.splice(idx, 1);
6481
+ }
6482
+ // Also update virtualExpandedNodesSet to keep it in sync
6483
+ if (this.virtualExpandedNodesSet) {
6484
+ this.virtualExpandedNodesSet.delete(id);
6485
+ }
6486
+ if (this.rootData) {
6487
+ var acutalRootData = this.rootData.find(function (item) { return item.id === parseInt(id, 10); });
6488
+ acutalRootData.isExpanded = false;
6489
+ }
6490
+ // ── Fix: Pin focused ID to the collapsing node for keyboard navigation ──
6491
+ // Same reasoning as handleVirtualExpand: when keyboard-triggered, `id` is
6492
+ // definitively the focused node. Set unconditionally to avoid the stale/null
6493
+ // state that could cause the RAF re-render to fall through to branch 3.
6494
+ // For mouse clicks (isKeyboardNav=false), clear virtualFocusedId so that
6495
+ // renderVirtualItems() Branch 2 does NOT apply the FOCUS/FOCUSED classes —
6496
+ // mouse-based expand/collapse should never add the e-focused class.
6497
+ if (isKeyboardNav) {
6498
+ this.virtualFocusedId = id;
6499
+ }
6500
+ else {
6501
+ this.virtualFocusedId = null;
6502
+ }
6503
+ this.virtualFlatNodes = this.flattenVisibleNodes();
6504
+ this.rebuildVirtualNodeMap();
6505
+ // ── Snapshot scrollTop before computing the window ────────────────────────
6506
+ // Same rationale as handleVirtualExpand: capture scrollTop before any focus()
6507
+ // call or padding change can cause the browser to auto-scroll.
6508
+ var scrollTopSnapshot = this.element.scrollTop;
6509
+ var win = this.getVirtualWindowAt(scrollTopSnapshot);
6510
+ this.virtualStartIndex = win.startIndex;
6511
+ this.virtualEndIndex = win.endIndex;
6512
+ // ── Block ALL scroll-triggered re-renders during this collapse ────────────
6513
+ // Same as handleVirtualExpand: use preventScroll:true on the focus() call
6514
+ // below so the second potential scroll event is never fired.
6515
+ this.virtualSkipNextScrollRender = true;
6516
+ this.renderVirtualItems();
6517
+ // renderVirtualItems() replaces all <li> elements so the old icon element that
6518
+ // had the PROCESS class is gone. Remove PROCESS from the newly rendered icon
6519
+ // so the node can be expanded/collapsed again without being blocked.
6520
+ var newLi = this.getElement(id);
6521
+ if (newLi) {
6522
+ var icon = select('.' + ICON, newLi);
6523
+ if (icon) {
6524
+ removeClass([icon], PROCESS);
6525
+ }
6526
+ // ── Use preventScroll:true to avoid consuming virtualSkipNextScrollRender ─
6527
+ newLi.focus({ preventScroll: true });
6528
+ }
6529
+ if (this.isLoaded) {
6530
+ var currLi = this.getElement(id);
6531
+ if (currLi) {
6532
+ var colArgs = this.getExpandEvent(currLi, null);
6533
+ colArgs.isInteracted = this.isInteracted;
6534
+ this.trigger('nodeCollapsed', colArgs);
6535
+ }
6536
+ }
6537
+ };
6538
+ /**
6539
+ * Updates the selected state of a node in `virtualFlatNodes` and re-renders the window.
6540
+ *
6541
+ * @param {string} id - The `data-uid` of the target node.
6542
+ * @param {boolean} isSelected - Whether the node is now selected.
6543
+ * @returns {void}
6544
+ * @private
6545
+ */
6546
+ TreeView.prototype.updateVirtualNodeSelection = function (id, isSelected) {
6547
+ var idx = this.virtualNodeMap.get(id);
6548
+ if (!isNOU(idx)) {
6549
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].isSelected = isSelected;
6550
+ this.renderVirtualItems();
6551
+ }
6552
+ };
6553
+ /**
6554
+ * Updates the checkbox state of a node in `virtualFlatNodes` and re-renders the window.
6555
+ *
6556
+ * @param {string} id - The `data-uid` of the target node.
6557
+ * @param {'check' | 'uncheck' | 'indeterminate'} state - The new check state.
6558
+ * @returns {void}
6559
+ * @private
6560
+ */
6561
+ TreeView.prototype.updateVirtualNodeCheckState = function (id, state) {
6562
+ var idx = this.virtualNodeMap.get(id);
6563
+ if (!isNOU(idx)) {
6564
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].isChecked =
6565
+ state === 'check' ? true : state === 'indeterminate' ? 'indeterminate' : false;
6566
+ // Keep the persistent indeterminate set in sync so flattenVisibleNodes()
6567
+ // correctly restores state after any full list rebuild (e.g. collapse).
6568
+ if (state === 'indeterminate') {
6569
+ this.virtualIndeterminateNodesSet.add(id);
6570
+ }
6571
+ else {
6572
+ this.virtualIndeterminateNodesSet.delete(id);
6573
+ }
6574
+ this.renderVirtualItems();
6575
+ }
6576
+ };
6577
+ // ═══════════════════════════════════════════════════════════════════════════════
6578
+ // END VIRTUALIZATION
6579
+ // ═══════════════════════════════════════════════════════════════════════════════
5198
6580
  /**
5199
6581
  * Called internally if any of the property value changed.
5200
6582
  *
@@ -5332,6 +6714,33 @@ var TreeView = /** @class */ (function (_super) {
5332
6714
  this.listBaseOption.itemNavigable = newProp.fullRowNavigable;
5333
6715
  this.reRenderNodes();
5334
6716
  break;
6717
+ case 'enableVirtualization':
6718
+ if (this.enableVirtualization) {
6719
+ if (!isNOU(this.height)) {
6720
+ this.initVirtualization();
6721
+ }
6722
+ }
6723
+ else {
6724
+ // Tear down virtualization and do a full re-render
6725
+ this.unwireVirtualScrollEvents();
6726
+ this.destroyVirtualMask();
6727
+ removeClass([this.element], VIRTUALIZATION);
6728
+ this.element.style.height = '';
6729
+ this.element.style.overflowY = '';
6730
+ this.reRenderNodes();
6731
+ }
6732
+ break;
6733
+ case 'height':
6734
+ if (this.enableVirtualization) {
6735
+ // Re-initialise so the new height takes effect
6736
+ this.unwireVirtualScrollEvents();
6737
+ this.initVirtualization();
6738
+ }
6739
+ else {
6740
+ var px = this.parseHeightToPixels();
6741
+ this.element.style.height = px > 0 ? px + 'px' : '';
6742
+ }
6743
+ break;
5335
6744
  }
5336
6745
  }
5337
6746
  };
@@ -5341,6 +6750,13 @@ var TreeView = /** @class */ (function (_super) {
5341
6750
  * @returns {void}
5342
6751
  */
5343
6752
  TreeView.prototype.destroy = function () {
6753
+ // Tear down virtualization before standard cleanup
6754
+ if (this.enableVirtualization) {
6755
+ this.unwireVirtualScrollEvents();
6756
+ this.destroyVirtualMask();
6757
+ this.virtualFlatNodes = [];
6758
+ this.virtualNodeMap.clear();
6759
+ }
5344
6760
  this.clearTemplate();
5345
6761
  this.element.removeAttribute('aria-activedescendant');
5346
6762
  this.unWireEvents();
@@ -5425,9 +6841,25 @@ var TreeView = /** @class */ (function (_super) {
5425
6841
  if (isNOU(fNode)) {
5426
6842
  this.setNodeFocusable();
5427
6843
  }
5428
- this.updateCheckedStateFromDS();
5429
- if (this.showCheckBox && dropLi && this.autoCheck) {
5430
- this.ensureParentCheckState(dropLi);
6844
+ // In virtual mode (with a fixed height), rebuild the flat node list and re-render
6845
+ // the window so the newly added nodes appear in the correct hierarchical position.
6846
+ // When height is not provided, the tree uses normal rendering and the DOM is
6847
+ // already updated by addGivenNodes() above.
6848
+ if (this.enableVirtualization && !isNOU(this.height)) {
6849
+ this.setRootData();
6850
+ this.rebuildVirtualCheckCache();
6851
+ this.virtualFlatNodes = this.flattenVisibleNodes();
6852
+ this.rebuildVirtualNodeMap();
6853
+ var win = this.getVirtualWindow();
6854
+ this.virtualStartIndex = win.startIndex;
6855
+ this.virtualEndIndex = win.endIndex;
6856
+ this.renderVirtualItems();
6857
+ }
6858
+ else {
6859
+ this.updateCheckedStateFromDS();
6860
+ if (this.showCheckBox && dropLi && this.autoCheck) {
6861
+ this.ensureParentCheckState(dropLi);
6862
+ }
5431
6863
  }
5432
6864
  if ((this.fields.dataSource instanceof DataManager === false)) {
5433
6865
  this.preventExpand = false;
@@ -5472,6 +6904,25 @@ var TreeView = /** @class */ (function (_super) {
5472
6904
  * @returns {void}
5473
6905
  */
5474
6906
  TreeView.prototype.collapseAll = function (nodes, level, excludeHiddenNodes) {
6907
+ var _this = this;
6908
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
6909
+ this.virtualExpandedNodesSet.clear();
6910
+ this.setProperties({ expandedNodes: [] }, true);
6911
+ var setAllCollapsed_1 = function (nodes) {
6912
+ if (!nodes) {
6913
+ return;
6914
+ }
6915
+ for (var i = 0; i < nodes.length; i++) {
6916
+ var node = nodes[parseInt(i.toString(), 10)];
6917
+ node['isExpanded'] = false;
6918
+ var children = getValue(_this.fields.child.toString(), node);
6919
+ if (children && Array.isArray(children)) {
6920
+ setAllCollapsed_1(children);
6921
+ }
6922
+ }
6923
+ };
6924
+ setAllCollapsed_1(this.treeData);
6925
+ }
5475
6926
  if (!isNOU(nodes)) {
5476
6927
  this.doGivenAction(nodes, COLLAPSIBLE, false);
5477
6928
  }
@@ -5549,6 +7000,33 @@ var TreeView = /** @class */ (function (_super) {
5549
7000
  */
5550
7001
  TreeView.prototype.expandAll = function (nodes, level, excludeHiddenNodes, preventAnimation) {
5551
7002
  this.isAnimate = !preventAnimation;
7003
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
7004
+ var allParentIds_1 = [];
7005
+ var idFieldName_1 = this.fields.id;
7006
+ var childFieldName = this.fields.child.toString();
7007
+ var isDataTypeFlat = this.dataType === 1;
7008
+ var processAllParents = function (nodeList) {
7009
+ if (!nodeList) {
7010
+ return;
7011
+ }
7012
+ for (var i = 0; i < nodeList.length; i++) {
7013
+ var node = nodeList[parseInt(i.toString(), 10)];
7014
+ var nodeId = getValue(idFieldName_1, node);
7015
+ if (node.hasChild) {
7016
+ allParentIds_1.push(nodeId);
7017
+ node.isExpanded = true;
7018
+ }
7019
+ }
7020
+ };
7021
+ processAllParents(this.treeData);
7022
+ for (var i = 0; i < allParentIds_1.length; i++) {
7023
+ var id = allParentIds_1[parseInt(i.toString(), 10)];
7024
+ if (!this.virtualExpandedNodesSet.has(id)) {
7025
+ this.virtualExpandedNodesSet.add(id);
7026
+ }
7027
+ }
7028
+ this.setProperties({ expandedNodes: allParentIds_1 }, true);
7029
+ }
5552
7030
  if (!isNOU(nodes)) {
5553
7031
  this.doGivenAction(nodes, EXPANDABLE, true);
5554
7032
  }
@@ -5851,6 +7329,12 @@ var TreeView = /** @class */ (function (_super) {
5851
7329
  __decorate([
5852
7330
  Property(false)
5853
7331
  ], TreeView.prototype, "allowTextWrap", void 0);
7332
+ __decorate([
7333
+ Property(false)
7334
+ ], TreeView.prototype, "enableVirtualization", void 0);
7335
+ __decorate([
7336
+ Property()
7337
+ ], TreeView.prototype, "height", void 0);
5854
7338
  __decorate([
5855
7339
  Complex({}, NodeAnimationSettings)
5856
7340
  ], TreeView.prototype, "animation", void 0);