@syncfusion/ej2-navigations 34.1.31 → 34.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.
Files changed (79) hide show
  1. package/dist/ej2-navigations.min.js +2 -2
  2. package/dist/ej2-navigations.umd.min.js +2 -2
  3. package/dist/ej2-navigations.umd.min.js.map +1 -1
  4. package/dist/es6/ej2-navigations.es2015.js +1591 -20
  5. package/dist/es6/ej2-navigations.es2015.js.map +1 -1
  6. package/dist/es6/ej2-navigations.es5.js +1593 -20
  7. package/dist/es6/ej2-navigations.es5.js.map +1 -1
  8. package/dist/global/ej2-navigations.min.js +2 -2
  9. package/dist/global/ej2-navigations.min.js.map +1 -1
  10. package/dist/global/index.d.ts +1 -1
  11. package/package.json +8 -7
  12. package/src/common/menu-base.js +14 -6
  13. package/src/treeview/treeview-model.d.ts +27 -1
  14. package/src/treeview/treeview.d.ts +302 -0
  15. package/src/treeview/treeview.js +1579 -14
  16. package/styles/bds-lite.css +3 -0
  17. package/styles/bds.css +3 -0
  18. package/styles/bootstrap-dark-lite.css +3 -0
  19. package/styles/bootstrap-dark.css +3 -0
  20. package/styles/bootstrap-lite.css +3 -0
  21. package/styles/bootstrap.css +3 -0
  22. package/styles/bootstrap4-lite.css +3 -0
  23. package/styles/bootstrap4.css +3 -0
  24. package/styles/bootstrap5-dark-lite.css +3 -0
  25. package/styles/bootstrap5-dark.css +3 -0
  26. package/styles/bootstrap5-lite.css +3 -0
  27. package/styles/bootstrap5.3-lite.css +3 -0
  28. package/styles/bootstrap5.3.css +3 -0
  29. package/styles/bootstrap5.css +3 -0
  30. package/styles/fabric-dark-lite.css +3 -0
  31. package/styles/fabric-dark.css +3 -0
  32. package/styles/fabric-lite.css +3 -0
  33. package/styles/fabric.css +3 -0
  34. package/styles/fluent-dark-lite.css +3 -0
  35. package/styles/fluent-dark.css +3 -0
  36. package/styles/fluent-lite.css +3 -0
  37. package/styles/fluent.css +3 -0
  38. package/styles/fluent2-lite.css +3 -0
  39. package/styles/fluent2.css +3 -0
  40. package/styles/highcontrast-light-lite.css +3 -0
  41. package/styles/highcontrast-light.css +3 -0
  42. package/styles/highcontrast-lite.css +3 -0
  43. package/styles/highcontrast.css +3 -0
  44. package/styles/material-dark-lite.css +3 -0
  45. package/styles/material-dark.css +3 -0
  46. package/styles/material-lite.css +3 -0
  47. package/styles/material.css +3 -0
  48. package/styles/material3-dark-lite.css +3 -0
  49. package/styles/material3-dark.css +3 -0
  50. package/styles/material3-lite.css +3 -0
  51. package/styles/material3.css +3 -0
  52. package/styles/tailwind-dark-lite.css +3 -0
  53. package/styles/tailwind-dark.css +3 -0
  54. package/styles/tailwind-lite.css +3 -0
  55. package/styles/tailwind.css +3 -0
  56. package/styles/tailwind3-lite.css +3 -0
  57. package/styles/tailwind3.css +3 -0
  58. package/styles/toolbar/_layout.scss +4 -0
  59. package/styles/toolbar/bds.css +3 -0
  60. package/styles/toolbar/bootstrap-dark.css +3 -0
  61. package/styles/toolbar/bootstrap.css +3 -0
  62. package/styles/toolbar/bootstrap4.css +3 -0
  63. package/styles/toolbar/bootstrap5-dark.css +3 -0
  64. package/styles/toolbar/bootstrap5.3.css +3 -0
  65. package/styles/toolbar/bootstrap5.css +3 -0
  66. package/styles/toolbar/fabric-dark.css +3 -0
  67. package/styles/toolbar/fabric.css +3 -0
  68. package/styles/toolbar/fluent-dark.css +3 -0
  69. package/styles/toolbar/fluent.css +3 -0
  70. package/styles/toolbar/fluent2.css +3 -0
  71. package/styles/toolbar/highcontrast-light.css +3 -0
  72. package/styles/toolbar/highcontrast.css +3 -0
  73. package/styles/toolbar/material-dark.css +3 -0
  74. package/styles/toolbar/material.css +3 -0
  75. package/styles/toolbar/material3-dark.css +3 -0
  76. package/styles/toolbar/material3.css +3 -0
  77. package/styles/toolbar/tailwind-dark.css +3 -0
  78. package/styles/toolbar/tailwind.css +3 -0
  79. package/styles/toolbar/tailwind3.css +3 -0
@@ -4,6 +4,7 @@ import { getScrollableParent, Popup, getZindexPartial, fit, isCollide, calculate
4
4
  import { Button, createCheckBox, rippleMouseHandler } from '@syncfusion/ej2-buttons';
5
5
  import { DataManager, Query } from '@syncfusion/ej2-data';
6
6
  import { Input } from '@syncfusion/ej2-inputs';
7
+ import { Skeleton } from '@syncfusion/ej2-notifications';
7
8
 
8
9
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
9
10
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -1429,8 +1430,10 @@ let MenuBase = class MenuBase extends Component {
1429
1430
  if (!active) {
1430
1431
  return;
1431
1432
  }
1433
+ const isInputElement = active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' ||
1434
+ active.contentEditable === 'true';
1432
1435
  const expanded = active.getAttribute('aria-expanded') === 'true';
1433
- if (!expanded) {
1436
+ if (!expanded || isInputElement) {
1434
1437
  return;
1435
1438
  }
1436
1439
  const ul = this.element;
@@ -1544,13 +1547,19 @@ let MenuBase = class MenuBase extends Component {
1544
1547
  const cli = cul.children[fliIdx];
1545
1548
  fliIdx = this.isValidLI(cli, fliIdx, e.action);
1546
1549
  cul.children[fliIdx].classList.add(FOCUSED);
1547
- cul.children[fliIdx].focus();
1550
+ const liToFocus = cul.children[fliIdx];
1548
1551
  if (closest(this.element, '.e-popup')) {
1549
- const focusedLiItem = cul.children[fliIdx];
1550
- const focusableChild = focusedLiItem.querySelector('[tabindex], span[tabindex], a, button, [tabindex]:not([tabindex="-1"])');
1551
- if (!focusableChild) {
1552
- cul.focus();
1552
+ const focusableChild = liToFocus.querySelector('[tabindex], span[tabindex], a, button, [tabindex]:not([tabindex="-1"])');
1553
+ const isSpreadsheet = this.element.id.includes('_cf_menu') && this.element.parentElement.classList.contains('e-cf-menu');
1554
+ if (focusableChild && !isSpreadsheet) {
1555
+ focusableChild.focus();
1553
1556
  }
1557
+ else {
1558
+ liToFocus.focus();
1559
+ }
1560
+ }
1561
+ else {
1562
+ liToFocus.focus();
1554
1563
  }
1555
1564
  }
1556
1565
  isValidLI(cli, index, action) {
@@ -10563,6 +10572,9 @@ const CHILD = 'e-has-child';
10563
10572
  const ITEM_ANIMATION_ACTIVE = 'e-animation-active';
10564
10573
  const DISABLED$1 = 'e-disabled';
10565
10574
  const PREVENTSELECT = 'e-prevent';
10575
+ const VIRTUALIZATION = 'e-virtualization';
10576
+ const VIRTUAL_MASK_PARENT = 'e-mask-parent';
10577
+ const VIRTUAL_MASK_ITEM = 'e-mask-item';
10566
10578
  const treeAriaAttr = {
10567
10579
  treeRole: 'group',
10568
10580
  itemRole: 'treeitem',
@@ -10686,6 +10698,55 @@ let TreeView = TreeView_1 = class TreeView extends Component {
10686
10698
  this.isDropIn = false;
10687
10699
  this.OldCheckedData = [];
10688
10700
  this.isHiddenItem = false;
10701
+ // ── Virtualization state ──────────────────────────────────────────────────────────────
10702
+ /** Flat ordered list of all currently-visible (non-collapsed) tree nodes. */
10703
+ this.virtualFlatNodes = [];
10704
+ /** Maps node id → index in virtualFlatNodes for O(1) lookups. */
10705
+ this.virtualNodeMap = new Map();
10706
+ /** Set of expanded node IDs for O(1) lookup performance during flattening (avoids array indexOf). */
10707
+ this.virtualExpandedNodesSet = new Set();
10708
+ /** Set of checked node IDs for O(1) lookup performance during flattening. */
10709
+ this.virtualCheckedNodesSet = new Set();
10710
+ /** Set of indeterminate node IDs — persists across flattenVisibleNodes() rebuilds. */
10711
+ this.virtualIndeterminateNodesSet = new Set();
10712
+ /** Set of selected node IDs for O(1) lookup performance during flattening. */
10713
+ this.virtualSelectedNodesSet = new Set();
10714
+ /** Row height in pixels, measured from the first rendered item or set to 40 as a default. */
10715
+ this.virtualItemHeight = 40;
10716
+ /** Height of the scrollable container in pixels. */
10717
+ this.virtualViewportHeight = 0;
10718
+ /** scrollTop captured during the previous scroll handler invocation. */
10719
+ this.virtualPrevScrollTop = 0;
10720
+ /** Index of the first rendered row in the current window. */
10721
+ this.virtualStartIndex = 0;
10722
+ /** Index one past the last rendered row in the current window. */
10723
+ this.virtualEndIndex = 0;
10724
+ /** Extra rows rendered above and below the visible window to smooth scrolling. */
10725
+ this.virtualOverscanCount = 3;
10726
+ /** RequestAnimationFrame handle used for throttling scroll events. */
10727
+ this.virtualScrollRAF = null;
10728
+ /** The `.e-mask-parent` overlay element shown during fast scroll. */
10729
+ this.virtualMaskElement = null;
10730
+ /** Bound scroll handler stored for later removeEventListener. */
10731
+ this.virtualScrollBound = null;
10732
+ /** Skeleton component instances inside the mask overlay. */
10733
+ this.skeletonInstances = [];
10734
+ /** Set to true whenever the flat node list must be rebuilt on next render. */
10735
+ this.virtualTreeDirty = false;
10736
+ /** The data-uid of the currently keyboard-focused node in virtual mode (persists across re-renders). */
10737
+ this.virtualFocusedId = null;
10738
+ /**
10739
+ * Set to true by the document-level `keydown` handler when the user presses
10740
+ * the Tab key. The browser will auto-scroll the focused element into view
10741
+ * immediately after focus, which fires a `scroll` event on the TreeView
10742
+ * container. We want to skip the re-render that the virtual scroll handler
10743
+ * would otherwise trigger, because the element being focused is already
10744
+ * rendered (it is the single `<li tabindex="0">` of the current window) and
10745
+ * re-rendering would tear down the DOM element that is currently holding
10746
+ * focus. The flag is consumed (reset to `false`) inside `onVirtualScroll`
10747
+ * after the auto-scroll has been handled.
10748
+ */
10749
+ this.virtualSkipNextScrollRender = false;
10689
10750
  }
10690
10751
  /**
10691
10752
  * Get component name.
@@ -10981,6 +11042,24 @@ let TreeView = TreeView_1 = class TreeView extends Component {
10981
11042
  }
10982
11043
  }
10983
11044
  renderItems(isSorted) {
11045
+ // In virtual mode (with a fixed height) create only the container <ul> —
11046
+ // initVirtualization will populate the items directly, avoiding the O(n)
11047
+ // ListBase.createList cost.
11048
+ // updateCheckedStateFromDS / updateParentCheckState are intentionally skipped here:
11049
+ // they call getCheckedNodeDetails which traverses the DOM (no DOM exists yet in
11050
+ // virtual mode) and would throw on empty recursion results. The initial checked
11051
+ // state is derived from the raw data inside flattenNodeRecursive instead.
11052
+ //
11053
+ // When enableVirtualization is true but height is NOT provided, we fall through
11054
+ // to the normal (non-virtualized) rendering path so the tree is still visible.
11055
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
11056
+ this.ulElement = this.createElement('ul', {
11057
+ className: PARENTITEM + ' ' + 'e-ul',
11058
+ attrs: { role: 'group' }
11059
+ });
11060
+ this.element.appendChild(this.ulElement);
11061
+ return;
11062
+ }
10984
11063
  this.listBaseOption.ariaAttributes.level = 1;
10985
11064
  const sortedData = this.getSortedData(this.rootData);
10986
11065
  this.ulElement = ListBase.createList(this.createElement, isSorted ? this.rootData : sortedData, this.listBaseOption);
@@ -12016,7 +12095,7 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12016
12095
  else if (state === 'indeterminate' && this.autoCheck) {
12017
12096
  frameSpan.classList.remove(CHECK);
12018
12097
  frameSpan.classList.add(INDETERMINATE);
12019
- this.removeCheck(currLi);
12098
+ this.removeCheck(currLi, true);
12020
12099
  ariaState = 'mixed';
12021
12100
  }
12022
12101
  ariaState = state === 'check' ? 'true' : state === 'uncheck' ? 'false' : ariaState;
@@ -12047,12 +12126,112 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12047
12126
  if (!isNullOrUndefined(id) && this.checkedNodes.indexOf(id) === -1) {
12048
12127
  this.checkDisabledState(id);
12049
12128
  }
12129
+ if (this.enableVirtualization) {
12130
+ // Clear any indeterminate state — node is now fully checked
12131
+ this.virtualIndeterminateNodesSet.delete(id);
12132
+ const actualIndex = this.virtualNodeMap.get(id);
12133
+ if (!isNullOrUndefined(actualIndex) && actualIndex > -1) {
12134
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = true;
12135
+ if (this.autoCheck && this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].hasChildren) {
12136
+ this.setVirtualChildCheckState(id, true);
12137
+ }
12138
+ }
12139
+ }
12050
12140
  }
12051
- removeCheck(liEle) {
12052
- const index = this.checkedNodes.indexOf(liEle.getAttribute('data-uid'));
12141
+ removeCheck(liEle, isIndeterminate) {
12142
+ const id = liEle.getAttribute('data-uid');
12143
+ const index = this.checkedNodes.indexOf(id);
12053
12144
  if (index > -1) {
12054
12145
  this.checkedNodes.splice(index, 1);
12055
12146
  }
12147
+ if (this.enableVirtualization) {
12148
+ const actualIndex = this.virtualNodeMap.get(id);
12149
+ if (!isNullOrUndefined(actualIndex) && actualIndex > -1) {
12150
+ if (isIndeterminate) {
12151
+ // Parent is transitioning to indeterminate — preserve children states
12152
+ // and mark this node as 'indeterminate' so scroll/collapse re-renders correctly.
12153
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = 'indeterminate';
12154
+ // Also record it in the persistent indeterminate set so flattenVisibleNodes()
12155
+ // can restore this state after a full rebuild (e.g. on collapse).
12156
+ this.virtualIndeterminateNodesSet.add(id);
12157
+ }
12158
+ else {
12159
+ // Node is being fully unchecked — clear indeterminate tracking too
12160
+ this.virtualIndeterminateNodesSet.delete(id);
12161
+ this.virtualCheckedNodesSet.delete(id);
12162
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = false;
12163
+ if (this.treeData && this.fields && this.fields.isChecked) {
12164
+ setValue(this.fields.isChecked, false, this.treeData[parseInt(id.toString(), 10) - 1]);
12165
+ }
12166
+ if (this.autoCheck && this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].hasChildren) {
12167
+ this.setVirtualChildCheckState(id, false);
12168
+ }
12169
+ }
12170
+ }
12171
+ }
12172
+ }
12173
+ /**
12174
+ * Recursively sets the `isChecked` state of all descendants of the node with
12175
+ * `parentId` in `virtualFlatNodes`, keeping `checkedNodes` and
12176
+ * `virtualCheckedNodesSet` in sync. Used in virtual mode to honor the
12177
+ * `autoCheck` cascade when a parent checkbox is toggled.
12178
+ *
12179
+ * @param {string} parentId - The `data-uid` of the parent whose descendants should be updated.
12180
+ * @param {boolean} isChecked - The new state to apply to every descendant.
12181
+ * @returns {void}
12182
+ * @private
12183
+ */
12184
+ setVirtualChildCheckState(parentId, isChecked) {
12185
+ if (!this.virtualFlatNodes || this.virtualFlatNodes.length === 0) {
12186
+ return;
12187
+ }
12188
+ for (let i = 0; i < this.virtualFlatNodes.length; i++) {
12189
+ const node = this.virtualFlatNodes[parseInt(i.toString(), 10)];
12190
+ if (node.parentId === parentId) {
12191
+ node.isChecked = isChecked;
12192
+ if (isChecked) {
12193
+ this.virtualCheckedNodesSet.add(node.id);
12194
+ if (this.checkedNodes && this.checkedNodes.indexOf(node.id) === -1) {
12195
+ this.checkedNodes.push(node.id);
12196
+ }
12197
+ }
12198
+ else {
12199
+ this.virtualCheckedNodesSet.delete(node.id);
12200
+ const idx = this.checkedNodes ? this.checkedNodes.indexOf(node.id) : -1;
12201
+ if (idx !== -1) {
12202
+ this.checkedNodes.splice(idx, 1);
12203
+ }
12204
+ }
12205
+ if (node.hasChildren) {
12206
+ this.setVirtualChildCheckState(node.id, isChecked);
12207
+ }
12208
+ }
12209
+ }
12210
+ // Collapsed children are not present in virtualFlatNodes, so walk the raw data
12211
+ // to keep this.treeData and checkedNodes in sync for hidden descendants too.
12212
+ const collapsedChildren = this.getChildNodes(this.treeData, parentId);
12213
+ if (collapsedChildren) {
12214
+ for (let i = 0; i < collapsedChildren.length; i++) {
12215
+ const childId = collapsedChildren[parseInt(i.toString(), 10)][this.fields.id]
12216
+ ? collapsedChildren[parseInt(i.toString(), 10)][this.fields.id].toString()
12217
+ : null;
12218
+ if (childId && (!this.virtualNodeMap || !this.virtualNodeMap.has(childId))) {
12219
+ this.updateField(this.treeData, this.fields, childId, this.fields.isChecked, isChecked);
12220
+ if (isChecked) {
12221
+ if (this.checkedNodes && this.checkedNodes.indexOf(childId) === -1) {
12222
+ this.checkedNodes.push(childId);
12223
+ }
12224
+ }
12225
+ else {
12226
+ const idx = this.checkedNodes ? this.checkedNodes.indexOf(childId) : -1;
12227
+ if (idx !== -1) {
12228
+ this.checkedNodes.splice(idx, 1);
12229
+ }
12230
+ }
12231
+ this.setVirtualChildCheckState(childId, isChecked);
12232
+ }
12233
+ }
12234
+ }
12056
12235
  }
12057
12236
  getCheckEvent(currLi, action, e) {
12058
12237
  this.checkActionNodes.push(this.getNodeData(currLi));
@@ -12084,6 +12263,9 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12084
12263
  return;
12085
12264
  }
12086
12265
  const liEle = ulEle ? selectAll('.' + LISTITEM, ulEle) : this.liList;
12266
+ if (!liEle) {
12267
+ return;
12268
+ }
12087
12269
  const length = liEle.length;
12088
12270
  for (let i = 0; i < length; i++) {
12089
12271
  this.calculateWrap(liEle[parseInt(i.toString(), 10)]);
@@ -12170,6 +12352,10 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12170
12352
  }
12171
12353
  this.isLoaded = true;
12172
12354
  this.isNodeDropped = false;
12355
+ // Initialise (or re-initialise) virtual mode after data binding has settled
12356
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
12357
+ this.initVirtualization();
12358
+ }
12173
12359
  }
12174
12360
  doSelectionAction() {
12175
12361
  const sNodes = selectAll('.' + SELECTED$1, this.element);
@@ -12303,7 +12489,17 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12303
12489
  };
12304
12490
  this.trigger('nodeClicked', eventArgs);
12305
12491
  }
12306
- expandNode(currLi, icon, loaded) {
12492
+ expandNode(currLi, icon, loaded, isKeyboardNav) {
12493
+ // Virtual mode fast-path: rebuild the flat list and re-render the window.
12494
+ // Only engage when height is also set; without a fixed height the tree falls
12495
+ // back to normal rendering and the standard expand path must be used.
12496
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
12497
+ const id = currLi ? currLi.getAttribute('data-uid') : null;
12498
+ if (id) {
12499
+ this.handleVirtualExpand(id, isKeyboardNav);
12500
+ }
12501
+ return;
12502
+ }
12307
12503
  this.renderReactTemplates();
12308
12504
  if (icon.classList.contains(LOAD)) {
12309
12505
  this.hideSpinner(icon);
@@ -12410,15 +12606,29 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12410
12606
  removeClass([icon], PROCESS);
12411
12607
  }
12412
12608
  else {
12413
- this.nodeCollapseAction(currLi, icon, observedArgs);
12609
+ // Detect if event is keyboard navigation by checking for 'action' property
12610
+ const isKeyboardNav = e && e.action !== undefined;
12611
+ this.nodeCollapseAction(currLi, icon, observedArgs, isKeyboardNav);
12414
12612
  }
12415
12613
  });
12416
12614
  }
12417
12615
  else {
12418
- this.nodeCollapseAction(currLi, icon, colArgs);
12616
+ // Detect if event is keyboard navigation by checking for 'action' property
12617
+ const isKeyboardNav = e && e.action !== undefined;
12618
+ this.nodeCollapseAction(currLi, icon, colArgs, isKeyboardNav);
12419
12619
  }
12420
12620
  }
12421
- nodeCollapseAction(currLi, icon, colArgs) {
12621
+ nodeCollapseAction(currLi, icon, colArgs, isKeyboardNav) {
12622
+ // Virtual mode fast-path: rebuild the flat list and re-render the window.
12623
+ // Only engage when height is also set; without a fixed height the tree falls
12624
+ // back to normal rendering and the standard collapse path must be used.
12625
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
12626
+ const id = currLi ? currLi.getAttribute('data-uid') : null;
12627
+ if (id) {
12628
+ this.handleVirtualCollapse(id, isKeyboardNav);
12629
+ }
12630
+ return;
12631
+ }
12422
12632
  removeClass([icon], COLLAPSIBLE);
12423
12633
  addClass([icon], EXPANDABLE);
12424
12634
  let start = 0;
@@ -12872,6 +13082,10 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12872
13082
  li.setAttribute('tabindex', '0');
12873
13083
  EventHandler.add(li, 'blur', this.focusOut, this);
12874
13084
  this.updateIdAttr(focusedNode, li);
13085
+ // Track focused id for virtual mode re-render restoration
13086
+ if (this.enableVirtualization) {
13087
+ this.virtualFocusedId = li.getAttribute('data-uid');
13088
+ }
12875
13089
  }
12876
13090
  }
12877
13091
  addSelect(liEle) {
@@ -12881,6 +13095,13 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12881
13095
  if (!isNullOrUndefined(id) && this.selectedNodes.indexOf(id) === -1) {
12882
13096
  this.selectedNodes.push(id);
12883
13097
  }
13098
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13099
+ this.virtualSelectedNodesSet.add(id);
13100
+ const actualIndex = this.virtualNodeMap.get(id);
13101
+ if (actualIndex && actualIndex > -1) {
13102
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].selected = true;
13103
+ }
13104
+ }
12884
13105
  }
12885
13106
  removeSelect(liEle) {
12886
13107
  if (this.allowMultiSelection) {
@@ -12894,6 +13115,14 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12894
13115
  if (index > -1) {
12895
13116
  this.selectedNodes.splice(index, 1);
12896
13117
  }
13118
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13119
+ const id = liEle.getAttribute('data-uid');
13120
+ this.virtualSelectedNodesSet.delete(id);
13121
+ const actualIndex = this.virtualNodeMap.get(id);
13122
+ if (actualIndex && actualIndex > -1) {
13123
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].selected = false;
13124
+ }
13125
+ }
12897
13126
  }
12898
13127
  removeSelectAll() {
12899
13128
  const selectedLI = this.element.querySelectorAll('.' + ACTIVE);
@@ -12907,6 +13136,15 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12907
13136
  }
12908
13137
  removeClass(selectedLI, ACTIVE);
12909
13138
  this.setProperties({ selectedNodes: [] }, true);
13139
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13140
+ this.virtualSelectedNodesSet.clear();
13141
+ for (let i = 0; i < this.virtualFlatNodes.length; i++) {
13142
+ const node = this.virtualFlatNodes[parseInt(i.toString(), 10)];
13143
+ if (node.selected) {
13144
+ node.selected = false;
13145
+ }
13146
+ }
13147
+ }
12910
13148
  }
12911
13149
  getSelectEvent(currLi, action, e) {
12912
13150
  const nodeData = this.getNodeData(currLi);
@@ -12929,6 +13167,9 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12929
13167
  }
12930
13168
  }
12931
13169
  expandCollapseAction(currLi, e) {
13170
+ if (this.enableVirtualization && !isNullOrUndefined(this.height) && currLi.classList.contains(EDITING)) {
13171
+ return;
13172
+ }
12932
13173
  const icon = select('div.' + ICON, currLi);
12933
13174
  if (!icon || icon.classList.contains(PROCESS)) {
12934
13175
  return;
@@ -12958,15 +13199,29 @@ let TreeView = TreeView_1 = class TreeView extends Component {
12958
13199
  removeClass([icon], PROCESS);
12959
13200
  }
12960
13201
  else {
12961
- this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll);
13202
+ // Detect if event is keyboard navigation by checking for 'action' property
13203
+ const isKeyboardNav = e && e.action !== undefined;
13204
+ this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav);
12962
13205
  }
12963
13206
  });
12964
13207
  }
12965
13208
  else {
12966
- this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll);
13209
+ // Detect if event is keyboard navigation by checking for 'action' property
13210
+ const isKeyboardNav = e && e.action !== undefined;
13211
+ this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav);
13212
+ }
13213
+ }
13214
+ nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav) {
13215
+ // In virtual mode (with a fixed height) the flat <li> never has a child <ul>
13216
+ // in the DOM, so the PARENTITEM check below would always fail and fall into
13217
+ // renderChildNodes(), which corrupts the virtual DOM.
13218
+ // Delegate directly to expandNode() which has the handleVirtualExpand fast-path.
13219
+ // When height is not provided, fall through to normal expand logic below since
13220
+ // the tree is rendered with a full nested DOM structure.
13221
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13222
+ this.expandNode(currLi, icon, undefined, isKeyboardNav);
13223
+ return;
12967
13224
  }
12968
- }
12969
- nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll) {
12970
13225
  const ul = select('.' + PARENTITEM, currLi);
12971
13226
  if (ul && ul.nodeName === 'UL') {
12972
13227
  this.expandNode(currLi, icon);
@@ -13079,6 +13334,19 @@ let TreeView = TreeView_1 = class TreeView extends Component {
13079
13334
  });
13080
13335
  }
13081
13336
  navigateToFocus(isUp) {
13337
+ // In virtual mode (with a fixed height) the element itself is the scroll
13338
+ // container; use ensureVirtualNodeVisible. When height is not provided,
13339
+ // the tree uses normal rendering, so fall through to the standard scroll logic.
13340
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13341
+ const focusedLi = this.getFocusedNode();
13342
+ if (focusedLi) {
13343
+ const id = focusedLi.getAttribute('data-uid');
13344
+ if (id) {
13345
+ this.ensureVirtualNodeVisible(id);
13346
+ }
13347
+ }
13348
+ return;
13349
+ }
13082
13350
  const focusNode = this.getFocusedNode().querySelector('.' + TEXTWRAP);
13083
13351
  const pos = focusNode.getBoundingClientRect();
13084
13352
  const parent = this.getScrollParent(this.element);
@@ -13497,6 +13765,46 @@ let TreeView = TreeView_1 = class TreeView extends Component {
13497
13765
  return isNullOrUndefined(fNode) ? (isNullOrUndefined(selectedItem) ? this.element.firstElementChild : selectedItem) : fNode;
13498
13766
  }
13499
13767
  focusNextNode(li, isTowards) {
13768
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13769
+ // Determine the id of the node one step in the requested direction
13770
+ const currentId = li ? li.getAttribute('data-uid') : null;
13771
+ let currentIdx = -1;
13772
+ if (!isNullOrUndefined(currentId)) {
13773
+ const idx = this.virtualNodeMap.get(currentId);
13774
+ currentIdx = !isNullOrUndefined(idx) ? idx : -1;
13775
+ }
13776
+ const targetIdx = isTowards ? currentIdx + 1 : currentIdx - 1;
13777
+ if (targetIdx < 0 || targetIdx >= this.virtualFlatNodes.length) {
13778
+ return;
13779
+ }
13780
+ const targetId = this.virtualFlatNodes[parseInt(targetIdx.toString(), 10)].id;
13781
+ // ── Fix 1: set virtualFocusedId BEFORE calling ensureVirtualNodeVisible ──
13782
+ // renderVirtualItems() is called synchronously inside ensureVirtualNodeVisible
13783
+ // when a re-render is needed. If virtualFocusedId is still the old value at
13784
+ // that point, focus restoration falls back to the first item. Setting it here
13785
+ // ensures renderVirtualItems always restores focus to the correct target node.
13786
+ this.virtualFocusedId = targetId;
13787
+ // ── Fix 2: clear focus classes from ALL nested list items, not just top-level ones ──
13788
+ // With hierarchical virtual DOM, the currently focused <li> may be nested inside
13789
+ // a child <ul>. Using ':scope > li' only cleans up direct children and leaves
13790
+ // stale e-focused / e-node-focus classes on nested nodes across re-renders.
13791
+ const allLi = this.element.querySelectorAll('.' + LISTITEM + '.' + FOCUS);
13792
+ for (let k = 0; k < allLi.length; k++) {
13793
+ allLi[parseInt(k.toString(), 10)].classList.remove(FOCUS);
13794
+ allLi[parseInt(k.toString(), 10)].classList.remove(FOCUSED$1);
13795
+ allLi[parseInt(k.toString(), 10)].setAttribute('tabindex', '-1');
13796
+ }
13797
+ // Ensure the target <li> is in the DOM window (may trigger a re-render).
13798
+ // virtualFocusedId is already set above so renderVirtualItems restores focus correctly.
13799
+ this.ensureVirtualNodeVisible(targetId);
13800
+ // Re-query both nodes after the potential re-render so setFocus gets live DOM elements
13801
+ const prevNode = currentId ? this.getElement(currentId) : li;
13802
+ const nextNode = this.getElement(targetId);
13803
+ if (!isNullOrUndefined(nextNode)) {
13804
+ this.setFocus(isNullOrUndefined(prevNode) ? nextNode : prevNode, nextNode);
13805
+ }
13806
+ return;
13807
+ }
13500
13808
  const nextNode = isTowards ? this.getNextNode(li) : this.getPrevNode(li);
13501
13809
  this.setFocus(li, nextNode);
13502
13810
  this.navigateToFocus(!isTowards);
@@ -13566,6 +13874,12 @@ let TreeView = TreeView_1 = class TreeView extends Component {
13566
13874
  addClass([nextNode], FOCUS);
13567
13875
  addClass([nextNode], FOCUSED$1);
13568
13876
  nextNode.setAttribute('tabindex', '0');
13877
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13878
+ nextNode.focus({ preventScroll: true });
13879
+ }
13880
+ else {
13881
+ nextNode.focus();
13882
+ }
13569
13883
  nextNode.focus();
13570
13884
  EventHandler.add(nextNode, 'blur', this.focusOut, this);
13571
13885
  this.updateIdAttr(preNode, nextNode);
@@ -13584,6 +13898,30 @@ let TreeView = TreeView_1 = class TreeView extends Component {
13584
13898
  if (!focusedElement) {
13585
13899
  return;
13586
13900
  }
13901
+ // Virtual-mode guard: when the browser moves focus into the TreeView (e.g. via
13902
+ // the Tab key) it always lands on the single `<li tabindex="0">`. In that
13903
+ // case the element is already the focusable one, FOCUS is already applied
13904
+ // and the only thing left to do is the keyboard focus styling. Skip the
13905
+ // redundant setAttribute/EventHandler.add work so we never re-render the
13906
+ // virtual window as a side effect of focusing the existing `tabindex="0"`
13907
+ // item.
13908
+ // ── Fix: Use document.activeElement to detect browser-focused node ──
13909
+ // After renderVirtualItems() restores focus, the browser-focused element is
13910
+ // the authoritative source, not getFocusedNode(). If they don't match, it means
13911
+ // renderVirtualItems already correctly set up the element, so we must not
13912
+ // override its setup by calling setFocus again.
13913
+ const actuallyFocusedElement = document.activeElement;
13914
+ const isAlreadyFocusedInVirtualMode = this.enableVirtualization
13915
+ && actuallyFocusedElement === focusedElement
13916
+ && actuallyFocusedElement.getAttribute('tabindex') === '0'
13917
+ && actuallyFocusedElement.classList.contains(FOCUS);
13918
+ if (isAlreadyFocusedInVirtualMode) {
13919
+ if (this.isKeyboardFocus && !actuallyFocusedElement.classList.contains(FOCUSED$1)) {
13920
+ addClass([actuallyFocusedElement], FOCUSED$1);
13921
+ }
13922
+ this.isKeyboardFocus = false;
13923
+ return;
13924
+ }
13587
13925
  if (!this.mouseDownStatus) {
13588
13926
  if (focusedElement.classList.contains('e-disable')) {
13589
13927
  focusedElement.setAttribute('tabindex', '-1');
@@ -13610,6 +13948,9 @@ let TreeView = TreeView_1 = class TreeView extends Component {
13610
13948
  removeClass([focusedElement], FOCUS);
13611
13949
  removeClass([focusedElement], FOCUSED$1);
13612
13950
  EventHandler.remove(focusedElement, 'blur', this.focusOut);
13951
+ if (this.enableVirtualization) {
13952
+ this.virtualFocusedId = null;
13953
+ }
13613
13954
  }
13614
13955
  }
13615
13956
  onMouseOver(e) {
@@ -13821,6 +14162,19 @@ let TreeView = TreeView_1 = class TreeView extends Component {
13821
14162
  else {
13822
14163
  txtEle[ListBase.getDomSetter(this.listBaseOption)] = ListBase.getSanitizedText(newText, this.listBaseOption);
13823
14164
  }
14165
+ // ── Virtual mode: sync the flat-node snapshot so re-renders show updated text ──
14166
+ // virtualFlatNodes is rebuilt from this.treeData (via flattenVisibleNodes) only on
14167
+ // expand/collapse/scroll. Between those events the snapshot is the single source of
14168
+ // truth used by createVirtualListItem. setValue() above updates this.editData (which
14169
+ // IS the treeData object reference), but virtualFlatNodes[idx].text still holds the
14170
+ // old value. Update it here so the next renderVirtualItems() call shows the new text.
14171
+ if (this.enableVirtualization) {
14172
+ const id = liEle.getAttribute('data-uid');
14173
+ const idx = this.virtualNodeMap ? this.virtualNodeMap.get(id) : undefined;
14174
+ if (!isNullOrUndefined(idx) && idx > -1) {
14175
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].text = newText;
14176
+ }
14177
+ }
13824
14178
  if (isInput) {
13825
14179
  removeClass([liEle], EDITING);
13826
14180
  liEle.focus();
@@ -15190,6 +15544,31 @@ let TreeView = TreeView_1 = class TreeView extends Component {
15190
15544
  keyDownHandler(e) {
15191
15545
  if (e.key === 'Tab' || e.keyCode === 9) {
15192
15546
  this.isKeyboardFocus = true;
15547
+ // Mark the upcoming scroll event (if any) as Tab-induced so the
15548
+ // virtual scroll handler skips its re-render path. The browser
15549
+ // calls `Element.scrollIntoView` semantics on focus, which scrolls
15550
+ // a `<li tabindex="0">` in the middle of the tree into view and
15551
+ // therefore fires `scroll` on the container. Without this guard
15552
+ // the scroll handler would tear down the focused `<li>` and move
15553
+ // focus elsewhere.
15554
+ if (this.enableVirtualization) {
15555
+ this.virtualSkipNextScrollRender = true;
15556
+ }
15557
+ }
15558
+ // ── Fix: Also skip scroll render for arrow key navigation ──────────────────
15559
+ // Arrow Down/Up calls focusNextNode() which calls ensureVirtualNodeVisible()
15560
+ // to scroll the target node into view. Without this guard, the scroll event
15561
+ // would trigger onVirtualScroll() which re-renders the window and resets the
15562
+ // focus that focusNextNode() just set. This manifests as tabindex resetting
15563
+ // to the first item instead of staying on the arrow-navigated target.
15564
+ // Arrow Right/Left (expand/collapse) also calls renderVirtualItems() which
15565
+ // calls li.focus() in branch 2; that can trigger a scroll on an edge node,
15566
+ // so we consume it here to prevent a stale RAF re-render.
15567
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.keyCode === 40 || e.keyCode === 38
15568
+ || e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.keyCode === 39 || e.keyCode === 37) {
15569
+ if (this.enableVirtualization) {
15570
+ this.virtualSkipNextScrollRender = true;
15571
+ }
15193
15572
  }
15194
15573
  }
15195
15574
  preventContextMenu(e) {
@@ -15543,6 +15922,18 @@ let TreeView = TreeView_1 = class TreeView extends Component {
15543
15922
  if (this.dataType === 1) {
15544
15923
  this.groupedData = this.getGroupedData(this.treeData, this.fields.parentID);
15545
15924
  }
15925
+ // In virtual mode (with a fixed height), rebuild the flat node list and re-render
15926
+ // after deletion so the removed nodes are correctly reflected in the hierarchical
15927
+ // virtual DOM. When height is not provided, the tree uses normal rendering and
15928
+ // the DOM is already updated by removeNode() above.
15929
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
15930
+ this.virtualFlatNodes = this.flattenVisibleNodes();
15931
+ this.rebuildVirtualNodeMap();
15932
+ const win = this.getVirtualWindow();
15933
+ this.virtualStartIndex = win.startIndex;
15934
+ this.virtualEndIndex = win.endIndex;
15935
+ this.renderVirtualItems();
15936
+ }
15546
15937
  this.triggerEvent('removeNode', nodeData);
15547
15938
  }
15548
15939
  editSucess(target, newText, prevent) {
@@ -15597,6 +15988,1085 @@ let TreeView = TreeView_1 = class TreeView extends Component {
15597
15988
  }
15598
15989
  return parentsID;
15599
15990
  }
15991
+ // ═══════════════════════════════════════════════════════════════════════════════
15992
+ // VIRTUALIZATION — private helpers
15993
+ // ═══════════════════════════════════════════════════════════════════════════════
15994
+ /**
15995
+ * Parses the `height` property (string like '400px' or number) to a pixel value.
15996
+ * Returns 0 if the value cannot be determined.
15997
+ *
15998
+ * @returns {number} Pixel height.
15999
+ * @private
16000
+ */
16001
+ parseHeightToPixels() {
16002
+ const h = this.height;
16003
+ if (isNullOrUndefined(h)) {
16004
+ return 0;
16005
+ }
16006
+ if (typeof h === 'number') {
16007
+ return h;
16008
+ }
16009
+ const parsed = parseFloat(h);
16010
+ return isNaN(parsed) ? 0 : parsed;
16011
+ }
16012
+ /**
16013
+ * Measures the height of the first rendered list item so subsequent calculations
16014
+ * use real layout values. Falls back to the stored `virtualItemHeight` default (40).
16015
+ *
16016
+ * @returns {number} Row height in pixels.
16017
+ * @private
16018
+ */
16019
+ measureVirtualItemHeight() {
16020
+ const firstLi = select('.' + LISTITEM, this.element);
16021
+ if (firstLi && firstLi.offsetHeight > 0) {
16022
+ return firstLi.offsetHeight;
16023
+ }
16024
+ return this.virtualItemHeight;
16025
+ }
16026
+ /**
16027
+ * Rebuilds the cached Sets for checked/selected nodes to enable O(1) lookup
16028
+ * during the flatten operation instead of O(n) array indexOf calls.
16029
+ *
16030
+ * @returns {void}
16031
+ * @private
16032
+ */
16033
+ rebuildVirtualCheckCache() {
16034
+ this.virtualExpandedNodesSet.clear();
16035
+ this.virtualCheckedNodesSet.clear();
16036
+ this.virtualSelectedNodesSet.clear();
16037
+ if (this.expandedNodes && Array.isArray(this.expandedNodes)) {
16038
+ for (let i = 0; i < this.expandedNodes.length; i++) {
16039
+ this.virtualExpandedNodesSet.add(this.expandedNodes[i]);
16040
+ }
16041
+ }
16042
+ if (this.checkedNodes && Array.isArray(this.checkedNodes)) {
16043
+ for (let i = 0; i < this.checkedNodes.length; i++) {
16044
+ this.virtualCheckedNodesSet.add(this.checkedNodes[i]);
16045
+ }
16046
+ }
16047
+ if (this.selectedNodes && Array.isArray(this.selectedNodes)) {
16048
+ for (let i = 0; i < this.selectedNodes.length; i++) {
16049
+ this.virtualSelectedNodesSet.add(this.selectedNodes[i]);
16050
+ }
16051
+ }
16052
+ }
16053
+ /**
16054
+ * Recursively walks the treeData (or groupedData for flat lists) and builds
16055
+ * `virtualFlatNodes` — an ordered list of every node that is currently visible
16056
+ * (i.e. its ancestors are all expanded). Uses cached Sets for O(1) state lookups.
16057
+ *
16058
+ * @returns {VirtualNodeData[]} Ordered flat list of visible nodes.
16059
+ * @private
16060
+ */
16061
+ flattenVisibleNodes() {
16062
+ // Build cached Sets for fast lookups
16063
+ this.rebuildVirtualCheckCache();
16064
+ const result = [];
16065
+ if (this.dataType === 1) {
16066
+ // Flat data with parentID field — use getChildGroup to find root nodes
16067
+ let rootNodes = [];
16068
+ if (this.groupedData) {
16069
+ rootNodes = this.getChildGroup(this.groupedData, null, true);
16070
+ }
16071
+ if (!rootNodes || rootNodes.length === 0) {
16072
+ rootNodes = this.rootData;
16073
+ }
16074
+ // Apply sortOrder to root-level nodes for flat data (same as hierarchical path below)
16075
+ rootNodes = this.getSortedData(rootNodes);
16076
+ this.flattenNodeRecursive(rootNodes, 1, null, result);
16077
+ }
16078
+ else {
16079
+ // Hierarchical data
16080
+ const sorted = this.getSortedData(this.rootData);
16081
+ this.flattenNodeRecursive(sorted, 1, null, result);
16082
+ }
16083
+ return result;
16084
+ }
16085
+ /**
16086
+ * Recursive helper for `flattenVisibleNodes`.
16087
+ *
16088
+ * @param {Object[]} nodes - Array of raw data objects at this level.
16089
+ * @param {number} level - 1-based depth level.
16090
+ * @param {string | null} parentId - The parent node id, or null for root.
16091
+ * @param {VirtualNodeData[]} result - Accumulator array.
16092
+ * @returns {void}
16093
+ * @private
16094
+ */
16095
+ flattenNodeRecursive(nodes, level, parentId, result) {
16096
+ if (!nodes || nodes.length === 0) {
16097
+ return;
16098
+ }
16099
+ // Cache field names and settings outside the loop to avoid repeated conversions
16100
+ const childFieldName = this.fields.child.toString();
16101
+ const expandedFieldName = this.fields.expanded;
16102
+ const isCheckedFieldName = this.fields.isChecked;
16103
+ const iconCssFieldName = this.fields.iconCss;
16104
+ const imageUrlFieldName = this.fields.imageUrl;
16105
+ const idFieldName = this.fields.id;
16106
+ const textFieldName = this.fields.text;
16107
+ const selectedFieldName = this.fields.selected;
16108
+ const needsSorting = this.sortOrder !== 'None';
16109
+ const isDataTypeFlat = this.dataType === 1;
16110
+ const enableVirtualization = this.enableVirtualization;
16111
+ for (let i = 0; i < nodes.length; i++) {
16112
+ const raw = nodes[i];
16113
+ const id = isNullOrUndefined(raw[idFieldName]) ? '' : raw[idFieldName].toString();
16114
+ const text = isNullOrUndefined(raw[textFieldName]) ? '' : raw[textFieldName].toString();
16115
+ // Use expandedNodes as the single source of truth.
16116
+ const inExpandedNodes = this.virtualExpandedNodesSet.has(id);
16117
+ const rawExpanded = !inExpandedNodes && getValue(expandedFieldName, raw) === true;
16118
+ const expanded = inExpandedNodes || rawExpanded;
16119
+ if (rawExpanded && id) {
16120
+ this.expandedNodes.push(id);
16121
+ this.virtualExpandedNodesSet.add(id);
16122
+ }
16123
+ // Inline hasChildRecords check to avoid function call (optimization)
16124
+ let hasChildren = false;
16125
+ if (isDataTypeFlat) {
16126
+ if (this.groupedData) {
16127
+ const groupedChild = this.getChildGroup(this.groupedData, id, false);
16128
+ hasChildren = !isNullOrUndefined(groupedChild) && groupedChild.length > 0;
16129
+ }
16130
+ }
16131
+ else {
16132
+ const childrenObj = getValue(childFieldName, raw);
16133
+ hasChildren = !isNullOrUndefined(childrenObj) && Array.isArray(childrenObj) && childrenObj.length > 0;
16134
+ }
16135
+ // Virtual-mode only: honour fields.isChecked === true on initial render
16136
+ if (enableVirtualization && id && isCheckedFieldName && !this.virtualCheckedNodesSet.has(id)) {
16137
+ const rawChecked = getValue(isCheckedFieldName, raw);
16138
+ if (!isNullOrUndefined(rawChecked) && rawChecked.toString() === 'true') {
16139
+ this.virtualCheckedNodesSet.add(id);
16140
+ if (this.checkedNodes && this.checkedNodes.indexOf(id) === -1) {
16141
+ this.checkedNodes.push(id);
16142
+ }
16143
+ }
16144
+ }
16145
+ if (this.initialRender && enableVirtualization && id && selectedFieldName && !this.virtualSelectedNodesSet.has(id)) {
16146
+ const rawSelected = getValue(selectedFieldName, raw);
16147
+ if (!isNullOrUndefined(rawSelected) && rawSelected.toString() === 'true') {
16148
+ this.virtualSelectedNodesSet.add(id);
16149
+ if (this.selectedNodes && this.selectedNodes.indexOf(id) === -1) {
16150
+ this.selectedNodes.push(id);
16151
+ }
16152
+ }
16153
+ }
16154
+ // Use cached Sets for O(1) lookup
16155
+ const selected = this.virtualSelectedNodesSet.has(id);
16156
+ // Resolve checkbox state: indeterminate takes precedence over the checked set,
16157
+ // because a node can be in the indeterminate state without being in checkedNodes.
16158
+ const isChecked = (enableVirtualization && this.virtualIndeterminateNodesSet.has(id))
16159
+ ? 'indeterminate'
16160
+ : this.virtualCheckedNodesSet.has(id);
16161
+ const iconCssVal = raw[iconCssFieldName];
16162
+ const iconCss = isNullOrUndefined(iconCssVal) ? '' : iconCssVal.toString();
16163
+ const imageUrlVal = raw[imageUrlFieldName];
16164
+ const imageUrl = isNullOrUndefined(imageUrlVal) ? '' : imageUrlVal.toString();
16165
+ const node = {
16166
+ id, text, level, parentId, hasChildren, expanded,
16167
+ selected, isChecked, iconCss, imageUrl
16168
+ };
16169
+ result.push(node);
16170
+ if (expanded && hasChildren) {
16171
+ // Determine children list
16172
+ let children = [];
16173
+ if (isDataTypeFlat) {
16174
+ if (this.groupedData) {
16175
+ children = this.getChildGroup(this.groupedData, id, false);
16176
+ // Apply sortOrder to flat-data children (same as hierarchical path)
16177
+ if (needsSorting) {
16178
+ children = ListBase.getDataSource(children, ListBase.addSorting(this.sortOrder, this.fields.text));
16179
+ }
16180
+ }
16181
+ }
16182
+ else {
16183
+ const rawChildren = getValue(childFieldName, raw);
16184
+ if (!isNullOrUndefined(rawChildren) && Array.isArray(rawChildren)) {
16185
+ children = rawChildren;
16186
+ // Only sort if needed
16187
+ if (needsSorting) {
16188
+ children = ListBase.getDataSource(children, ListBase.addSorting(this.sortOrder, this.fields.text));
16189
+ }
16190
+ }
16191
+ }
16192
+ this.flattenNodeRecursive(children, level + 1, id, result);
16193
+ }
16194
+ }
16195
+ }
16196
+ /**
16197
+ * Checks whether a raw datasource node has child records.
16198
+ *
16199
+ * @param {Object} raw - Raw datasource object.
16200
+ * @returns {boolean} True if children exist.
16201
+ * @private
16202
+ */
16203
+ hasChildRecords(raw) {
16204
+ if (this.dataType === 1) {
16205
+ if (!this.groupedData) {
16206
+ return false;
16207
+ }
16208
+ const rawId = raw[this.fields.id];
16209
+ if (isNullOrUndefined(rawId)) {
16210
+ return false;
16211
+ }
16212
+ const groupedChild = this.getChildGroup(this.groupedData, rawId.toString(), false);
16213
+ return !isNullOrUndefined(groupedChild) && groupedChild.length > 0;
16214
+ }
16215
+ const childField = this.fields.child.toString();
16216
+ const children = getValue(childField, raw);
16217
+ return !isNullOrUndefined(children) && Array.isArray(children) && children.length > 0;
16218
+ }
16219
+ /**
16220
+ * Rebuilds `virtualNodeMap` (id → index) from the current `virtualFlatNodes`.
16221
+ *
16222
+ * @returns {void}
16223
+ * @private
16224
+ */
16225
+ rebuildVirtualNodeMap() {
16226
+ this.virtualNodeMap.clear();
16227
+ for (let i = 0; i < this.virtualFlatNodes.length; i++) {
16228
+ this.virtualNodeMap.set(this.virtualFlatNodes[parseInt(i.toString(), 10)].id, i);
16229
+ }
16230
+ }
16231
+ getVirtualWindow(scrollTop) {
16232
+ const total = this.virtualFlatNodes.length;
16233
+ if (total === 0) {
16234
+ return { startIndex: 0, endIndex: 0 };
16235
+ }
16236
+ const st = scrollTop !== undefined ? scrollTop : this.element.scrollTop;
16237
+ const itemH = this.virtualItemHeight;
16238
+ const vpH = this.virtualViewportHeight;
16239
+ const rawStart = Math.floor(st / itemH) - this.virtualOverscanCount;
16240
+ const rawEnd = Math.ceil((st + vpH) / itemH) + this.virtualOverscanCount;
16241
+ const startIndex = Math.max(0, rawStart);
16242
+ const endIndex = Math.min(total, rawEnd);
16243
+ return { startIndex, endIndex };
16244
+ }
16245
+ getVirtualWindowAt(scrollTop) {
16246
+ return this.getVirtualWindow(scrollTop);
16247
+ }
16248
+ getVirtualPadding() {
16249
+ const total = this.virtualFlatNodes.length;
16250
+ const itemH = this.virtualItemHeight;
16251
+ const start = this.virtualStartIndex;
16252
+ const end = this.virtualEndIndex;
16253
+ const paddingTop = start * itemH;
16254
+ const paddingBottom = Math.max(0, (total - end) * itemH);
16255
+ return { paddingTop, paddingBottom };
16256
+ }
16257
+ /**
16258
+ * Renders the virtual window into a **nested** `<ul>/<li>` hierarchy identical to
16259
+ * the non-virtualized DOM structure.
16260
+ *
16261
+ * ## Scroll math
16262
+ * The flat-index ↔ pixel mapping stays unchanged: every entry in
16263
+ * `virtualFlatNodes` represents exactly one `itemHeight` row.
16264
+ * `paddingTop = startIndex * itemHeight` and
16265
+ * `paddingBottom = (total - endIndex) * itemHeight` keep the scrollbar
16266
+ * thumb in the correct position.
16267
+ *
16268
+ * ## Ancestor injection
16269
+ * When `startIndex` points at a child node whose ancestor(s) have already
16270
+ * scrolled above the viewport (their flat indices are < startIndex), the
16271
+ * ancestors would normally be missing from the DOM, forcing the children to
16272
+ * fall back to the root `<ul>` as flat siblings — producing wrong HTML.
16273
+ *
16274
+ * To fix this, before rendering the window we walk the ancestor chain of the
16275
+ * first visible node and collect every ancestor that lies **outside** the
16276
+ * window. Those ancestors are injected as real `<li>` containers at the
16277
+ * very top of the root `<ul>`. To compensate for their presence, `paddingTop`
16278
+ * is reduced by `injectedCount * itemHeight` so the total scroll height
16279
+ * remains the same.
16280
+ *
16281
+ * Once the ancestors are in the DOM they are registered in `renderedParentMap`
16282
+ * so the normal window-rendering loop can nest children inside them exactly
16283
+ * as it does when both parent and child are in the same window.
16284
+ *
16285
+ * @returns {void}
16286
+ * @private
16287
+ */
16288
+ renderVirtualItems() {
16289
+ if (!this.ulElement) {
16290
+ return;
16291
+ }
16292
+ const ul = this.ulElement;
16293
+ // ── Snapshot focused ID BEFORE clearing the DOM ───────────────────────────
16294
+ // Removing a focused <li> from the DOM triggers a synchronous `blur` event,
16295
+ // which calls focusOut() and sets virtualFocusedId = null. Capturing the id
16296
+ // here (before any removeChild) ensures focus is correctly restored to the
16297
+ // same node after re-render even when keyboard navigation was in progress.
16298
+ const focusedUid = this.virtualFocusedId || null;
16299
+ // ── Clear previous render (direct children only) ─────────────────────────
16300
+ const existingItems = ul.querySelectorAll(':scope > li');
16301
+ for (let i = existingItems.length - 1; i >= 0; i--) {
16302
+ ul.removeChild(existingItems[parseInt(i.toString(), 10)]);
16303
+ }
16304
+ const renderedParentMap = new Map();
16305
+ const itemH = this.virtualItemHeight;
16306
+ let injectedCount = 0;
16307
+ if (this.virtualStartIndex < this.virtualFlatNodes.length) {
16308
+ const firstNode = this.virtualFlatNodes[this.virtualStartIndex];
16309
+ if (firstNode.parentId) {
16310
+ // Build the ancestor chain from root down to the immediate parent
16311
+ const ancestors = [];
16312
+ let searchId = firstNode.parentId;
16313
+ while (searchId) {
16314
+ const ancestorIdx = this.virtualNodeMap.get(searchId);
16315
+ if (!isNullOrUndefined(ancestorIdx) && ancestorIdx < this.virtualStartIndex) {
16316
+ const ancestor = this.virtualFlatNodes[parseInt(ancestorIdx.toString(), 10)];
16317
+ ancestors.unshift(ancestor); // prepend so root comes first
16318
+ searchId = ancestor.parentId;
16319
+ }
16320
+ else {
16321
+ break;
16322
+ }
16323
+ }
16324
+ // Inject each ancestor as a real <li> into the root <ul>
16325
+ // and register it in renderedParentMap for child-nesting below.
16326
+ for (let a = 0; a < ancestors.length; a++) {
16327
+ const ancestor = ancestors[parseInt(a.toString(), 10)];
16328
+ const ancestorLi = this.createVirtualListItem(ancestor, false);
16329
+ // Find the correct container: root or a previously injected ancestor's <ul>
16330
+ let targetUl = ul;
16331
+ if (ancestor.parentId && renderedParentMap.has(ancestor.parentId)) {
16332
+ const pe = renderedParentMap.get(ancestor.parentId);
16333
+ if (!pe.childUl) {
16334
+ pe.childUl = this.createElement('ul', {
16335
+ className: PARENTITEM + ' e-ul',
16336
+ attrs: { role: 'group' }
16337
+ });
16338
+ pe.li.appendChild(pe.childUl);
16339
+ }
16340
+ targetUl = pe.childUl;
16341
+ }
16342
+ targetUl.appendChild(ancestorLi);
16343
+ renderedParentMap.set(ancestor.id, { li: ancestorLi, childUl: null });
16344
+ injectedCount++;
16345
+ }
16346
+ }
16347
+ }
16348
+ // ── Scroll-space padding (adjusted for injected ancestors) ────────────────
16349
+ const total = this.virtualFlatNodes.length;
16350
+ const start = this.virtualStartIndex;
16351
+ const end = this.virtualEndIndex;
16352
+ const rawPaddingTop = start * itemH;
16353
+ // Subtract the height of injected ancestors so the scroll position is
16354
+ // exact: those ancestor rows are now real DOM elements, not hidden space.
16355
+ const adjustedPaddingTop = Math.max(0, rawPaddingTop - injectedCount * itemH);
16356
+ ul.style.paddingTop = adjustedPaddingTop + 'px';
16357
+ ul.style.paddingBottom = Math.max(0, (total - end) * itemH) + 'px';
16358
+ // ── Render window items (nested via renderedParentMap) ────────────────────
16359
+ const liListBuf = [];
16360
+ for (let i = start; i < end; i++) {
16361
+ const node = this.virtualFlatNodes[parseInt(i.toString(), 10)];
16362
+ const li = this.createVirtualListItem(node, i === start);
16363
+ // Find the correct parent container
16364
+ let targetUl = ul;
16365
+ if (node.parentId && renderedParentMap.has(node.parentId)) {
16366
+ const parentEntry = renderedParentMap.get(node.parentId);
16367
+ // Lazily create the child <ul> the first time a child needs it
16368
+ if (!parentEntry.childUl) {
16369
+ parentEntry.childUl = this.createElement('ul', {
16370
+ className: PARENTITEM + ' e-ul',
16371
+ attrs: { role: 'group' }
16372
+ });
16373
+ parentEntry.li.appendChild(parentEntry.childUl);
16374
+ }
16375
+ targetUl = parentEntry.childUl;
16376
+ }
16377
+ targetUl.appendChild(li);
16378
+ liListBuf.push(li);
16379
+ // Register this node so its children in the same window can nest under it
16380
+ if (node.expanded && node.hasChildren) {
16381
+ renderedParentMap.set(node.id, { li, childUl: null });
16382
+ }
16383
+ }
16384
+ // ── Restore focus / tabindex ──────────────────────────────────────────────
16385
+ // In virtual mode only the li that carries the 'e-active' class should be
16386
+ // focusable (tabindex='0'). All other rendered li elements must be set
16387
+ // to tabindex='-1' so the tab order remains unambiguous as the window
16388
+ // changes during scrolling.
16389
+ let focusRestored = false;
16390
+ // 1. Prefer the currently active node if it is in the rendered window.
16391
+ for (let k = 0; k < liListBuf.length; k++) {
16392
+ const li = liListBuf[parseInt(k.toString(), 10)];
16393
+ if (li.classList.contains(ACTIVE)) {
16394
+ li.setAttribute('tabindex', '0');
16395
+ // ── Fix: ensure FOCUS class set on active node too ──
16396
+ // This ensures focusIn() guard condition is met: tabindex='0' && FOCUS class
16397
+ addClass([li], FOCUS);
16398
+ focusRestored = true;
16399
+ break;
16400
+ }
16401
+ }
16402
+ // 2. Otherwise fall back to the previously focused node (if it is in window).
16403
+ if (!focusRestored && focusedUid) {
16404
+ for (let k = 0; k < liListBuf.length; k++) {
16405
+ const li = liListBuf[parseInt(k.toString(), 10)];
16406
+ if (li.getAttribute('data-uid') === focusedUid) {
16407
+ li.setAttribute('tabindex', '0');
16408
+ addClass([li], FOCUS);
16409
+ addClass([li], FOCUSED$1);
16410
+ // ── Fix: restore actual browser focus so keyboard events keep working ──
16411
+ // After renderVirtualItems() replaces all <li> elements the old element
16412
+ // is gone from the DOM and document.activeElement falls back to
16413
+ // document.body. Setting tabindex + classes is not enough — the new
16414
+ // element also needs a programmatic .focus() call so subsequent keyboard
16415
+ // events (Arrow keys, Home, End, etc.) are delivered to the TreeView.
16416
+ // Re-attach the blur→focusOut handler so virtualFocusedId is cleared
16417
+ // correctly when the user tabs away.
16418
+ // ── Use preventScroll:true ────────────────────────────────────────────
16419
+ // Without preventScroll the browser scrolls the container to bring this
16420
+ // node into view during the re-render, changing scrollTop AFTER the
16421
+ // correct paddingTop has been applied. This is the root cause of the
16422
+ // KBN expand position-jump bug: the render is correct but the browser
16423
+ // silently scrolls the viewport, making the parent node appear to jump.
16424
+ // ensureVirtualNodeVisible() handles any legitimate scroll-into-view
16425
+ // needs before renderVirtualItems() is called, so scroll here is wrong.
16426
+ EventHandler.add(li, 'blur', this.focusOut, this);
16427
+ li.focus({ preventScroll: true });
16428
+ focusRestored = true;
16429
+ break;
16430
+ }
16431
+ }
16432
+ }
16433
+ // 3. Finally default to the first item in the window.
16434
+ if (!focusRestored && liListBuf.length > 0) {
16435
+ const firstLi = liListBuf[0];
16436
+ firstLi.setAttribute('tabindex', '0');
16437
+ }
16438
+ // ── Fix: Ensure all other rendered nodes have tabindex="-1" ──
16439
+ // This prevents stale tabindex="0" on multiple nodes which breaks Tab order
16440
+ for (let k = 0; k < liListBuf.length; k++) {
16441
+ const li = liListBuf[parseInt(k.toString(), 10)];
16442
+ if (li.getAttribute('tabindex') !== '0') {
16443
+ li.setAttribute('tabindex', '-1');
16444
+ }
16445
+ }
16446
+ // Keep liList in sync so keyboard-navigation helpers stay functional.
16447
+ this.liList = liListBuf;
16448
+ }
16449
+ /**
16450
+ * Creates a single `<li>` element for the virtual window representing `node`.
16451
+ *
16452
+ * @param {VirtualNodeData} node - The node data.
16453
+ * @param {boolean} _isFirst - True if this is the first rendered item (unused here but kept for potential override).
16454
+ * @returns {HTMLElement} The constructed `<li>` element.
16455
+ * @private
16456
+ */
16457
+ createVirtualListItem(node, _isFirst) {
16458
+ const li = this.createElement('li', { className: LISTITEM });
16459
+ // Level class for CSS indentation
16460
+ li.classList.add('e-level-' + node.level);
16461
+ li.setAttribute('data-uid', node.id);
16462
+ li.setAttribute('aria-level', node.level.toString());
16463
+ li.setAttribute('role', 'treeitem');
16464
+ li.setAttribute('tabindex', '-1');
16465
+ // Full-row overlay (required for fullRowSelect mode)
16466
+ if (this.fullRowSelect) {
16467
+ const fullRow = this.createElement('div', { className: FULLROW });
16468
+ li.appendChild(fullRow);
16469
+ }
16470
+ // Text/icon wrapper
16471
+ const textWrap = this.createElement('div', { className: TEXTWRAP });
16472
+ // Expand/collapse icon
16473
+ if (node.hasChildren) {
16474
+ const iconSpan = this.createElement('div', {
16475
+ className: ICON + ' ' + (node.expanded ? COLLAPSIBLE : EXPANDABLE)
16476
+ });
16477
+ textWrap.appendChild(iconSpan);
16478
+ }
16479
+ // Checkbox
16480
+ if (this.showCheckBox) {
16481
+ const checkWrap = createCheckBox(this.createElement, false, {
16482
+ checked: node.isChecked === true,
16483
+ cssClass: CHECKBOXWRAP
16484
+ });
16485
+ if (node.isChecked === 'indeterminate') {
16486
+ const frame = checkWrap.querySelector('.' + CHECKBOXFRAME);
16487
+ if (frame) {
16488
+ frame.classList.add(INDETERMINATE);
16489
+ }
16490
+ }
16491
+ textWrap.appendChild(checkWrap);
16492
+ }
16493
+ // Icon
16494
+ if (node.iconCss) {
16495
+ const iconEl = this.createElement('span', {
16496
+ className: ICON + ' ' + node.iconCss
16497
+ });
16498
+ textWrap.appendChild(iconEl);
16499
+ }
16500
+ // Image
16501
+ if (node.imageUrl) {
16502
+ const img = document.createElement('img');
16503
+ img.src = node.imageUrl;
16504
+ img.classList.add(IMAGE);
16505
+ textWrap.appendChild(img);
16506
+ }
16507
+ // Text anchor
16508
+ const anchor = this.createElement('span', {
16509
+ className: 'e-list-text',
16510
+ innerHTML: node.text
16511
+ });
16512
+ textWrap.appendChild(anchor);
16513
+ li.appendChild(textWrap);
16514
+ // nodeTemplate
16515
+ if (!isNullOrUndefined(this.nodeTemplateFn)) {
16516
+ const rawData = this.findNodeInTreeData(node.id);
16517
+ if (!isNullOrUndefined(rawData)) {
16518
+ anchor.innerHTML = '';
16519
+ this.renderNodeTemplate(rawData, anchor, node.id);
16520
+ }
16521
+ }
16522
+ // State classes
16523
+ if (node.expanded) {
16524
+ li.classList.add(EXPANDED);
16525
+ li.setAttribute('aria-expanded', 'true');
16526
+ }
16527
+ else if (node.hasChildren) {
16528
+ li.classList.add(NODECOLLAPSED);
16529
+ li.setAttribute('aria-expanded', 'false');
16530
+ }
16531
+ if (node.selected) {
16532
+ li.classList.add(ACTIVE);
16533
+ li.setAttribute('aria-selected', 'true');
16534
+ }
16535
+ if (node.hasChildren) {
16536
+ li.classList.add(CHILD);
16537
+ }
16538
+ // Wire the focus event so `focusIn()` fires when the user Tabs into this
16539
+ // item. The non-virtualized path registers this in `beforeNodeCreate()`,
16540
+ // but virtual items never go through that callback, so we must register
16541
+ // it here to keep focus styling (e.g. `e-focused`) consistent between
16542
+ // virtual and non-virtual modes. `unWireEvents()` already removes this
16543
+ // handler during teardown by iterating over the rendered `<li>`s.
16544
+ EventHandler.add(li, 'focus', this.focusIn, this);
16545
+ return li;
16546
+ }
16547
+ /**
16548
+ * Initialises the virtual-scrolling layout:
16549
+ * 1. Adds the `e-virtualization` CSS class and applies a fixed `height` + `overflow-y`.
16550
+ * 2. Flattens all visible nodes into `virtualFlatNodes`.
16551
+ * 3. Measures item height and viewport height.
16552
+ * 4. Computes the initial window and renders it.
16553
+ * 5. Wires the scroll event handler.
16554
+ *
16555
+ * @returns {void}
16556
+ * @private
16557
+ */
16558
+ initVirtualization() {
16559
+ addClass([this.element], VIRTUALIZATION);
16560
+ // Apply container height
16561
+ const px = this.parseHeightToPixels();
16562
+ if (px > 0) {
16563
+ this.element.style.height = px + 'px';
16564
+ this.element.style.overflowY = 'auto';
16565
+ }
16566
+ // Ensure the inner UL is ready
16567
+ if (!this.ulElement) {
16568
+ this.ulElement = select('.' + PARENTITEM, this.element);
16569
+ }
16570
+ if (!this.ulElement) {
16571
+ return;
16572
+ }
16573
+ // Make the UL position-relative for the absolute spacers
16574
+ this.ulElement.style.position = 'relative';
16575
+ // Rebuild flat list
16576
+ this.virtualFlatNodes = this.flattenVisibleNodes();
16577
+ this.rebuildVirtualNodeMap();
16578
+ // Measure dimensions
16579
+ this.virtualViewportHeight = px > 0 ? px : this.element.offsetHeight;
16580
+ // Render a single probe item to measure the real item height before the full render
16581
+ if (this.virtualFlatNodes.length > 0) {
16582
+ const probe = this.createVirtualListItem(this.virtualFlatNodes[0], true);
16583
+ this.ulElement.appendChild(probe);
16584
+ this.virtualItemHeight = probe.offsetHeight > 0 ? probe.offsetHeight : this.virtualItemHeight;
16585
+ this.ulElement.removeChild(probe);
16586
+ }
16587
+ // Compute window and render all items in the initial viewport
16588
+ const win = this.getVirtualWindow();
16589
+ this.virtualStartIndex = win.startIndex;
16590
+ this.virtualEndIndex = win.endIndex;
16591
+ this.renderVirtualItems();
16592
+ // Wire scroll
16593
+ this.wireVirtualScrollEvents();
16594
+ }
16595
+ /**
16596
+ * Wires the scroll event listener on the TreeView element.
16597
+ *
16598
+ * @returns {void}
16599
+ * @private
16600
+ */
16601
+ wireVirtualScrollEvents() {
16602
+ this.unwireVirtualScrollEvents();
16603
+ this.virtualScrollBound = this.onVirtualScroll.bind(this);
16604
+ this.element.addEventListener('scroll', this.virtualScrollBound, { passive: true });
16605
+ }
16606
+ /**
16607
+ * Removes the scroll event listener previously wired by `wireVirtualScrollEvents`.
16608
+ *
16609
+ * @returns {void}
16610
+ * @private
16611
+ */
16612
+ unwireVirtualScrollEvents() {
16613
+ if (this.virtualScrollBound) {
16614
+ this.element.removeEventListener('scroll', this.virtualScrollBound);
16615
+ this.virtualScrollBound = null;
16616
+ }
16617
+ if (this.virtualScrollRAF !== null) {
16618
+ cancelAnimationFrame(this.virtualScrollRAF);
16619
+ this.virtualScrollRAF = null;
16620
+ }
16621
+ }
16622
+ /**
16623
+ * Scroll handler for the virtual TreeView. Throttled via `requestAnimationFrame`.
16624
+ * Shows the loading mask during fast scrolls, re-renders the window, then hides the mask.
16625
+ *
16626
+ * @returns {void}
16627
+ * @private
16628
+ */
16629
+ onVirtualScroll() {
16630
+ if (this.virtualScrollRAF !== null) {
16631
+ cancelAnimationFrame(this.virtualScrollRAF);
16632
+ }
16633
+ // ── Tab-induced focus guard ────────────────────────────────────────────────
16634
+ // When the user Tabs into a `<li tabindex="0">` that is in the middle of
16635
+ // the treeview, the browser auto-scrolls the container so the focused
16636
+ // element becomes visible. That fires a `scroll` event here, but the
16637
+ // virtual window already contains the focused element — re-rendering
16638
+ // would destroy the focused `<li>` and break focus. Detect this case
16639
+ // via the flag set in `keyDownHandler` and skip the re-render entirely.
16640
+ if (this.virtualSkipNextScrollRender) {
16641
+ this.virtualSkipNextScrollRender = false;
16642
+ // Update prev scrollTop so a subsequent user-driven scroll still
16643
+ // calculates the correct delta.
16644
+ this.virtualPrevScrollTop = this.element.scrollTop;
16645
+ return;
16646
+ }
16647
+ const currentScrollTop = this.element.scrollTop;
16648
+ const delta = Math.abs(currentScrollTop - this.virtualPrevScrollTop);
16649
+ const fastScrollThreshold = this.virtualViewportHeight * 2;
16650
+ if (delta > fastScrollThreshold) {
16651
+ this.showVirtualMask(currentScrollTop);
16652
+ }
16653
+ this.virtualScrollRAF = requestAnimationFrame(() => {
16654
+ this.virtualScrollRAF = null;
16655
+ this.virtualPrevScrollTop = this.element.scrollTop;
16656
+ const win = this.getVirtualWindow();
16657
+ this.virtualStartIndex = win.startIndex;
16658
+ this.virtualEndIndex = win.endIndex;
16659
+ this.renderVirtualItems();
16660
+ // Hide the skeleton mask after a short delay so it stays visible long enough
16661
+ // for the user to perceive the loading indicator even on short scrolls.
16662
+ setTimeout(() => {
16663
+ this.hideVirtualMask();
16664
+ // Keep the (now hidden) mask's coords fresh for the next show.
16665
+ this.updateMaskPosition();
16666
+ }, 200);
16667
+ });
16668
+ }
16669
+ /**
16670
+ * Creates the skeleton mask overlay used to indicate loading during fast scrolls.
16671
+ * Skeleton instances are stored in `skeletonInstances` for later disposal.
16672
+ *
16673
+ * @returns {void}
16674
+ * @private
16675
+ */
16676
+ createVirtualMask() {
16677
+ if (this.virtualMaskElement) {
16678
+ return;
16679
+ }
16680
+ // Use position:fixed so the mask is anchored to the visible viewport (not to the
16681
+ // scrolling container). Width/height/left/top are set/refreshed by
16682
+ // updateMaskPosition() so the mask always covers the TreeView's visible area
16683
+ // regardless of how the page itself is scrolled.
16684
+ const rect = this.element.getBoundingClientRect();
16685
+ const scrollBarWidth = this.element.offsetWidth - this.element.clientWidth;
16686
+ const maskParent = this.createElement('div', {
16687
+ className: VIRTUAL_MASK_PARENT,
16688
+ 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;`
16689
+ });
16690
+ const visibleRows = Math.ceil(this.virtualViewportHeight / this.virtualItemHeight) + 1;
16691
+ for (let r = 0; r < visibleRows; r++) {
16692
+ const maskItem = this.createElement('div', {
16693
+ className: VIRTUAL_MASK_ITEM,
16694
+ styles: `display:flex;align-items:center;height:${this.virtualItemHeight}px;padding:0 12px;gap:8px;`
16695
+ });
16696
+ // Skeleton circle for icon placeholder
16697
+ // const iconHolder: HTMLElement = this.createElement('span', {
16698
+ // styles: `width:20px;height:20px;`
16699
+ // });
16700
+ // maskItem.appendChild(iconHolder);
16701
+ // const iconSkel: Skeleton = new Skeleton({
16702
+ // shape: 'Circle',
16703
+ // width: '20px',
16704
+ // height: '20px',
16705
+ // cssClass: 'e-virtual-skel-icon'
16706
+ // });
16707
+ // iconSkel.appendTo(iconHolder);
16708
+ // this.skeletonInstances.push(iconSkel);
16709
+ // Skeleton text bar
16710
+ const textHolder = this.createElement('span');
16711
+ textHolder.style.flex = '1';
16712
+ textHolder.style.height = '14px';
16713
+ maskItem.appendChild(textHolder);
16714
+ const textSkel = new Skeleton({
16715
+ shape: 'Text',
16716
+ width: '80%',
16717
+ height: '14px',
16718
+ cssClass: 'e-virtual-skel-text'
16719
+ });
16720
+ textSkel.appendTo(textHolder);
16721
+ this.skeletonInstances.push(textSkel);
16722
+ maskParent.appendChild(maskItem);
16723
+ }
16724
+ this.virtualMaskElement = maskParent;
16725
+ this.element.appendChild(maskParent);
16726
+ }
16727
+ /**
16728
+ * Refreshes the mask position so it always overlays the TreeView's viewport.
16729
+ * Called on every show / scroll, since `position:fixed` is relative to the
16730
+ * page viewport, not to the TreeView scroll container.
16731
+ *
16732
+ * @returns {void}
16733
+ * @private
16734
+ */
16735
+ updateMaskPosition() {
16736
+ if (!this.virtualMaskElement) {
16737
+ return;
16738
+ }
16739
+ const rect = this.element.getBoundingClientRect();
16740
+ const scrollBarWidth = this.element.offsetWidth - this.element.clientWidth;
16741
+ this.virtualMaskElement.style.top = rect.top + 'px';
16742
+ this.virtualMaskElement.style.left = rect.left + 'px';
16743
+ this.virtualMaskElement.style.width = (rect.width - scrollBarWidth) + 'px';
16744
+ this.virtualMaskElement.style.height = rect.height + 'px';
16745
+ }
16746
+ /**
16747
+ * Destroys all skeleton instances and removes the mask overlay from the DOM.
16748
+ *
16749
+ * @returns {void}
16750
+ * @private
16751
+ */
16752
+ destroyVirtualMask() {
16753
+ for (let i = this.skeletonInstances.length - 1; i >= 0; i--) {
16754
+ this.skeletonInstances[parseInt(i.toString(), 10)].destroy();
16755
+ }
16756
+ this.skeletonInstances = [];
16757
+ if (this.virtualMaskElement) {
16758
+ if (this.virtualMaskElement.parentElement) {
16759
+ this.virtualMaskElement.parentElement.removeChild(this.virtualMaskElement);
16760
+ }
16761
+ this.virtualMaskElement = null;
16762
+ }
16763
+ }
16764
+ /**
16765
+ * Positions and shows the skeleton mask at the current `scrollTop` position.
16766
+ *
16767
+ * @param {number} _scrollTop - Current scrollTop of the container.
16768
+ * @returns {void}
16769
+ * @private
16770
+ */
16771
+ showVirtualMask(_scrollTop) {
16772
+ if (!this.virtualMaskElement) {
16773
+ this.createVirtualMask();
16774
+ }
16775
+ if (this.virtualMaskElement) {
16776
+ // Refresh the fixed-position coords in case the TreeView was moved/resized
16777
+ // since the last time the mask was shown.
16778
+ this.updateMaskPosition();
16779
+ this.virtualMaskElement.style.display = 'block';
16780
+ // Visually hide the real list while the skeleton is shown
16781
+ if (this.ulElement) {
16782
+ this.ulElement.style.visibility = 'hidden';
16783
+ }
16784
+ }
16785
+ }
16786
+ /**
16787
+ * Hides the skeleton mask overlay after the virtual window has been rendered.
16788
+ *
16789
+ * @returns {void}
16790
+ * @private
16791
+ */
16792
+ hideVirtualMask() {
16793
+ if (this.virtualMaskElement) {
16794
+ this.virtualMaskElement.style.display = 'none';
16795
+ // Restore real list visibility
16796
+ if (this.ulElement) {
16797
+ this.ulElement.style.visibility = '';
16798
+ }
16799
+ }
16800
+ }
16801
+ /**
16802
+ * Ensures the node identified by `id` is scrolled into view inside the virtual container.
16803
+ * If the node falls outside the rendered window, the container's `scrollTop` is adjusted so
16804
+ * that the node becomes visible, the window is re-computed and `renderVirtualItems` is called.
16805
+ *
16806
+ * @param {string} id - The `data-uid` of the target node.
16807
+ * @returns {void}
16808
+ * @private
16809
+ */
16810
+ ensureVirtualNodeVisible(id) {
16811
+ const idx = this.virtualNodeMap.get(id);
16812
+ if (isNullOrUndefined(idx)) {
16813
+ return;
16814
+ }
16815
+ const itemH = this.virtualItemHeight;
16816
+ const vpH = this.virtualViewportHeight;
16817
+ const nodeTop = idx * itemH;
16818
+ const nodeBottom = nodeTop + itemH;
16819
+ const currentTop = this.element.scrollTop;
16820
+ const currentBottom = currentTop + vpH;
16821
+ if (nodeTop < currentTop) {
16822
+ this.element.scrollTop = nodeTop;
16823
+ }
16824
+ else if (nodeBottom > currentBottom) {
16825
+ this.element.scrollTop = nodeBottom - vpH;
16826
+ }
16827
+ else {
16828
+ return; // already visible, no re-render needed
16829
+ }
16830
+ // Re-render the new window synchronously so the <li> is in the DOM
16831
+ const win = this.getVirtualWindow();
16832
+ this.virtualStartIndex = win.startIndex;
16833
+ this.virtualEndIndex = win.endIndex;
16834
+ this.renderVirtualItems();
16835
+ }
16836
+ /**
16837
+ * Recursively walks the treeData (and any nested `childFieldName` arrays) to
16838
+ * find the node whose `idFieldName` matches the given `id` (compared as
16839
+ * strings). Used by the virtual-mode expand/collapse handlers so they can
16840
+ * locate nodes at any depth — not just the root level — before writing the
16841
+ * `expanded` flag back to the datasource.
16842
+ *
16843
+ * @param {string} id - The id (as a string) of the node to locate.
16844
+ * @returns {Object | null} The matching raw datasource node, or null if not found.
16845
+ * @private
16846
+ */
16847
+ findNodeInTreeData(id) {
16848
+ if (!this.treeData) {
16849
+ return null;
16850
+ }
16851
+ const idFieldName = this.fields.id.toString();
16852
+ const childFieldName = this.fields.child.toString();
16853
+ const findRecursive = (nodes) => {
16854
+ if (!nodes || nodes.length === 0) {
16855
+ return null;
16856
+ }
16857
+ for (let i = 0; i < nodes.length; i++) {
16858
+ const node = nodes[i];
16859
+ if (node[idFieldName] && node[idFieldName].toString() === id) {
16860
+ return node;
16861
+ }
16862
+ const children = getValue(childFieldName, node);
16863
+ if (children && Array.isArray(children)) {
16864
+ const found = findRecursive(children);
16865
+ if (found) {
16866
+ return found;
16867
+ }
16868
+ }
16869
+ }
16870
+ return null;
16871
+ };
16872
+ return findRecursive(this.treeData);
16873
+ }
16874
+ handleVirtualExpand(id, isKeyboardNav) {
16875
+ // ── Cancel any pending scroll RAF ─────────────────────────────────────────
16876
+ // A scroll RAF queued before the expand click would run asynchronously after
16877
+ // this synchronous expand completes, re-rendering the window from a stale
16878
+ // scrollTop and moving the parent node to the wrong position. Cancel it now
16879
+ // so the expand render is authoritative.
16880
+ if (this.virtualScrollRAF !== null) {
16881
+ cancelAnimationFrame(this.virtualScrollRAF);
16882
+ this.virtualScrollRAF = null;
16883
+ }
16884
+ if (this.expandedNodes.indexOf(id) === -1) {
16885
+ this.expandedNodes.push(id);
16886
+ }
16887
+ // Also update virtualExpandedNodesSet to keep it in sync
16888
+ if (this.virtualExpandedNodesSet) {
16889
+ this.virtualExpandedNodesSet.add(id);
16890
+ }
16891
+ if (this.treeData) {
16892
+ const acutalRootData = this.findNodeInTreeData(id);
16893
+ if (acutalRootData && this.fields && this.fields.expanded) {
16894
+ setValue(this.fields.expanded, true, acutalRootData);
16895
+ }
16896
+ }
16897
+ // ── Fix: Pin focused ID to the expanding node for keyboard navigation ──
16898
+ // When keyboard-triggered, the expanded node IS definitively the focused node
16899
+ // (`id` comes from currLi.getAttribute('data-uid') in expandNode, where currLi
16900
+ // is getFocusedNode()). Set unconditionally — the old conditional guard
16901
+ // `!this.virtualFocusedId` could leave a stale or null value if a previous
16902
+ // RAF render (from arrow-key scroll) hadn't yet completed, causing branch 3
16903
+ // (first item) to fire in renderVirtualItems() instead of branch 2.
16904
+ // For mouse clicks (isKeyboardNav=false), clear virtualFocusedId so that
16905
+ // renderVirtualItems() Branch 2 does NOT apply the FOCUS/FOCUSED classes —
16906
+ // mouse-based expand/collapse should never add the e-focused class.
16907
+ if (isKeyboardNav) {
16908
+ this.virtualFocusedId = id;
16909
+ }
16910
+ else {
16911
+ this.virtualFocusedId = null;
16912
+ }
16913
+ this.virtualFlatNodes = this.flattenVisibleNodes();
16914
+ this.rebuildVirtualNodeMap();
16915
+ // ── Snapshot scrollTop before computing the window ────────────────────────
16916
+ // Read scrollTop once here and pass it into getVirtualWindow so that any
16917
+ // browser scroll-anchoring adjustment that happens when the padding values
16918
+ // change cannot affect which window we render.
16919
+ const scrollTopSnapshot = this.element.scrollTop;
16920
+ const win = this.getVirtualWindowAt(scrollTopSnapshot);
16921
+ this.virtualStartIndex = win.startIndex;
16922
+ this.virtualEndIndex = win.endIndex;
16923
+ // ── Block ALL scroll-triggered re-renders during this expand ─────────────
16924
+ // renderVirtualItems() calls li.focus() in branch 2 (keyboard focus restore).
16925
+ // The post-render focus() call below can also trigger browser scroll. Both
16926
+ // can fire the 'scroll' event and cause onVirtualScroll() to queue a RAF.
16927
+ // We set the skip flag ONCE here; it is consumed by the first scroll event.
16928
+ // To cover the second potential scroll (from the focus() call below) we use
16929
+ // preventScroll:true on that call so it never moves the container at all.
16930
+ this.virtualSkipNextScrollRender = true;
16931
+ this.renderVirtualItems();
16932
+ // renderVirtualItems() replaces all <li> elements so the old icon element that
16933
+ // had the PROCESS class is gone. Remove PROCESS from the newly rendered icon
16934
+ // so the node can be collapsed/expanded again without being blocked.
16935
+ const newLi = this.getElement(id);
16936
+ if (newLi) {
16937
+ const icon = select('.' + ICON, newLi);
16938
+ if (icon) {
16939
+ removeClass([icon], PROCESS);
16940
+ }
16941
+ // ── Use preventScroll:true ────────────────────────────────────────────
16942
+ // Without preventScroll the browser scrolls the container to bring this
16943
+ // node into view, which fires a second 'scroll' event. The first scroll
16944
+ // was already consumed by virtualSkipNextScrollRender (set above), so the
16945
+ // second scroll would queue a new RAF that re-renders from the
16946
+ // focus-adjusted scrollTop — moving the parent node to the wrong position.
16947
+ newLi.focus({ preventScroll: true });
16948
+ }
16949
+ if (this.isLoaded) {
16950
+ const currLi = this.getElement(id);
16951
+ if (currLi) {
16952
+ const expandArgs = this.getExpandEvent(currLi, null);
16953
+ expandArgs.isInteracted = this.isInteracted;
16954
+ this.trigger('nodeExpanded', expandArgs);
16955
+ }
16956
+ }
16957
+ }
16958
+ handleVirtualCollapse(id, isKeyboardNav) {
16959
+ // ── Cancel any pending scroll RAF ─────────────────────────────────────────
16960
+ // Mirrors handleVirtualExpand: a queued RAF from a previous scroll event
16961
+ // must not override the collapse render with a stale window.
16962
+ if (this.virtualScrollRAF !== null) {
16963
+ cancelAnimationFrame(this.virtualScrollRAF);
16964
+ this.virtualScrollRAF = null;
16965
+ }
16966
+ const idx = this.expandedNodes.indexOf(id);
16967
+ if (idx !== -1) {
16968
+ this.expandedNodes.splice(idx, 1);
16969
+ }
16970
+ // Also update virtualExpandedNodesSet to keep it in sync
16971
+ if (this.virtualExpandedNodesSet) {
16972
+ this.virtualExpandedNodesSet.delete(id);
16973
+ }
16974
+ if (this.treeData) {
16975
+ const acutalRootData = this.findNodeInTreeData(id);
16976
+ if (acutalRootData && this.fields && this.fields.expanded) {
16977
+ setValue(this.fields.expanded, false, acutalRootData);
16978
+ }
16979
+ }
16980
+ // ── Fix: Pin focused ID to the collapsing node for keyboard navigation ──
16981
+ // Same reasoning as handleVirtualExpand: when keyboard-triggered, `id` is
16982
+ // definitively the focused node. Set unconditionally to avoid the stale/null
16983
+ // state that could cause the RAF re-render to fall through to branch 3.
16984
+ // For mouse clicks (isKeyboardNav=false), clear virtualFocusedId so that
16985
+ // renderVirtualItems() Branch 2 does NOT apply the FOCUS/FOCUSED classes —
16986
+ // mouse-based expand/collapse should never add the e-focused class.
16987
+ if (isKeyboardNav) {
16988
+ this.virtualFocusedId = id;
16989
+ }
16990
+ else {
16991
+ this.virtualFocusedId = null;
16992
+ }
16993
+ this.virtualFlatNodes = this.flattenVisibleNodes();
16994
+ this.rebuildVirtualNodeMap();
16995
+ // ── Snapshot scrollTop before computing the window ────────────────────────
16996
+ // Same rationale as handleVirtualExpand: capture scrollTop before any focus()
16997
+ // call or padding change can cause the browser to auto-scroll.
16998
+ const scrollTopSnapshot = this.element.scrollTop;
16999
+ const win = this.getVirtualWindowAt(scrollTopSnapshot);
17000
+ this.virtualStartIndex = win.startIndex;
17001
+ this.virtualEndIndex = win.endIndex;
17002
+ // ── Block ALL scroll-triggered re-renders during this collapse ────────────
17003
+ // Same as handleVirtualExpand: use preventScroll:true on the focus() call
17004
+ // below so the second potential scroll event is never fired.
17005
+ this.virtualSkipNextScrollRender = true;
17006
+ this.renderVirtualItems();
17007
+ // renderVirtualItems() replaces all <li> elements so the old icon element that
17008
+ // had the PROCESS class is gone. Remove PROCESS from the newly rendered icon
17009
+ // so the node can be expanded/collapsed again without being blocked.
17010
+ const newLi = this.getElement(id);
17011
+ if (newLi) {
17012
+ const icon = select('.' + ICON, newLi);
17013
+ if (icon) {
17014
+ removeClass([icon], PROCESS);
17015
+ }
17016
+ // ── Use preventScroll:true to avoid consuming virtualSkipNextScrollRender ─
17017
+ newLi.focus({ preventScroll: true });
17018
+ }
17019
+ if (this.isLoaded) {
17020
+ const currLi = this.getElement(id);
17021
+ if (currLi) {
17022
+ const colArgs = this.getExpandEvent(currLi, null);
17023
+ colArgs.isInteracted = this.isInteracted;
17024
+ this.trigger('nodeCollapsed', colArgs);
17025
+ }
17026
+ }
17027
+ }
17028
+ /**
17029
+ * Updates the selected state of a node in `virtualFlatNodes` and re-renders the window.
17030
+ *
17031
+ * @param {string} id - The `data-uid` of the target node.
17032
+ * @param {boolean} selected - Whether the node is now selected.
17033
+ * @returns {void}
17034
+ * @private
17035
+ */
17036
+ updateVirtualNodeSelection(id, selected) {
17037
+ const idx = this.virtualNodeMap.get(id);
17038
+ if (!isNullOrUndefined(idx)) {
17039
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].selected = selected;
17040
+ this.renderVirtualItems();
17041
+ }
17042
+ }
17043
+ /**
17044
+ * Updates the checkbox state of a node in `virtualFlatNodes` and re-renders the window.
17045
+ *
17046
+ * @param {string} id - The `data-uid` of the target node.
17047
+ * @param {'check' | 'uncheck' | 'indeterminate'} state - The new check state.
17048
+ * @returns {void}
17049
+ * @private
17050
+ */
17051
+ updateVirtualNodeCheckState(id, state) {
17052
+ const idx = this.virtualNodeMap.get(id);
17053
+ if (!isNullOrUndefined(idx)) {
17054
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].isChecked =
17055
+ state === 'check' ? true : state === 'indeterminate' ? 'indeterminate' : false;
17056
+ // Keep the persistent indeterminate set in sync so flattenVisibleNodes()
17057
+ // correctly restores state after any full list rebuild (e.g. collapse).
17058
+ if (state === 'indeterminate') {
17059
+ this.virtualIndeterminateNodesSet.add(id);
17060
+ }
17061
+ else {
17062
+ this.virtualIndeterminateNodesSet.delete(id);
17063
+ }
17064
+ this.renderVirtualItems();
17065
+ }
17066
+ }
17067
+ // ═══════════════════════════════════════════════════════════════════════════════
17068
+ // END VIRTUALIZATION
17069
+ // ═══════════════════════════════════════════════════════════════════════════════
15600
17070
  /**
15601
17071
  * Called internally if any of the property value changed.
15602
17072
  *
@@ -15733,6 +17203,33 @@ let TreeView = TreeView_1 = class TreeView extends Component {
15733
17203
  this.listBaseOption.itemNavigable = newProp.fullRowNavigable;
15734
17204
  this.reRenderNodes();
15735
17205
  break;
17206
+ case 'enableVirtualization':
17207
+ if (this.enableVirtualization) {
17208
+ if (!isNullOrUndefined(this.height)) {
17209
+ this.initVirtualization();
17210
+ }
17211
+ }
17212
+ else {
17213
+ // Tear down virtualization and do a full re-render
17214
+ this.unwireVirtualScrollEvents();
17215
+ this.destroyVirtualMask();
17216
+ removeClass([this.element], VIRTUALIZATION);
17217
+ this.element.style.height = '';
17218
+ this.element.style.overflowY = '';
17219
+ this.reRenderNodes();
17220
+ }
17221
+ break;
17222
+ case 'height':
17223
+ if (this.enableVirtualization) {
17224
+ // Re-initialise so the new height takes effect
17225
+ this.unwireVirtualScrollEvents();
17226
+ this.initVirtualization();
17227
+ }
17228
+ else {
17229
+ const px = this.parseHeightToPixels();
17230
+ this.element.style.height = px > 0 ? px + 'px' : '';
17231
+ }
17232
+ break;
15736
17233
  }
15737
17234
  }
15738
17235
  }
@@ -15742,6 +17239,13 @@ let TreeView = TreeView_1 = class TreeView extends Component {
15742
17239
  * @returns {void}
15743
17240
  */
15744
17241
  destroy() {
17242
+ // Tear down virtualization before standard cleanup
17243
+ if (this.enableVirtualization) {
17244
+ this.unwireVirtualScrollEvents();
17245
+ this.destroyVirtualMask();
17246
+ this.virtualFlatNodes = [];
17247
+ this.virtualNodeMap.clear();
17248
+ }
15745
17249
  this.clearTemplate();
15746
17250
  this.element.removeAttribute('aria-activedescendant');
15747
17251
  this.unWireEvents();
@@ -15826,9 +17330,25 @@ let TreeView = TreeView_1 = class TreeView extends Component {
15826
17330
  if (isNullOrUndefined(fNode)) {
15827
17331
  this.setNodeFocusable();
15828
17332
  }
15829
- this.updateCheckedStateFromDS();
15830
- if (this.showCheckBox && dropLi && this.autoCheck) {
15831
- this.ensureParentCheckState(dropLi);
17333
+ // In virtual mode (with a fixed height), rebuild the flat node list and re-render
17334
+ // the window so the newly added nodes appear in the correct hierarchical position.
17335
+ // When height is not provided, the tree uses normal rendering and the DOM is
17336
+ // already updated by addGivenNodes() above.
17337
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
17338
+ this.setRootData();
17339
+ this.rebuildVirtualCheckCache();
17340
+ this.virtualFlatNodes = this.flattenVisibleNodes();
17341
+ this.rebuildVirtualNodeMap();
17342
+ const win = this.getVirtualWindow();
17343
+ this.virtualStartIndex = win.startIndex;
17344
+ this.virtualEndIndex = win.endIndex;
17345
+ this.renderVirtualItems();
17346
+ }
17347
+ else {
17348
+ this.updateCheckedStateFromDS();
17349
+ if (this.showCheckBox && dropLi && this.autoCheck) {
17350
+ this.ensureParentCheckState(dropLi);
17351
+ }
15832
17352
  }
15833
17353
  if ((this.fields.dataSource instanceof DataManager === false)) {
15834
17354
  this.preventExpand = false;
@@ -15873,6 +17393,24 @@ let TreeView = TreeView_1 = class TreeView extends Component {
15873
17393
  * @returns {void}
15874
17394
  */
15875
17395
  collapseAll(nodes, level, excludeHiddenNodes) {
17396
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
17397
+ this.virtualExpandedNodesSet.clear();
17398
+ this.setProperties({ expandedNodes: [] }, true);
17399
+ const setAllCollapsed = (nodes) => {
17400
+ if (!nodes) {
17401
+ return;
17402
+ }
17403
+ for (let i = 0; i < nodes.length; i++) {
17404
+ const node = nodes[parseInt(i.toString(), 10)];
17405
+ node['expanded'] = false;
17406
+ const children = getValue(this.fields.child.toString(), node);
17407
+ if (children && Array.isArray(children)) {
17408
+ setAllCollapsed(children);
17409
+ }
17410
+ }
17411
+ };
17412
+ setAllCollapsed(this.treeData);
17413
+ }
15876
17414
  if (!isNullOrUndefined(nodes)) {
15877
17415
  this.doGivenAction(nodes, COLLAPSIBLE, false);
15878
17416
  }
@@ -15950,6 +17488,33 @@ let TreeView = TreeView_1 = class TreeView extends Component {
15950
17488
  */
15951
17489
  expandAll(nodes, level, excludeHiddenNodes, preventAnimation) {
15952
17490
  this.isAnimate = !preventAnimation;
17491
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
17492
+ const allParentIds = [];
17493
+ const idFieldName = this.fields.id;
17494
+ const childFieldName = this.fields.child.toString();
17495
+ const isDataTypeFlat = this.dataType === 1;
17496
+ const processAllParents = (nodeList) => {
17497
+ if (!nodeList) {
17498
+ return;
17499
+ }
17500
+ for (let i = 0; i < nodeList.length; i++) {
17501
+ const node = nodeList[parseInt(i.toString(), 10)];
17502
+ const nodeId = getValue(idFieldName, node);
17503
+ if (node.hasChild) {
17504
+ allParentIds.push(nodeId);
17505
+ node.expanded = true;
17506
+ }
17507
+ }
17508
+ };
17509
+ processAllParents(this.treeData);
17510
+ for (let i = 0; i < allParentIds.length; i++) {
17511
+ const id = allParentIds[parseInt(i.toString(), 10)];
17512
+ if (!this.virtualExpandedNodesSet.has(id)) {
17513
+ this.virtualExpandedNodesSet.add(id);
17514
+ }
17515
+ }
17516
+ this.setProperties({ expandedNodes: allParentIds }, true);
17517
+ }
15953
17518
  if (!isNullOrUndefined(nodes)) {
15954
17519
  this.doGivenAction(nodes, EXPANDABLE, true);
15955
17520
  }
@@ -16251,6 +17816,12 @@ __decorate$8([
16251
17816
  __decorate$8([
16252
17817
  Property(false)
16253
17818
  ], TreeView.prototype, "allowTextWrap", void 0);
17819
+ __decorate$8([
17820
+ Property(false)
17821
+ ], TreeView.prototype, "enableVirtualization", void 0);
17822
+ __decorate$8([
17823
+ Property()
17824
+ ], TreeView.prototype, "height", void 0);
16254
17825
  __decorate$8([
16255
17826
  Complex({}, NodeAnimationSettings)
16256
17827
  ], TreeView.prototype, "animation", void 0);