@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 __extends = (undefined && undefined.__extends) || (function () {
9
10
  var extendStatics = function (d, b) {
@@ -1505,8 +1506,10 @@ var MenuBase = /** @__PURE__ @class */ (function (_super) {
1505
1506
  if (!active) {
1506
1507
  return;
1507
1508
  }
1509
+ var isInputElement = active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' ||
1510
+ active.contentEditable === 'true';
1508
1511
  var expanded = active.getAttribute('aria-expanded') === 'true';
1509
- if (!expanded) {
1512
+ if (!expanded || isInputElement) {
1510
1513
  return;
1511
1514
  }
1512
1515
  var ul = this.element;
@@ -1620,13 +1623,19 @@ var MenuBase = /** @__PURE__ @class */ (function (_super) {
1620
1623
  var cli = cul.children[fliIdx];
1621
1624
  fliIdx = this.isValidLI(cli, fliIdx, e.action);
1622
1625
  cul.children[fliIdx].classList.add(FOCUSED);
1623
- cul.children[fliIdx].focus();
1626
+ var liToFocus = cul.children[fliIdx];
1624
1627
  if (closest(this.element, '.e-popup')) {
1625
- var focusedLiItem = cul.children[fliIdx];
1626
- var focusableChild = focusedLiItem.querySelector('[tabindex], span[tabindex], a, button, [tabindex]:not([tabindex="-1"])');
1627
- if (!focusableChild) {
1628
- cul.focus();
1628
+ var focusableChild = liToFocus.querySelector('[tabindex], span[tabindex], a, button, [tabindex]:not([tabindex="-1"])');
1629
+ var isSpreadsheet = this.element.id.includes('_cf_menu') && this.element.parentElement.classList.contains('e-cf-menu');
1630
+ if (focusableChild && !isSpreadsheet) {
1631
+ focusableChild.focus();
1629
1632
  }
1633
+ else {
1634
+ liToFocus.focus();
1635
+ }
1636
+ }
1637
+ else {
1638
+ liToFocus.focus();
1630
1639
  }
1631
1640
  };
1632
1641
  MenuBase.prototype.isValidLI = function (cli, index, action) {
@@ -10873,6 +10882,9 @@ var CHILD = 'e-has-child';
10873
10882
  var ITEM_ANIMATION_ACTIVE = 'e-animation-active';
10874
10883
  var DISABLED$1 = 'e-disabled';
10875
10884
  var PREVENTSELECT = 'e-prevent';
10885
+ var VIRTUALIZATION = 'e-virtualization';
10886
+ var VIRTUAL_MASK_PARENT = 'e-mask-parent';
10887
+ var VIRTUAL_MASK_ITEM = 'e-mask-item';
10876
10888
  var treeAriaAttr = {
10877
10889
  treeRole: 'group',
10878
10890
  itemRole: 'treeitem',
@@ -11012,6 +11024,55 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
11012
11024
  _this.isDropIn = false;
11013
11025
  _this.OldCheckedData = [];
11014
11026
  _this.isHiddenItem = false;
11027
+ // ── Virtualization state ──────────────────────────────────────────────────────────────
11028
+ /** Flat ordered list of all currently-visible (non-collapsed) tree nodes. */
11029
+ _this.virtualFlatNodes = [];
11030
+ /** Maps node id → index in virtualFlatNodes for O(1) lookups. */
11031
+ _this.virtualNodeMap = new Map();
11032
+ /** Set of expanded node IDs for O(1) lookup performance during flattening (avoids array indexOf). */
11033
+ _this.virtualExpandedNodesSet = new Set();
11034
+ /** Set of checked node IDs for O(1) lookup performance during flattening. */
11035
+ _this.virtualCheckedNodesSet = new Set();
11036
+ /** Set of indeterminate node IDs — persists across flattenVisibleNodes() rebuilds. */
11037
+ _this.virtualIndeterminateNodesSet = new Set();
11038
+ /** Set of selected node IDs for O(1) lookup performance during flattening. */
11039
+ _this.virtualSelectedNodesSet = new Set();
11040
+ /** Row height in pixels, measured from the first rendered item or set to 40 as a default. */
11041
+ _this.virtualItemHeight = 40;
11042
+ /** Height of the scrollable container in pixels. */
11043
+ _this.virtualViewportHeight = 0;
11044
+ /** scrollTop captured during the previous scroll handler invocation. */
11045
+ _this.virtualPrevScrollTop = 0;
11046
+ /** Index of the first rendered row in the current window. */
11047
+ _this.virtualStartIndex = 0;
11048
+ /** Index one past the last rendered row in the current window. */
11049
+ _this.virtualEndIndex = 0;
11050
+ /** Extra rows rendered above and below the visible window to smooth scrolling. */
11051
+ _this.virtualOverscanCount = 3;
11052
+ /** RequestAnimationFrame handle used for throttling scroll events. */
11053
+ _this.virtualScrollRAF = null;
11054
+ /** The `.e-mask-parent` overlay element shown during fast scroll. */
11055
+ _this.virtualMaskElement = null;
11056
+ /** Bound scroll handler stored for later removeEventListener. */
11057
+ _this.virtualScrollBound = null;
11058
+ /** Skeleton component instances inside the mask overlay. */
11059
+ _this.skeletonInstances = [];
11060
+ /** Set to true whenever the flat node list must be rebuilt on next render. */
11061
+ _this.virtualTreeDirty = false;
11062
+ /** The data-uid of the currently keyboard-focused node in virtual mode (persists across re-renders). */
11063
+ _this.virtualFocusedId = null;
11064
+ /**
11065
+ * Set to true by the document-level `keydown` handler when the user presses
11066
+ * the Tab key. The browser will auto-scroll the focused element into view
11067
+ * immediately after focus, which fires a `scroll` event on the TreeView
11068
+ * container. We want to skip the re-render that the virtual scroll handler
11069
+ * would otherwise trigger, because the element being focused is already
11070
+ * rendered (it is the single `<li tabindex="0">` of the current window) and
11071
+ * re-rendering would tear down the DOM element that is currently holding
11072
+ * focus. The flag is consumed (reset to `false`) inside `onVirtualScroll`
11073
+ * after the auto-scroll has been handled.
11074
+ */
11075
+ _this.virtualSkipNextScrollRender = false;
11015
11076
  return _this;
11016
11077
  }
11017
11078
  TreeView_1 = TreeView;
@@ -11313,6 +11374,24 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
11313
11374
  }
11314
11375
  };
11315
11376
  TreeView.prototype.renderItems = function (isSorted) {
11377
+ // In virtual mode (with a fixed height) create only the container <ul> —
11378
+ // initVirtualization will populate the items directly, avoiding the O(n)
11379
+ // ListBase.createList cost.
11380
+ // updateCheckedStateFromDS / updateParentCheckState are intentionally skipped here:
11381
+ // they call getCheckedNodeDetails which traverses the DOM (no DOM exists yet in
11382
+ // virtual mode) and would throw on empty recursion results. The initial checked
11383
+ // state is derived from the raw data inside flattenNodeRecursive instead.
11384
+ //
11385
+ // When enableVirtualization is true but height is NOT provided, we fall through
11386
+ // to the normal (non-virtualized) rendering path so the tree is still visible.
11387
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
11388
+ this.ulElement = this.createElement('ul', {
11389
+ className: PARENTITEM + ' ' + 'e-ul',
11390
+ attrs: { role: 'group' }
11391
+ });
11392
+ this.element.appendChild(this.ulElement);
11393
+ return;
11394
+ }
11316
11395
  this.listBaseOption.ariaAttributes.level = 1;
11317
11396
  var sortedData = this.getSortedData(this.rootData);
11318
11397
  this.ulElement = ListBase.createList(this.createElement, isSorted ? this.rootData : sortedData, this.listBaseOption);
@@ -12359,7 +12438,7 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
12359
12438
  else if (state === 'indeterminate' && this.autoCheck) {
12360
12439
  frameSpan.classList.remove(CHECK);
12361
12440
  frameSpan.classList.add(INDETERMINATE);
12362
- this.removeCheck(currLi);
12441
+ this.removeCheck(currLi, true);
12363
12442
  ariaState = 'mixed';
12364
12443
  }
12365
12444
  ariaState = state === 'check' ? 'true' : state === 'uncheck' ? 'false' : ariaState;
@@ -12390,12 +12469,112 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
12390
12469
  if (!isNullOrUndefined(id) && this.checkedNodes.indexOf(id) === -1) {
12391
12470
  this.checkDisabledState(id);
12392
12471
  }
12472
+ if (this.enableVirtualization) {
12473
+ // Clear any indeterminate state — node is now fully checked
12474
+ this.virtualIndeterminateNodesSet.delete(id);
12475
+ var actualIndex = this.virtualNodeMap.get(id);
12476
+ if (!isNullOrUndefined(actualIndex) && actualIndex > -1) {
12477
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = true;
12478
+ if (this.autoCheck && this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].hasChildren) {
12479
+ this.setVirtualChildCheckState(id, true);
12480
+ }
12481
+ }
12482
+ }
12393
12483
  };
12394
- TreeView.prototype.removeCheck = function (liEle) {
12395
- var index = this.checkedNodes.indexOf(liEle.getAttribute('data-uid'));
12484
+ TreeView.prototype.removeCheck = function (liEle, isIndeterminate) {
12485
+ var id = liEle.getAttribute('data-uid');
12486
+ var index = this.checkedNodes.indexOf(id);
12396
12487
  if (index > -1) {
12397
12488
  this.checkedNodes.splice(index, 1);
12398
12489
  }
12490
+ if (this.enableVirtualization) {
12491
+ var actualIndex = this.virtualNodeMap.get(id);
12492
+ if (!isNullOrUndefined(actualIndex) && actualIndex > -1) {
12493
+ if (isIndeterminate) {
12494
+ // Parent is transitioning to indeterminate — preserve children states
12495
+ // and mark this node as 'indeterminate' so scroll/collapse re-renders correctly.
12496
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = 'indeterminate';
12497
+ // Also record it in the persistent indeterminate set so flattenVisibleNodes()
12498
+ // can restore this state after a full rebuild (e.g. on collapse).
12499
+ this.virtualIndeterminateNodesSet.add(id);
12500
+ }
12501
+ else {
12502
+ // Node is being fully unchecked — clear indeterminate tracking too
12503
+ this.virtualIndeterminateNodesSet.delete(id);
12504
+ this.virtualCheckedNodesSet.delete(id);
12505
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].isChecked = false;
12506
+ if (this.treeData && this.fields && this.fields.isChecked) {
12507
+ setValue(this.fields.isChecked, false, this.treeData[parseInt(id.toString(), 10) - 1]);
12508
+ }
12509
+ if (this.autoCheck && this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].hasChildren) {
12510
+ this.setVirtualChildCheckState(id, false);
12511
+ }
12512
+ }
12513
+ }
12514
+ }
12515
+ };
12516
+ /**
12517
+ * Recursively sets the `isChecked` state of all descendants of the node with
12518
+ * `parentId` in `virtualFlatNodes`, keeping `checkedNodes` and
12519
+ * `virtualCheckedNodesSet` in sync. Used in virtual mode to honor the
12520
+ * `autoCheck` cascade when a parent checkbox is toggled.
12521
+ *
12522
+ * @param {string} parentId - The `data-uid` of the parent whose descendants should be updated.
12523
+ * @param {boolean} isChecked - The new state to apply to every descendant.
12524
+ * @returns {void}
12525
+ * @private
12526
+ */
12527
+ TreeView.prototype.setVirtualChildCheckState = function (parentId, isChecked) {
12528
+ if (!this.virtualFlatNodes || this.virtualFlatNodes.length === 0) {
12529
+ return;
12530
+ }
12531
+ for (var i = 0; i < this.virtualFlatNodes.length; i++) {
12532
+ var node = this.virtualFlatNodes[parseInt(i.toString(), 10)];
12533
+ if (node.parentId === parentId) {
12534
+ node.isChecked = isChecked;
12535
+ if (isChecked) {
12536
+ this.virtualCheckedNodesSet.add(node.id);
12537
+ if (this.checkedNodes && this.checkedNodes.indexOf(node.id) === -1) {
12538
+ this.checkedNodes.push(node.id);
12539
+ }
12540
+ }
12541
+ else {
12542
+ this.virtualCheckedNodesSet.delete(node.id);
12543
+ var idx = this.checkedNodes ? this.checkedNodes.indexOf(node.id) : -1;
12544
+ if (idx !== -1) {
12545
+ this.checkedNodes.splice(idx, 1);
12546
+ }
12547
+ }
12548
+ if (node.hasChildren) {
12549
+ this.setVirtualChildCheckState(node.id, isChecked);
12550
+ }
12551
+ }
12552
+ }
12553
+ // Collapsed children are not present in virtualFlatNodes, so walk the raw data
12554
+ // to keep this.treeData and checkedNodes in sync for hidden descendants too.
12555
+ var collapsedChildren = this.getChildNodes(this.treeData, parentId);
12556
+ if (collapsedChildren) {
12557
+ for (var i = 0; i < collapsedChildren.length; i++) {
12558
+ var childId = collapsedChildren[parseInt(i.toString(), 10)][this.fields.id]
12559
+ ? collapsedChildren[parseInt(i.toString(), 10)][this.fields.id].toString()
12560
+ : null;
12561
+ if (childId && (!this.virtualNodeMap || !this.virtualNodeMap.has(childId))) {
12562
+ this.updateField(this.treeData, this.fields, childId, this.fields.isChecked, isChecked);
12563
+ if (isChecked) {
12564
+ if (this.checkedNodes && this.checkedNodes.indexOf(childId) === -1) {
12565
+ this.checkedNodes.push(childId);
12566
+ }
12567
+ }
12568
+ else {
12569
+ var idx = this.checkedNodes ? this.checkedNodes.indexOf(childId) : -1;
12570
+ if (idx !== -1) {
12571
+ this.checkedNodes.splice(idx, 1);
12572
+ }
12573
+ }
12574
+ this.setVirtualChildCheckState(childId, isChecked);
12575
+ }
12576
+ }
12577
+ }
12399
12578
  };
12400
12579
  TreeView.prototype.getCheckEvent = function (currLi, action, e) {
12401
12580
  this.checkActionNodes.push(this.getNodeData(currLi));
@@ -12427,6 +12606,9 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
12427
12606
  return;
12428
12607
  }
12429
12608
  var liEle = ulEle ? selectAll('.' + LISTITEM, ulEle) : this.liList;
12609
+ if (!liEle) {
12610
+ return;
12611
+ }
12430
12612
  var length = liEle.length;
12431
12613
  for (var i = 0; i < length; i++) {
12432
12614
  this.calculateWrap(liEle[parseInt(i.toString(), 10)]);
@@ -12515,6 +12697,10 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
12515
12697
  }
12516
12698
  this.isLoaded = true;
12517
12699
  this.isNodeDropped = false;
12700
+ // Initialise (or re-initialise) virtual mode after data binding has settled
12701
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
12702
+ this.initVirtualization();
12703
+ }
12518
12704
  };
12519
12705
  TreeView.prototype.doSelectionAction = function () {
12520
12706
  var sNodes = selectAll('.' + SELECTED$1, this.element);
@@ -12649,8 +12835,18 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
12649
12835
  };
12650
12836
  this.trigger('nodeClicked', eventArgs);
12651
12837
  };
12652
- TreeView.prototype.expandNode = function (currLi, icon, loaded) {
12838
+ TreeView.prototype.expandNode = function (currLi, icon, loaded, isKeyboardNav) {
12653
12839
  var _this = this;
12840
+ // Virtual mode fast-path: rebuild the flat list and re-render the window.
12841
+ // Only engage when height is also set; without a fixed height the tree falls
12842
+ // back to normal rendering and the standard expand path must be used.
12843
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
12844
+ var id = currLi ? currLi.getAttribute('data-uid') : null;
12845
+ if (id) {
12846
+ this.handleVirtualExpand(id, isKeyboardNav);
12847
+ }
12848
+ return;
12849
+ }
12654
12850
  this.renderReactTemplates();
12655
12851
  if (icon.classList.contains(LOAD)) {
12656
12852
  this.hideSpinner(icon);
@@ -12758,16 +12954,30 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
12758
12954
  removeClass([icon], PROCESS);
12759
12955
  }
12760
12956
  else {
12761
- _this.nodeCollapseAction(currLi, icon, observedArgs);
12957
+ // Detect if event is keyboard navigation by checking for 'action' property
12958
+ var isKeyboardNav = e && e.action !== undefined;
12959
+ _this.nodeCollapseAction(currLi, icon, observedArgs, isKeyboardNav);
12762
12960
  }
12763
12961
  });
12764
12962
  }
12765
12963
  else {
12766
- this.nodeCollapseAction(currLi, icon, colArgs);
12964
+ // Detect if event is keyboard navigation by checking for 'action' property
12965
+ var isKeyboardNav = e && e.action !== undefined;
12966
+ this.nodeCollapseAction(currLi, icon, colArgs, isKeyboardNav);
12767
12967
  }
12768
12968
  };
12769
- TreeView.prototype.nodeCollapseAction = function (currLi, icon, colArgs) {
12969
+ TreeView.prototype.nodeCollapseAction = function (currLi, icon, colArgs, isKeyboardNav) {
12770
12970
  var _this = this;
12971
+ // Virtual mode fast-path: rebuild the flat list and re-render the window.
12972
+ // Only engage when height is also set; without a fixed height the tree falls
12973
+ // back to normal rendering and the standard collapse path must be used.
12974
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
12975
+ var id = currLi ? currLi.getAttribute('data-uid') : null;
12976
+ if (id) {
12977
+ this.handleVirtualCollapse(id, isKeyboardNav);
12978
+ }
12979
+ return;
12980
+ }
12771
12981
  removeClass([icon], COLLAPSIBLE);
12772
12982
  addClass([icon], EXPANDABLE);
12773
12983
  var start = 0;
@@ -13231,6 +13441,10 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13231
13441
  li.setAttribute('tabindex', '0');
13232
13442
  EventHandler.add(li, 'blur', this.focusOut, this);
13233
13443
  this.updateIdAttr(focusedNode, li);
13444
+ // Track focused id for virtual mode re-render restoration
13445
+ if (this.enableVirtualization) {
13446
+ this.virtualFocusedId = li.getAttribute('data-uid');
13447
+ }
13234
13448
  }
13235
13449
  };
13236
13450
  TreeView.prototype.addSelect = function (liEle) {
@@ -13240,6 +13454,13 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13240
13454
  if (!isNullOrUndefined(id) && this.selectedNodes.indexOf(id) === -1) {
13241
13455
  this.selectedNodes.push(id);
13242
13456
  }
13457
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13458
+ this.virtualSelectedNodesSet.add(id);
13459
+ var actualIndex = this.virtualNodeMap.get(id);
13460
+ if (actualIndex && actualIndex > -1) {
13461
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].selected = true;
13462
+ }
13463
+ }
13243
13464
  };
13244
13465
  TreeView.prototype.removeSelect = function (liEle) {
13245
13466
  if (this.allowMultiSelection) {
@@ -13253,6 +13474,14 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13253
13474
  if (index > -1) {
13254
13475
  this.selectedNodes.splice(index, 1);
13255
13476
  }
13477
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13478
+ var id = liEle.getAttribute('data-uid');
13479
+ this.virtualSelectedNodesSet.delete(id);
13480
+ var actualIndex = this.virtualNodeMap.get(id);
13481
+ if (actualIndex && actualIndex > -1) {
13482
+ this.virtualFlatNodes[parseInt(actualIndex.toString(), 10)].selected = false;
13483
+ }
13484
+ }
13256
13485
  };
13257
13486
  TreeView.prototype.removeSelectAll = function () {
13258
13487
  var selectedLI = this.element.querySelectorAll('.' + ACTIVE);
@@ -13267,6 +13496,15 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13267
13496
  }
13268
13497
  removeClass(selectedLI, ACTIVE);
13269
13498
  this.setProperties({ selectedNodes: [] }, true);
13499
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13500
+ this.virtualSelectedNodesSet.clear();
13501
+ for (var i = 0; i < this.virtualFlatNodes.length; i++) {
13502
+ var node = this.virtualFlatNodes[parseInt(i.toString(), 10)];
13503
+ if (node.selected) {
13504
+ node.selected = false;
13505
+ }
13506
+ }
13507
+ }
13270
13508
  };
13271
13509
  TreeView.prototype.getSelectEvent = function (currLi, action, e) {
13272
13510
  var nodeData = this.getNodeData(currLi);
@@ -13289,6 +13527,9 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13289
13527
  }
13290
13528
  };
13291
13529
  TreeView.prototype.expandCollapseAction = function (currLi, e) {
13530
+ if (this.enableVirtualization && !isNullOrUndefined(this.height) && currLi.classList.contains(EDITING)) {
13531
+ return;
13532
+ }
13292
13533
  var icon = select('div.' + ICON, currLi);
13293
13534
  if (!icon || icon.classList.contains(PROCESS)) {
13294
13535
  return;
@@ -13319,15 +13560,29 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13319
13560
  removeClass([icon], PROCESS);
13320
13561
  }
13321
13562
  else {
13322
- _this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll);
13563
+ // Detect if event is keyboard navigation by checking for 'action' property
13564
+ var isKeyboardNav = e && e.action !== undefined;
13565
+ _this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav);
13323
13566
  }
13324
13567
  });
13325
13568
  }
13326
13569
  else {
13327
- this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll);
13570
+ // Detect if event is keyboard navigation by checking for 'action' property
13571
+ var isKeyboardNav = e && e.action !== undefined;
13572
+ this.nodeExpandAction(currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav);
13573
+ }
13574
+ };
13575
+ TreeView.prototype.nodeExpandAction = function (currLi, icon, expandChild, callback, isFromExpandAll, isKeyboardNav) {
13576
+ // In virtual mode (with a fixed height) the flat <li> never has a child <ul>
13577
+ // in the DOM, so the PARENTITEM check below would always fail and fall into
13578
+ // renderChildNodes(), which corrupts the virtual DOM.
13579
+ // Delegate directly to expandNode() which has the handleVirtualExpand fast-path.
13580
+ // When height is not provided, fall through to normal expand logic below since
13581
+ // the tree is rendered with a full nested DOM structure.
13582
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13583
+ this.expandNode(currLi, icon, undefined, isKeyboardNav);
13584
+ return;
13328
13585
  }
13329
- };
13330
- TreeView.prototype.nodeExpandAction = function (currLi, icon, expandChild, callback, isFromExpandAll) {
13331
13586
  var ul = select('.' + PARENTITEM, currLi);
13332
13587
  if (ul && ul.nodeName === 'UL') {
13333
13588
  this.expandNode(currLi, icon);
@@ -13441,6 +13696,19 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13441
13696
  });
13442
13697
  };
13443
13698
  TreeView.prototype.navigateToFocus = function (isUp) {
13699
+ // In virtual mode (with a fixed height) the element itself is the scroll
13700
+ // container; use ensureVirtualNodeVisible. When height is not provided,
13701
+ // the tree uses normal rendering, so fall through to the standard scroll logic.
13702
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
13703
+ var focusedLi = this.getFocusedNode();
13704
+ if (focusedLi) {
13705
+ var id = focusedLi.getAttribute('data-uid');
13706
+ if (id) {
13707
+ this.ensureVirtualNodeVisible(id);
13708
+ }
13709
+ }
13710
+ return;
13711
+ }
13444
13712
  var focusNode = this.getFocusedNode().querySelector('.' + TEXTWRAP);
13445
13713
  var pos = focusNode.getBoundingClientRect();
13446
13714
  var parent = this.getScrollParent(this.element);
@@ -13861,6 +14129,46 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13861
14129
  return isNullOrUndefined(fNode) ? (isNullOrUndefined(selectedItem) ? this.element.firstElementChild : selectedItem) : fNode;
13862
14130
  };
13863
14131
  TreeView.prototype.focusNextNode = function (li, isTowards) {
14132
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
14133
+ // Determine the id of the node one step in the requested direction
14134
+ var currentId = li ? li.getAttribute('data-uid') : null;
14135
+ var currentIdx = -1;
14136
+ if (!isNullOrUndefined(currentId)) {
14137
+ var idx = this.virtualNodeMap.get(currentId);
14138
+ currentIdx = !isNullOrUndefined(idx) ? idx : -1;
14139
+ }
14140
+ var targetIdx = isTowards ? currentIdx + 1 : currentIdx - 1;
14141
+ if (targetIdx < 0 || targetIdx >= this.virtualFlatNodes.length) {
14142
+ return;
14143
+ }
14144
+ var targetId = this.virtualFlatNodes[parseInt(targetIdx.toString(), 10)].id;
14145
+ // ── Fix 1: set virtualFocusedId BEFORE calling ensureVirtualNodeVisible ──
14146
+ // renderVirtualItems() is called synchronously inside ensureVirtualNodeVisible
14147
+ // when a re-render is needed. If virtualFocusedId is still the old value at
14148
+ // that point, focus restoration falls back to the first item. Setting it here
14149
+ // ensures renderVirtualItems always restores focus to the correct target node.
14150
+ this.virtualFocusedId = targetId;
14151
+ // ── Fix 2: clear focus classes from ALL nested list items, not just top-level ones ──
14152
+ // With hierarchical virtual DOM, the currently focused <li> may be nested inside
14153
+ // a child <ul>. Using ':scope > li' only cleans up direct children and leaves
14154
+ // stale e-focused / e-node-focus classes on nested nodes across re-renders.
14155
+ var allLi = this.element.querySelectorAll('.' + LISTITEM + '.' + FOCUS);
14156
+ for (var k = 0; k < allLi.length; k++) {
14157
+ allLi[parseInt(k.toString(), 10)].classList.remove(FOCUS);
14158
+ allLi[parseInt(k.toString(), 10)].classList.remove(FOCUSED$1);
14159
+ allLi[parseInt(k.toString(), 10)].setAttribute('tabindex', '-1');
14160
+ }
14161
+ // Ensure the target <li> is in the DOM window (may trigger a re-render).
14162
+ // virtualFocusedId is already set above so renderVirtualItems restores focus correctly.
14163
+ this.ensureVirtualNodeVisible(targetId);
14164
+ // Re-query both nodes after the potential re-render so setFocus gets live DOM elements
14165
+ var prevNode = currentId ? this.getElement(currentId) : li;
14166
+ var nextNode_1 = this.getElement(targetId);
14167
+ if (!isNullOrUndefined(nextNode_1)) {
14168
+ this.setFocus(isNullOrUndefined(prevNode) ? nextNode_1 : prevNode, nextNode_1);
14169
+ }
14170
+ return;
14171
+ }
13864
14172
  var nextNode = isTowards ? this.getNextNode(li) : this.getPrevNode(li);
13865
14173
  this.setFocus(li, nextNode);
13866
14174
  this.navigateToFocus(!isTowards);
@@ -13930,6 +14238,12 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13930
14238
  addClass([nextNode], FOCUS);
13931
14239
  addClass([nextNode], FOCUSED$1);
13932
14240
  nextNode.setAttribute('tabindex', '0');
14241
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
14242
+ nextNode.focus({ preventScroll: true });
14243
+ }
14244
+ else {
14245
+ nextNode.focus();
14246
+ }
13933
14247
  nextNode.focus();
13934
14248
  EventHandler.add(nextNode, 'blur', this.focusOut, this);
13935
14249
  this.updateIdAttr(preNode, nextNode);
@@ -13948,6 +14262,30 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13948
14262
  if (!focusedElement) {
13949
14263
  return;
13950
14264
  }
14265
+ // Virtual-mode guard: when the browser moves focus into the TreeView (e.g. via
14266
+ // the Tab key) it always lands on the single `<li tabindex="0">`. In that
14267
+ // case the element is already the focusable one, FOCUS is already applied
14268
+ // and the only thing left to do is the keyboard focus styling. Skip the
14269
+ // redundant setAttribute/EventHandler.add work so we never re-render the
14270
+ // virtual window as a side effect of focusing the existing `tabindex="0"`
14271
+ // item.
14272
+ // ── Fix: Use document.activeElement to detect browser-focused node ──
14273
+ // After renderVirtualItems() restores focus, the browser-focused element is
14274
+ // the authoritative source, not getFocusedNode(). If they don't match, it means
14275
+ // renderVirtualItems already correctly set up the element, so we must not
14276
+ // override its setup by calling setFocus again.
14277
+ var actuallyFocusedElement = document.activeElement;
14278
+ var isAlreadyFocusedInVirtualMode = this.enableVirtualization
14279
+ && actuallyFocusedElement === focusedElement
14280
+ && actuallyFocusedElement.getAttribute('tabindex') === '0'
14281
+ && actuallyFocusedElement.classList.contains(FOCUS);
14282
+ if (isAlreadyFocusedInVirtualMode) {
14283
+ if (this.isKeyboardFocus && !actuallyFocusedElement.classList.contains(FOCUSED$1)) {
14284
+ addClass([actuallyFocusedElement], FOCUSED$1);
14285
+ }
14286
+ this.isKeyboardFocus = false;
14287
+ return;
14288
+ }
13951
14289
  if (!this.mouseDownStatus) {
13952
14290
  if (focusedElement.classList.contains('e-disable')) {
13953
14291
  focusedElement.setAttribute('tabindex', '-1');
@@ -13974,6 +14312,9 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
13974
14312
  removeClass([focusedElement], FOCUS);
13975
14313
  removeClass([focusedElement], FOCUSED$1);
13976
14314
  EventHandler.remove(focusedElement, 'blur', this.focusOut);
14315
+ if (this.enableVirtualization) {
14316
+ this.virtualFocusedId = null;
14317
+ }
13977
14318
  }
13978
14319
  };
13979
14320
  TreeView.prototype.onMouseOver = function (e) {
@@ -14187,6 +14528,19 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
14187
14528
  else {
14188
14529
  txtEle[ListBase.getDomSetter(this.listBaseOption)] = ListBase.getSanitizedText(newText, this.listBaseOption);
14189
14530
  }
14531
+ // ── Virtual mode: sync the flat-node snapshot so re-renders show updated text ──
14532
+ // virtualFlatNodes is rebuilt from this.treeData (via flattenVisibleNodes) only on
14533
+ // expand/collapse/scroll. Between those events the snapshot is the single source of
14534
+ // truth used by createVirtualListItem. setValue() above updates this.editData (which
14535
+ // IS the treeData object reference), but virtualFlatNodes[idx].text still holds the
14536
+ // old value. Update it here so the next renderVirtualItems() call shows the new text.
14537
+ if (this.enableVirtualization) {
14538
+ var id = liEle.getAttribute('data-uid');
14539
+ var idx = this.virtualNodeMap ? this.virtualNodeMap.get(id) : undefined;
14540
+ if (!isNullOrUndefined(idx) && idx > -1) {
14541
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].text = newText;
14542
+ }
14543
+ }
14190
14544
  if (isInput) {
14191
14545
  removeClass([liEle], EDITING);
14192
14546
  liEle.focus();
@@ -15564,6 +15918,31 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
15564
15918
  TreeView.prototype.keyDownHandler = function (e) {
15565
15919
  if (e.key === 'Tab' || e.keyCode === 9) {
15566
15920
  this.isKeyboardFocus = true;
15921
+ // Mark the upcoming scroll event (if any) as Tab-induced so the
15922
+ // virtual scroll handler skips its re-render path. The browser
15923
+ // calls `Element.scrollIntoView` semantics on focus, which scrolls
15924
+ // a `<li tabindex="0">` in the middle of the tree into view and
15925
+ // therefore fires `scroll` on the container. Without this guard
15926
+ // the scroll handler would tear down the focused `<li>` and move
15927
+ // focus elsewhere.
15928
+ if (this.enableVirtualization) {
15929
+ this.virtualSkipNextScrollRender = true;
15930
+ }
15931
+ }
15932
+ // ── Fix: Also skip scroll render for arrow key navigation ──────────────────
15933
+ // Arrow Down/Up calls focusNextNode() which calls ensureVirtualNodeVisible()
15934
+ // to scroll the target node into view. Without this guard, the scroll event
15935
+ // would trigger onVirtualScroll() which re-renders the window and resets the
15936
+ // focus that focusNextNode() just set. This manifests as tabindex resetting
15937
+ // to the first item instead of staying on the arrow-navigated target.
15938
+ // Arrow Right/Left (expand/collapse) also calls renderVirtualItems() which
15939
+ // calls li.focus() in branch 2; that can trigger a scroll on an edge node,
15940
+ // so we consume it here to prevent a stale RAF re-render.
15941
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.keyCode === 40 || e.keyCode === 38
15942
+ || e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.keyCode === 39 || e.keyCode === 37) {
15943
+ if (this.enableVirtualization) {
15944
+ this.virtualSkipNextScrollRender = true;
15945
+ }
15567
15946
  }
15568
15947
  };
15569
15948
  TreeView.prototype.preventContextMenu = function (e) {
@@ -15920,6 +16299,18 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
15920
16299
  if (this.dataType === 1) {
15921
16300
  this.groupedData = this.getGroupedData(this.treeData, this.fields.parentID);
15922
16301
  }
16302
+ // In virtual mode (with a fixed height), rebuild the flat node list and re-render
16303
+ // after deletion so the removed nodes are correctly reflected in the hierarchical
16304
+ // virtual DOM. When height is not provided, the tree uses normal rendering and
16305
+ // the DOM is already updated by removeNode() above.
16306
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
16307
+ this.virtualFlatNodes = this.flattenVisibleNodes();
16308
+ this.rebuildVirtualNodeMap();
16309
+ var win = this.getVirtualWindow();
16310
+ this.virtualStartIndex = win.startIndex;
16311
+ this.virtualEndIndex = win.endIndex;
16312
+ this.renderVirtualItems();
16313
+ }
15923
16314
  this.triggerEvent('removeNode', nodeData);
15924
16315
  };
15925
16316
  TreeView.prototype.editSucess = function (target, newText, prevent) {
@@ -15980,6 +16371,1086 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
15980
16371
  }
15981
16372
  return parentsID;
15982
16373
  };
16374
+ // ═══════════════════════════════════════════════════════════════════════════════
16375
+ // VIRTUALIZATION — private helpers
16376
+ // ═══════════════════════════════════════════════════════════════════════════════
16377
+ /**
16378
+ * Parses the `height` property (string like '400px' or number) to a pixel value.
16379
+ * Returns 0 if the value cannot be determined.
16380
+ *
16381
+ * @returns {number} Pixel height.
16382
+ * @private
16383
+ */
16384
+ TreeView.prototype.parseHeightToPixels = function () {
16385
+ var h = this.height;
16386
+ if (isNullOrUndefined(h)) {
16387
+ return 0;
16388
+ }
16389
+ if (typeof h === 'number') {
16390
+ return h;
16391
+ }
16392
+ var parsed = parseFloat(h);
16393
+ return isNaN(parsed) ? 0 : parsed;
16394
+ };
16395
+ /**
16396
+ * Measures the height of the first rendered list item so subsequent calculations
16397
+ * use real layout values. Falls back to the stored `virtualItemHeight` default (40).
16398
+ *
16399
+ * @returns {number} Row height in pixels.
16400
+ * @private
16401
+ */
16402
+ TreeView.prototype.measureVirtualItemHeight = function () {
16403
+ var firstLi = select('.' + LISTITEM, this.element);
16404
+ if (firstLi && firstLi.offsetHeight > 0) {
16405
+ return firstLi.offsetHeight;
16406
+ }
16407
+ return this.virtualItemHeight;
16408
+ };
16409
+ /**
16410
+ * Rebuilds the cached Sets for checked/selected nodes to enable O(1) lookup
16411
+ * during the flatten operation instead of O(n) array indexOf calls.
16412
+ *
16413
+ * @returns {void}
16414
+ * @private
16415
+ */
16416
+ TreeView.prototype.rebuildVirtualCheckCache = function () {
16417
+ this.virtualExpandedNodesSet.clear();
16418
+ this.virtualCheckedNodesSet.clear();
16419
+ this.virtualSelectedNodesSet.clear();
16420
+ if (this.expandedNodes && Array.isArray(this.expandedNodes)) {
16421
+ for (var i = 0; i < this.expandedNodes.length; i++) {
16422
+ this.virtualExpandedNodesSet.add(this.expandedNodes[i]);
16423
+ }
16424
+ }
16425
+ if (this.checkedNodes && Array.isArray(this.checkedNodes)) {
16426
+ for (var i = 0; i < this.checkedNodes.length; i++) {
16427
+ this.virtualCheckedNodesSet.add(this.checkedNodes[i]);
16428
+ }
16429
+ }
16430
+ if (this.selectedNodes && Array.isArray(this.selectedNodes)) {
16431
+ for (var i = 0; i < this.selectedNodes.length; i++) {
16432
+ this.virtualSelectedNodesSet.add(this.selectedNodes[i]);
16433
+ }
16434
+ }
16435
+ };
16436
+ /**
16437
+ * Recursively walks the treeData (or groupedData for flat lists) and builds
16438
+ * `virtualFlatNodes` — an ordered list of every node that is currently visible
16439
+ * (i.e. its ancestors are all expanded). Uses cached Sets for O(1) state lookups.
16440
+ *
16441
+ * @returns {VirtualNodeData[]} Ordered flat list of visible nodes.
16442
+ * @private
16443
+ */
16444
+ TreeView.prototype.flattenVisibleNodes = function () {
16445
+ // Build cached Sets for fast lookups
16446
+ this.rebuildVirtualCheckCache();
16447
+ var result = [];
16448
+ if (this.dataType === 1) {
16449
+ // Flat data with parentID field — use getChildGroup to find root nodes
16450
+ var rootNodes = [];
16451
+ if (this.groupedData) {
16452
+ rootNodes = this.getChildGroup(this.groupedData, null, true);
16453
+ }
16454
+ if (!rootNodes || rootNodes.length === 0) {
16455
+ rootNodes = this.rootData;
16456
+ }
16457
+ // Apply sortOrder to root-level nodes for flat data (same as hierarchical path below)
16458
+ rootNodes = this.getSortedData(rootNodes);
16459
+ this.flattenNodeRecursive(rootNodes, 1, null, result);
16460
+ }
16461
+ else {
16462
+ // Hierarchical data
16463
+ var sorted = this.getSortedData(this.rootData);
16464
+ this.flattenNodeRecursive(sorted, 1, null, result);
16465
+ }
16466
+ return result;
16467
+ };
16468
+ /**
16469
+ * Recursive helper for `flattenVisibleNodes`.
16470
+ *
16471
+ * @param {Object[]} nodes - Array of raw data objects at this level.
16472
+ * @param {number} level - 1-based depth level.
16473
+ * @param {string | null} parentId - The parent node id, or null for root.
16474
+ * @param {VirtualNodeData[]} result - Accumulator array.
16475
+ * @returns {void}
16476
+ * @private
16477
+ */
16478
+ TreeView.prototype.flattenNodeRecursive = function (nodes, level, parentId, result) {
16479
+ if (!nodes || nodes.length === 0) {
16480
+ return;
16481
+ }
16482
+ // Cache field names and settings outside the loop to avoid repeated conversions
16483
+ var childFieldName = this.fields.child.toString();
16484
+ var expandedFieldName = this.fields.expanded;
16485
+ var isCheckedFieldName = this.fields.isChecked;
16486
+ var iconCssFieldName = this.fields.iconCss;
16487
+ var imageUrlFieldName = this.fields.imageUrl;
16488
+ var idFieldName = this.fields.id;
16489
+ var textFieldName = this.fields.text;
16490
+ var selectedFieldName = this.fields.selected;
16491
+ var needsSorting = this.sortOrder !== 'None';
16492
+ var isDataTypeFlat = this.dataType === 1;
16493
+ var enableVirtualization = this.enableVirtualization;
16494
+ for (var i = 0; i < nodes.length; i++) {
16495
+ var raw = nodes[i];
16496
+ var id = isNullOrUndefined(raw[idFieldName]) ? '' : raw[idFieldName].toString();
16497
+ var text = isNullOrUndefined(raw[textFieldName]) ? '' : raw[textFieldName].toString();
16498
+ // Use expandedNodes as the single source of truth.
16499
+ var inExpandedNodes = this.virtualExpandedNodesSet.has(id);
16500
+ var rawExpanded = !inExpandedNodes && getValue(expandedFieldName, raw) === true;
16501
+ var expanded = inExpandedNodes || rawExpanded;
16502
+ if (rawExpanded && id) {
16503
+ this.expandedNodes.push(id);
16504
+ this.virtualExpandedNodesSet.add(id);
16505
+ }
16506
+ // Inline hasChildRecords check to avoid function call (optimization)
16507
+ var hasChildren = false;
16508
+ if (isDataTypeFlat) {
16509
+ if (this.groupedData) {
16510
+ var groupedChild = this.getChildGroup(this.groupedData, id, false);
16511
+ hasChildren = !isNullOrUndefined(groupedChild) && groupedChild.length > 0;
16512
+ }
16513
+ }
16514
+ else {
16515
+ var childrenObj = getValue(childFieldName, raw);
16516
+ hasChildren = !isNullOrUndefined(childrenObj) && Array.isArray(childrenObj) && childrenObj.length > 0;
16517
+ }
16518
+ // Virtual-mode only: honour fields.isChecked === true on initial render
16519
+ if (enableVirtualization && id && isCheckedFieldName && !this.virtualCheckedNodesSet.has(id)) {
16520
+ var rawChecked = getValue(isCheckedFieldName, raw);
16521
+ if (!isNullOrUndefined(rawChecked) && rawChecked.toString() === 'true') {
16522
+ this.virtualCheckedNodesSet.add(id);
16523
+ if (this.checkedNodes && this.checkedNodes.indexOf(id) === -1) {
16524
+ this.checkedNodes.push(id);
16525
+ }
16526
+ }
16527
+ }
16528
+ if (this.initialRender && enableVirtualization && id && selectedFieldName && !this.virtualSelectedNodesSet.has(id)) {
16529
+ var rawSelected = getValue(selectedFieldName, raw);
16530
+ if (!isNullOrUndefined(rawSelected) && rawSelected.toString() === 'true') {
16531
+ this.virtualSelectedNodesSet.add(id);
16532
+ if (this.selectedNodes && this.selectedNodes.indexOf(id) === -1) {
16533
+ this.selectedNodes.push(id);
16534
+ }
16535
+ }
16536
+ }
16537
+ // Use cached Sets for O(1) lookup
16538
+ var selected = this.virtualSelectedNodesSet.has(id);
16539
+ // Resolve checkbox state: indeterminate takes precedence over the checked set,
16540
+ // because a node can be in the indeterminate state without being in checkedNodes.
16541
+ var isChecked = (enableVirtualization && this.virtualIndeterminateNodesSet.has(id))
16542
+ ? 'indeterminate'
16543
+ : this.virtualCheckedNodesSet.has(id);
16544
+ var iconCssVal = raw[iconCssFieldName];
16545
+ var iconCss = isNullOrUndefined(iconCssVal) ? '' : iconCssVal.toString();
16546
+ var imageUrlVal = raw[imageUrlFieldName];
16547
+ var imageUrl = isNullOrUndefined(imageUrlVal) ? '' : imageUrlVal.toString();
16548
+ var node = {
16549
+ id: id, text: text, level: level, parentId: parentId, hasChildren: hasChildren, expanded: expanded,
16550
+ selected: selected, isChecked: isChecked, iconCss: iconCss, imageUrl: imageUrl
16551
+ };
16552
+ result.push(node);
16553
+ if (expanded && hasChildren) {
16554
+ // Determine children list
16555
+ var children = [];
16556
+ if (isDataTypeFlat) {
16557
+ if (this.groupedData) {
16558
+ children = this.getChildGroup(this.groupedData, id, false);
16559
+ // Apply sortOrder to flat-data children (same as hierarchical path)
16560
+ if (needsSorting) {
16561
+ children = ListBase.getDataSource(children, ListBase.addSorting(this.sortOrder, this.fields.text));
16562
+ }
16563
+ }
16564
+ }
16565
+ else {
16566
+ var rawChildren = getValue(childFieldName, raw);
16567
+ if (!isNullOrUndefined(rawChildren) && Array.isArray(rawChildren)) {
16568
+ children = rawChildren;
16569
+ // Only sort if needed
16570
+ if (needsSorting) {
16571
+ children = ListBase.getDataSource(children, ListBase.addSorting(this.sortOrder, this.fields.text));
16572
+ }
16573
+ }
16574
+ }
16575
+ this.flattenNodeRecursive(children, level + 1, id, result);
16576
+ }
16577
+ }
16578
+ };
16579
+ /**
16580
+ * Checks whether a raw datasource node has child records.
16581
+ *
16582
+ * @param {Object} raw - Raw datasource object.
16583
+ * @returns {boolean} True if children exist.
16584
+ * @private
16585
+ */
16586
+ TreeView.prototype.hasChildRecords = function (raw) {
16587
+ if (this.dataType === 1) {
16588
+ if (!this.groupedData) {
16589
+ return false;
16590
+ }
16591
+ var rawId = raw[this.fields.id];
16592
+ if (isNullOrUndefined(rawId)) {
16593
+ return false;
16594
+ }
16595
+ var groupedChild = this.getChildGroup(this.groupedData, rawId.toString(), false);
16596
+ return !isNullOrUndefined(groupedChild) && groupedChild.length > 0;
16597
+ }
16598
+ var childField = this.fields.child.toString();
16599
+ var children = getValue(childField, raw);
16600
+ return !isNullOrUndefined(children) && Array.isArray(children) && children.length > 0;
16601
+ };
16602
+ /**
16603
+ * Rebuilds `virtualNodeMap` (id → index) from the current `virtualFlatNodes`.
16604
+ *
16605
+ * @returns {void}
16606
+ * @private
16607
+ */
16608
+ TreeView.prototype.rebuildVirtualNodeMap = function () {
16609
+ this.virtualNodeMap.clear();
16610
+ for (var i = 0; i < this.virtualFlatNodes.length; i++) {
16611
+ this.virtualNodeMap.set(this.virtualFlatNodes[parseInt(i.toString(), 10)].id, i);
16612
+ }
16613
+ };
16614
+ TreeView.prototype.getVirtualWindow = function (scrollTop) {
16615
+ var total = this.virtualFlatNodes.length;
16616
+ if (total === 0) {
16617
+ return { startIndex: 0, endIndex: 0 };
16618
+ }
16619
+ var st = scrollTop !== undefined ? scrollTop : this.element.scrollTop;
16620
+ var itemH = this.virtualItemHeight;
16621
+ var vpH = this.virtualViewportHeight;
16622
+ var rawStart = Math.floor(st / itemH) - this.virtualOverscanCount;
16623
+ var rawEnd = Math.ceil((st + vpH) / itemH) + this.virtualOverscanCount;
16624
+ var startIndex = Math.max(0, rawStart);
16625
+ var endIndex = Math.min(total, rawEnd);
16626
+ return { startIndex: startIndex, endIndex: endIndex };
16627
+ };
16628
+ TreeView.prototype.getVirtualWindowAt = function (scrollTop) {
16629
+ return this.getVirtualWindow(scrollTop);
16630
+ };
16631
+ TreeView.prototype.getVirtualPadding = function () {
16632
+ var total = this.virtualFlatNodes.length;
16633
+ var itemH = this.virtualItemHeight;
16634
+ var start = this.virtualStartIndex;
16635
+ var end = this.virtualEndIndex;
16636
+ var paddingTop = start * itemH;
16637
+ var paddingBottom = Math.max(0, (total - end) * itemH);
16638
+ return { paddingTop: paddingTop, paddingBottom: paddingBottom };
16639
+ };
16640
+ /**
16641
+ * Renders the virtual window into a **nested** `<ul>/<li>` hierarchy identical to
16642
+ * the non-virtualized DOM structure.
16643
+ *
16644
+ * ## Scroll math
16645
+ * The flat-index ↔ pixel mapping stays unchanged: every entry in
16646
+ * `virtualFlatNodes` represents exactly one `itemHeight` row.
16647
+ * `paddingTop = startIndex * itemHeight` and
16648
+ * `paddingBottom = (total - endIndex) * itemHeight` keep the scrollbar
16649
+ * thumb in the correct position.
16650
+ *
16651
+ * ## Ancestor injection
16652
+ * When `startIndex` points at a child node whose ancestor(s) have already
16653
+ * scrolled above the viewport (their flat indices are < startIndex), the
16654
+ * ancestors would normally be missing from the DOM, forcing the children to
16655
+ * fall back to the root `<ul>` as flat siblings — producing wrong HTML.
16656
+ *
16657
+ * To fix this, before rendering the window we walk the ancestor chain of the
16658
+ * first visible node and collect every ancestor that lies **outside** the
16659
+ * window. Those ancestors are injected as real `<li>` containers at the
16660
+ * very top of the root `<ul>`. To compensate for their presence, `paddingTop`
16661
+ * is reduced by `injectedCount * itemHeight` so the total scroll height
16662
+ * remains the same.
16663
+ *
16664
+ * Once the ancestors are in the DOM they are registered in `renderedParentMap`
16665
+ * so the normal window-rendering loop can nest children inside them exactly
16666
+ * as it does when both parent and child are in the same window.
16667
+ *
16668
+ * @returns {void}
16669
+ * @private
16670
+ */
16671
+ TreeView.prototype.renderVirtualItems = function () {
16672
+ if (!this.ulElement) {
16673
+ return;
16674
+ }
16675
+ var ul = this.ulElement;
16676
+ // ── Snapshot focused ID BEFORE clearing the DOM ───────────────────────────
16677
+ // Removing a focused <li> from the DOM triggers a synchronous `blur` event,
16678
+ // which calls focusOut() and sets virtualFocusedId = null. Capturing the id
16679
+ // here (before any removeChild) ensures focus is correctly restored to the
16680
+ // same node after re-render even when keyboard navigation was in progress.
16681
+ var focusedUid = this.virtualFocusedId || null;
16682
+ // ── Clear previous render (direct children only) ─────────────────────────
16683
+ var existingItems = ul.querySelectorAll(':scope > li');
16684
+ for (var i = existingItems.length - 1; i >= 0; i--) {
16685
+ ul.removeChild(existingItems[parseInt(i.toString(), 10)]);
16686
+ }
16687
+ var renderedParentMap = new Map();
16688
+ var itemH = this.virtualItemHeight;
16689
+ var injectedCount = 0;
16690
+ if (this.virtualStartIndex < this.virtualFlatNodes.length) {
16691
+ var firstNode = this.virtualFlatNodes[this.virtualStartIndex];
16692
+ if (firstNode.parentId) {
16693
+ // Build the ancestor chain from root down to the immediate parent
16694
+ var ancestors = [];
16695
+ var searchId = firstNode.parentId;
16696
+ while (searchId) {
16697
+ var ancestorIdx = this.virtualNodeMap.get(searchId);
16698
+ if (!isNullOrUndefined(ancestorIdx) && ancestorIdx < this.virtualStartIndex) {
16699
+ var ancestor = this.virtualFlatNodes[parseInt(ancestorIdx.toString(), 10)];
16700
+ ancestors.unshift(ancestor); // prepend so root comes first
16701
+ searchId = ancestor.parentId;
16702
+ }
16703
+ else {
16704
+ break;
16705
+ }
16706
+ }
16707
+ // Inject each ancestor as a real <li> into the root <ul>
16708
+ // and register it in renderedParentMap for child-nesting below.
16709
+ for (var a = 0; a < ancestors.length; a++) {
16710
+ var ancestor = ancestors[parseInt(a.toString(), 10)];
16711
+ var ancestorLi = this.createVirtualListItem(ancestor, false);
16712
+ // Find the correct container: root or a previously injected ancestor's <ul>
16713
+ var targetUl = ul;
16714
+ if (ancestor.parentId && renderedParentMap.has(ancestor.parentId)) {
16715
+ var pe = renderedParentMap.get(ancestor.parentId);
16716
+ if (!pe.childUl) {
16717
+ pe.childUl = this.createElement('ul', {
16718
+ className: PARENTITEM + ' e-ul',
16719
+ attrs: { role: 'group' }
16720
+ });
16721
+ pe.li.appendChild(pe.childUl);
16722
+ }
16723
+ targetUl = pe.childUl;
16724
+ }
16725
+ targetUl.appendChild(ancestorLi);
16726
+ renderedParentMap.set(ancestor.id, { li: ancestorLi, childUl: null });
16727
+ injectedCount++;
16728
+ }
16729
+ }
16730
+ }
16731
+ // ── Scroll-space padding (adjusted for injected ancestors) ────────────────
16732
+ var total = this.virtualFlatNodes.length;
16733
+ var start = this.virtualStartIndex;
16734
+ var end = this.virtualEndIndex;
16735
+ var rawPaddingTop = start * itemH;
16736
+ // Subtract the height of injected ancestors so the scroll position is
16737
+ // exact: those ancestor rows are now real DOM elements, not hidden space.
16738
+ var adjustedPaddingTop = Math.max(0, rawPaddingTop - injectedCount * itemH);
16739
+ ul.style.paddingTop = adjustedPaddingTop + 'px';
16740
+ ul.style.paddingBottom = Math.max(0, (total - end) * itemH) + 'px';
16741
+ // ── Render window items (nested via renderedParentMap) ────────────────────
16742
+ var liListBuf = [];
16743
+ for (var i = start; i < end; i++) {
16744
+ var node = this.virtualFlatNodes[parseInt(i.toString(), 10)];
16745
+ var li = this.createVirtualListItem(node, i === start);
16746
+ // Find the correct parent container
16747
+ var targetUl = ul;
16748
+ if (node.parentId && renderedParentMap.has(node.parentId)) {
16749
+ var parentEntry = renderedParentMap.get(node.parentId);
16750
+ // Lazily create the child <ul> the first time a child needs it
16751
+ if (!parentEntry.childUl) {
16752
+ parentEntry.childUl = this.createElement('ul', {
16753
+ className: PARENTITEM + ' e-ul',
16754
+ attrs: { role: 'group' }
16755
+ });
16756
+ parentEntry.li.appendChild(parentEntry.childUl);
16757
+ }
16758
+ targetUl = parentEntry.childUl;
16759
+ }
16760
+ targetUl.appendChild(li);
16761
+ liListBuf.push(li);
16762
+ // Register this node so its children in the same window can nest under it
16763
+ if (node.expanded && node.hasChildren) {
16764
+ renderedParentMap.set(node.id, { li: li, childUl: null });
16765
+ }
16766
+ }
16767
+ // ── Restore focus / tabindex ──────────────────────────────────────────────
16768
+ // In virtual mode only the li that carries the 'e-active' class should be
16769
+ // focusable (tabindex='0'). All other rendered li elements must be set
16770
+ // to tabindex='-1' so the tab order remains unambiguous as the window
16771
+ // changes during scrolling.
16772
+ var focusRestored = false;
16773
+ // 1. Prefer the currently active node if it is in the rendered window.
16774
+ for (var k = 0; k < liListBuf.length; k++) {
16775
+ var li = liListBuf[parseInt(k.toString(), 10)];
16776
+ if (li.classList.contains(ACTIVE)) {
16777
+ li.setAttribute('tabindex', '0');
16778
+ // ── Fix: ensure FOCUS class set on active node too ──
16779
+ // This ensures focusIn() guard condition is met: tabindex='0' && FOCUS class
16780
+ addClass([li], FOCUS);
16781
+ focusRestored = true;
16782
+ break;
16783
+ }
16784
+ }
16785
+ // 2. Otherwise fall back to the previously focused node (if it is in window).
16786
+ if (!focusRestored && focusedUid) {
16787
+ for (var k = 0; k < liListBuf.length; k++) {
16788
+ var li = liListBuf[parseInt(k.toString(), 10)];
16789
+ if (li.getAttribute('data-uid') === focusedUid) {
16790
+ li.setAttribute('tabindex', '0');
16791
+ addClass([li], FOCUS);
16792
+ addClass([li], FOCUSED$1);
16793
+ // ── Fix: restore actual browser focus so keyboard events keep working ──
16794
+ // After renderVirtualItems() replaces all <li> elements the old element
16795
+ // is gone from the DOM and document.activeElement falls back to
16796
+ // document.body. Setting tabindex + classes is not enough — the new
16797
+ // element also needs a programmatic .focus() call so subsequent keyboard
16798
+ // events (Arrow keys, Home, End, etc.) are delivered to the TreeView.
16799
+ // Re-attach the blur→focusOut handler so virtualFocusedId is cleared
16800
+ // correctly when the user tabs away.
16801
+ // ── Use preventScroll:true ────────────────────────────────────────────
16802
+ // Without preventScroll the browser scrolls the container to bring this
16803
+ // node into view during the re-render, changing scrollTop AFTER the
16804
+ // correct paddingTop has been applied. This is the root cause of the
16805
+ // KBN expand position-jump bug: the render is correct but the browser
16806
+ // silently scrolls the viewport, making the parent node appear to jump.
16807
+ // ensureVirtualNodeVisible() handles any legitimate scroll-into-view
16808
+ // needs before renderVirtualItems() is called, so scroll here is wrong.
16809
+ EventHandler.add(li, 'blur', this.focusOut, this);
16810
+ li.focus({ preventScroll: true });
16811
+ focusRestored = true;
16812
+ break;
16813
+ }
16814
+ }
16815
+ }
16816
+ // 3. Finally default to the first item in the window.
16817
+ if (!focusRestored && liListBuf.length > 0) {
16818
+ var firstLi = liListBuf[0];
16819
+ firstLi.setAttribute('tabindex', '0');
16820
+ }
16821
+ // ── Fix: Ensure all other rendered nodes have tabindex="-1" ──
16822
+ // This prevents stale tabindex="0" on multiple nodes which breaks Tab order
16823
+ for (var k = 0; k < liListBuf.length; k++) {
16824
+ var li = liListBuf[parseInt(k.toString(), 10)];
16825
+ if (li.getAttribute('tabindex') !== '0') {
16826
+ li.setAttribute('tabindex', '-1');
16827
+ }
16828
+ }
16829
+ // Keep liList in sync so keyboard-navigation helpers stay functional.
16830
+ this.liList = liListBuf;
16831
+ };
16832
+ /**
16833
+ * Creates a single `<li>` element for the virtual window representing `node`.
16834
+ *
16835
+ * @param {VirtualNodeData} node - The node data.
16836
+ * @param {boolean} _isFirst - True if this is the first rendered item (unused here but kept for potential override).
16837
+ * @returns {HTMLElement} The constructed `<li>` element.
16838
+ * @private
16839
+ */
16840
+ TreeView.prototype.createVirtualListItem = function (node, _isFirst) {
16841
+ var li = this.createElement('li', { className: LISTITEM });
16842
+ // Level class for CSS indentation
16843
+ li.classList.add('e-level-' + node.level);
16844
+ li.setAttribute('data-uid', node.id);
16845
+ li.setAttribute('aria-level', node.level.toString());
16846
+ li.setAttribute('role', 'treeitem');
16847
+ li.setAttribute('tabindex', '-1');
16848
+ // Full-row overlay (required for fullRowSelect mode)
16849
+ if (this.fullRowSelect) {
16850
+ var fullRow = this.createElement('div', { className: FULLROW });
16851
+ li.appendChild(fullRow);
16852
+ }
16853
+ // Text/icon wrapper
16854
+ var textWrap = this.createElement('div', { className: TEXTWRAP });
16855
+ // Expand/collapse icon
16856
+ if (node.hasChildren) {
16857
+ var iconSpan = this.createElement('div', {
16858
+ className: ICON + ' ' + (node.expanded ? COLLAPSIBLE : EXPANDABLE)
16859
+ });
16860
+ textWrap.appendChild(iconSpan);
16861
+ }
16862
+ // Checkbox
16863
+ if (this.showCheckBox) {
16864
+ var checkWrap = createCheckBox(this.createElement, false, {
16865
+ checked: node.isChecked === true,
16866
+ cssClass: CHECKBOXWRAP
16867
+ });
16868
+ if (node.isChecked === 'indeterminate') {
16869
+ var frame = checkWrap.querySelector('.' + CHECKBOXFRAME);
16870
+ if (frame) {
16871
+ frame.classList.add(INDETERMINATE);
16872
+ }
16873
+ }
16874
+ textWrap.appendChild(checkWrap);
16875
+ }
16876
+ // Icon
16877
+ if (node.iconCss) {
16878
+ var iconEl = this.createElement('span', {
16879
+ className: ICON + ' ' + node.iconCss
16880
+ });
16881
+ textWrap.appendChild(iconEl);
16882
+ }
16883
+ // Image
16884
+ if (node.imageUrl) {
16885
+ var img = document.createElement('img');
16886
+ img.src = node.imageUrl;
16887
+ img.classList.add(IMAGE);
16888
+ textWrap.appendChild(img);
16889
+ }
16890
+ // Text anchor
16891
+ var anchor = this.createElement('span', {
16892
+ className: 'e-list-text',
16893
+ innerHTML: node.text
16894
+ });
16895
+ textWrap.appendChild(anchor);
16896
+ li.appendChild(textWrap);
16897
+ // nodeTemplate
16898
+ if (!isNullOrUndefined(this.nodeTemplateFn)) {
16899
+ var rawData = this.findNodeInTreeData(node.id);
16900
+ if (!isNullOrUndefined(rawData)) {
16901
+ anchor.innerHTML = '';
16902
+ this.renderNodeTemplate(rawData, anchor, node.id);
16903
+ }
16904
+ }
16905
+ // State classes
16906
+ if (node.expanded) {
16907
+ li.classList.add(EXPANDED);
16908
+ li.setAttribute('aria-expanded', 'true');
16909
+ }
16910
+ else if (node.hasChildren) {
16911
+ li.classList.add(NODECOLLAPSED);
16912
+ li.setAttribute('aria-expanded', 'false');
16913
+ }
16914
+ if (node.selected) {
16915
+ li.classList.add(ACTIVE);
16916
+ li.setAttribute('aria-selected', 'true');
16917
+ }
16918
+ if (node.hasChildren) {
16919
+ li.classList.add(CHILD);
16920
+ }
16921
+ // Wire the focus event so `focusIn()` fires when the user Tabs into this
16922
+ // item. The non-virtualized path registers this in `beforeNodeCreate()`,
16923
+ // but virtual items never go through that callback, so we must register
16924
+ // it here to keep focus styling (e.g. `e-focused`) consistent between
16925
+ // virtual and non-virtual modes. `unWireEvents()` already removes this
16926
+ // handler during teardown by iterating over the rendered `<li>`s.
16927
+ EventHandler.add(li, 'focus', this.focusIn, this);
16928
+ return li;
16929
+ };
16930
+ /**
16931
+ * Initialises the virtual-scrolling layout:
16932
+ * 1. Adds the `e-virtualization` CSS class and applies a fixed `height` + `overflow-y`.
16933
+ * 2. Flattens all visible nodes into `virtualFlatNodes`.
16934
+ * 3. Measures item height and viewport height.
16935
+ * 4. Computes the initial window and renders it.
16936
+ * 5. Wires the scroll event handler.
16937
+ *
16938
+ * @returns {void}
16939
+ * @private
16940
+ */
16941
+ TreeView.prototype.initVirtualization = function () {
16942
+ addClass([this.element], VIRTUALIZATION);
16943
+ // Apply container height
16944
+ var px = this.parseHeightToPixels();
16945
+ if (px > 0) {
16946
+ this.element.style.height = px + 'px';
16947
+ this.element.style.overflowY = 'auto';
16948
+ }
16949
+ // Ensure the inner UL is ready
16950
+ if (!this.ulElement) {
16951
+ this.ulElement = select('.' + PARENTITEM, this.element);
16952
+ }
16953
+ if (!this.ulElement) {
16954
+ return;
16955
+ }
16956
+ // Make the UL position-relative for the absolute spacers
16957
+ this.ulElement.style.position = 'relative';
16958
+ // Rebuild flat list
16959
+ this.virtualFlatNodes = this.flattenVisibleNodes();
16960
+ this.rebuildVirtualNodeMap();
16961
+ // Measure dimensions
16962
+ this.virtualViewportHeight = px > 0 ? px : this.element.offsetHeight;
16963
+ // Render a single probe item to measure the real item height before the full render
16964
+ if (this.virtualFlatNodes.length > 0) {
16965
+ var probe = this.createVirtualListItem(this.virtualFlatNodes[0], true);
16966
+ this.ulElement.appendChild(probe);
16967
+ this.virtualItemHeight = probe.offsetHeight > 0 ? probe.offsetHeight : this.virtualItemHeight;
16968
+ this.ulElement.removeChild(probe);
16969
+ }
16970
+ // Compute window and render all items in the initial viewport
16971
+ var win = this.getVirtualWindow();
16972
+ this.virtualStartIndex = win.startIndex;
16973
+ this.virtualEndIndex = win.endIndex;
16974
+ this.renderVirtualItems();
16975
+ // Wire scroll
16976
+ this.wireVirtualScrollEvents();
16977
+ };
16978
+ /**
16979
+ * Wires the scroll event listener on the TreeView element.
16980
+ *
16981
+ * @returns {void}
16982
+ * @private
16983
+ */
16984
+ TreeView.prototype.wireVirtualScrollEvents = function () {
16985
+ this.unwireVirtualScrollEvents();
16986
+ this.virtualScrollBound = this.onVirtualScroll.bind(this);
16987
+ this.element.addEventListener('scroll', this.virtualScrollBound, { passive: true });
16988
+ };
16989
+ /**
16990
+ * Removes the scroll event listener previously wired by `wireVirtualScrollEvents`.
16991
+ *
16992
+ * @returns {void}
16993
+ * @private
16994
+ */
16995
+ TreeView.prototype.unwireVirtualScrollEvents = function () {
16996
+ if (this.virtualScrollBound) {
16997
+ this.element.removeEventListener('scroll', this.virtualScrollBound);
16998
+ this.virtualScrollBound = null;
16999
+ }
17000
+ if (this.virtualScrollRAF !== null) {
17001
+ cancelAnimationFrame(this.virtualScrollRAF);
17002
+ this.virtualScrollRAF = null;
17003
+ }
17004
+ };
17005
+ /**
17006
+ * Scroll handler for the virtual TreeView. Throttled via `requestAnimationFrame`.
17007
+ * Shows the loading mask during fast scrolls, re-renders the window, then hides the mask.
17008
+ *
17009
+ * @returns {void}
17010
+ * @private
17011
+ */
17012
+ TreeView.prototype.onVirtualScroll = function () {
17013
+ var _this = this;
17014
+ if (this.virtualScrollRAF !== null) {
17015
+ cancelAnimationFrame(this.virtualScrollRAF);
17016
+ }
17017
+ // ── Tab-induced focus guard ────────────────────────────────────────────────
17018
+ // When the user Tabs into a `<li tabindex="0">` that is in the middle of
17019
+ // the treeview, the browser auto-scrolls the container so the focused
17020
+ // element becomes visible. That fires a `scroll` event here, but the
17021
+ // virtual window already contains the focused element — re-rendering
17022
+ // would destroy the focused `<li>` and break focus. Detect this case
17023
+ // via the flag set in `keyDownHandler` and skip the re-render entirely.
17024
+ if (this.virtualSkipNextScrollRender) {
17025
+ this.virtualSkipNextScrollRender = false;
17026
+ // Update prev scrollTop so a subsequent user-driven scroll still
17027
+ // calculates the correct delta.
17028
+ this.virtualPrevScrollTop = this.element.scrollTop;
17029
+ return;
17030
+ }
17031
+ var currentScrollTop = this.element.scrollTop;
17032
+ var delta = Math.abs(currentScrollTop - this.virtualPrevScrollTop);
17033
+ var fastScrollThreshold = this.virtualViewportHeight * 2;
17034
+ if (delta > fastScrollThreshold) {
17035
+ this.showVirtualMask(currentScrollTop);
17036
+ }
17037
+ this.virtualScrollRAF = requestAnimationFrame(function () {
17038
+ _this.virtualScrollRAF = null;
17039
+ _this.virtualPrevScrollTop = _this.element.scrollTop;
17040
+ var win = _this.getVirtualWindow();
17041
+ _this.virtualStartIndex = win.startIndex;
17042
+ _this.virtualEndIndex = win.endIndex;
17043
+ _this.renderVirtualItems();
17044
+ // Hide the skeleton mask after a short delay so it stays visible long enough
17045
+ // for the user to perceive the loading indicator even on short scrolls.
17046
+ setTimeout(function () {
17047
+ _this.hideVirtualMask();
17048
+ // Keep the (now hidden) mask's coords fresh for the next show.
17049
+ _this.updateMaskPosition();
17050
+ }, 200);
17051
+ });
17052
+ };
17053
+ /**
17054
+ * Creates the skeleton mask overlay used to indicate loading during fast scrolls.
17055
+ * Skeleton instances are stored in `skeletonInstances` for later disposal.
17056
+ *
17057
+ * @returns {void}
17058
+ * @private
17059
+ */
17060
+ TreeView.prototype.createVirtualMask = function () {
17061
+ if (this.virtualMaskElement) {
17062
+ return;
17063
+ }
17064
+ // Use position:fixed so the mask is anchored to the visible viewport (not to the
17065
+ // scrolling container). Width/height/left/top are set/refreshed by
17066
+ // updateMaskPosition() so the mask always covers the TreeView's visible area
17067
+ // regardless of how the page itself is scrolled.
17068
+ var rect = this.element.getBoundingClientRect();
17069
+ var scrollBarWidth = this.element.offsetWidth - this.element.clientWidth;
17070
+ var maskParent = this.createElement('div', {
17071
+ className: VIRTUAL_MASK_PARENT,
17072
+ 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;"
17073
+ });
17074
+ var visibleRows = Math.ceil(this.virtualViewportHeight / this.virtualItemHeight) + 1;
17075
+ for (var r = 0; r < visibleRows; r++) {
17076
+ var maskItem = this.createElement('div', {
17077
+ className: VIRTUAL_MASK_ITEM,
17078
+ styles: "display:flex;align-items:center;height:" + this.virtualItemHeight + "px;padding:0 12px;gap:8px;"
17079
+ });
17080
+ // Skeleton circle for icon placeholder
17081
+ // const iconHolder: HTMLElement = this.createElement('span', {
17082
+ // styles: `width:20px;height:20px;`
17083
+ // });
17084
+ // maskItem.appendChild(iconHolder);
17085
+ // const iconSkel: Skeleton = new Skeleton({
17086
+ // shape: 'Circle',
17087
+ // width: '20px',
17088
+ // height: '20px',
17089
+ // cssClass: 'e-virtual-skel-icon'
17090
+ // });
17091
+ // iconSkel.appendTo(iconHolder);
17092
+ // this.skeletonInstances.push(iconSkel);
17093
+ // Skeleton text bar
17094
+ var textHolder = this.createElement('span');
17095
+ textHolder.style.flex = '1';
17096
+ textHolder.style.height = '14px';
17097
+ maskItem.appendChild(textHolder);
17098
+ var textSkel = new Skeleton({
17099
+ shape: 'Text',
17100
+ width: '80%',
17101
+ height: '14px',
17102
+ cssClass: 'e-virtual-skel-text'
17103
+ });
17104
+ textSkel.appendTo(textHolder);
17105
+ this.skeletonInstances.push(textSkel);
17106
+ maskParent.appendChild(maskItem);
17107
+ }
17108
+ this.virtualMaskElement = maskParent;
17109
+ this.element.appendChild(maskParent);
17110
+ };
17111
+ /**
17112
+ * Refreshes the mask position so it always overlays the TreeView's viewport.
17113
+ * Called on every show / scroll, since `position:fixed` is relative to the
17114
+ * page viewport, not to the TreeView scroll container.
17115
+ *
17116
+ * @returns {void}
17117
+ * @private
17118
+ */
17119
+ TreeView.prototype.updateMaskPosition = function () {
17120
+ if (!this.virtualMaskElement) {
17121
+ return;
17122
+ }
17123
+ var rect = this.element.getBoundingClientRect();
17124
+ var scrollBarWidth = this.element.offsetWidth - this.element.clientWidth;
17125
+ this.virtualMaskElement.style.top = rect.top + 'px';
17126
+ this.virtualMaskElement.style.left = rect.left + 'px';
17127
+ this.virtualMaskElement.style.width = (rect.width - scrollBarWidth) + 'px';
17128
+ this.virtualMaskElement.style.height = rect.height + 'px';
17129
+ };
17130
+ /**
17131
+ * Destroys all skeleton instances and removes the mask overlay from the DOM.
17132
+ *
17133
+ * @returns {void}
17134
+ * @private
17135
+ */
17136
+ TreeView.prototype.destroyVirtualMask = function () {
17137
+ for (var i = this.skeletonInstances.length - 1; i >= 0; i--) {
17138
+ this.skeletonInstances[parseInt(i.toString(), 10)].destroy();
17139
+ }
17140
+ this.skeletonInstances = [];
17141
+ if (this.virtualMaskElement) {
17142
+ if (this.virtualMaskElement.parentElement) {
17143
+ this.virtualMaskElement.parentElement.removeChild(this.virtualMaskElement);
17144
+ }
17145
+ this.virtualMaskElement = null;
17146
+ }
17147
+ };
17148
+ /**
17149
+ * Positions and shows the skeleton mask at the current `scrollTop` position.
17150
+ *
17151
+ * @param {number} _scrollTop - Current scrollTop of the container.
17152
+ * @returns {void}
17153
+ * @private
17154
+ */
17155
+ TreeView.prototype.showVirtualMask = function (_scrollTop) {
17156
+ if (!this.virtualMaskElement) {
17157
+ this.createVirtualMask();
17158
+ }
17159
+ if (this.virtualMaskElement) {
17160
+ // Refresh the fixed-position coords in case the TreeView was moved/resized
17161
+ // since the last time the mask was shown.
17162
+ this.updateMaskPosition();
17163
+ this.virtualMaskElement.style.display = 'block';
17164
+ // Visually hide the real list while the skeleton is shown
17165
+ if (this.ulElement) {
17166
+ this.ulElement.style.visibility = 'hidden';
17167
+ }
17168
+ }
17169
+ };
17170
+ /**
17171
+ * Hides the skeleton mask overlay after the virtual window has been rendered.
17172
+ *
17173
+ * @returns {void}
17174
+ * @private
17175
+ */
17176
+ TreeView.prototype.hideVirtualMask = function () {
17177
+ if (this.virtualMaskElement) {
17178
+ this.virtualMaskElement.style.display = 'none';
17179
+ // Restore real list visibility
17180
+ if (this.ulElement) {
17181
+ this.ulElement.style.visibility = '';
17182
+ }
17183
+ }
17184
+ };
17185
+ /**
17186
+ * Ensures the node identified by `id` is scrolled into view inside the virtual container.
17187
+ * If the node falls outside the rendered window, the container's `scrollTop` is adjusted so
17188
+ * that the node becomes visible, the window is re-computed and `renderVirtualItems` is called.
17189
+ *
17190
+ * @param {string} id - The `data-uid` of the target node.
17191
+ * @returns {void}
17192
+ * @private
17193
+ */
17194
+ TreeView.prototype.ensureVirtualNodeVisible = function (id) {
17195
+ var idx = this.virtualNodeMap.get(id);
17196
+ if (isNullOrUndefined(idx)) {
17197
+ return;
17198
+ }
17199
+ var itemH = this.virtualItemHeight;
17200
+ var vpH = this.virtualViewportHeight;
17201
+ var nodeTop = idx * itemH;
17202
+ var nodeBottom = nodeTop + itemH;
17203
+ var currentTop = this.element.scrollTop;
17204
+ var currentBottom = currentTop + vpH;
17205
+ if (nodeTop < currentTop) {
17206
+ this.element.scrollTop = nodeTop;
17207
+ }
17208
+ else if (nodeBottom > currentBottom) {
17209
+ this.element.scrollTop = nodeBottom - vpH;
17210
+ }
17211
+ else {
17212
+ return; // already visible, no re-render needed
17213
+ }
17214
+ // Re-render the new window synchronously so the <li> is in the DOM
17215
+ var win = this.getVirtualWindow();
17216
+ this.virtualStartIndex = win.startIndex;
17217
+ this.virtualEndIndex = win.endIndex;
17218
+ this.renderVirtualItems();
17219
+ };
17220
+ /**
17221
+ * Recursively walks the treeData (and any nested `childFieldName` arrays) to
17222
+ * find the node whose `idFieldName` matches the given `id` (compared as
17223
+ * strings). Used by the virtual-mode expand/collapse handlers so they can
17224
+ * locate nodes at any depth — not just the root level — before writing the
17225
+ * `expanded` flag back to the datasource.
17226
+ *
17227
+ * @param {string} id - The id (as a string) of the node to locate.
17228
+ * @returns {Object | null} The matching raw datasource node, or null if not found.
17229
+ * @private
17230
+ */
17231
+ TreeView.prototype.findNodeInTreeData = function (id) {
17232
+ if (!this.treeData) {
17233
+ return null;
17234
+ }
17235
+ var idFieldName = this.fields.id.toString();
17236
+ var childFieldName = this.fields.child.toString();
17237
+ var findRecursive = function (nodes) {
17238
+ if (!nodes || nodes.length === 0) {
17239
+ return null;
17240
+ }
17241
+ for (var i = 0; i < nodes.length; i++) {
17242
+ var node = nodes[i];
17243
+ if (node[idFieldName] && node[idFieldName].toString() === id) {
17244
+ return node;
17245
+ }
17246
+ var children = getValue(childFieldName, node);
17247
+ if (children && Array.isArray(children)) {
17248
+ var found = findRecursive(children);
17249
+ if (found) {
17250
+ return found;
17251
+ }
17252
+ }
17253
+ }
17254
+ return null;
17255
+ };
17256
+ return findRecursive(this.treeData);
17257
+ };
17258
+ TreeView.prototype.handleVirtualExpand = function (id, isKeyboardNav) {
17259
+ // ── Cancel any pending scroll RAF ─────────────────────────────────────────
17260
+ // A scroll RAF queued before the expand click would run asynchronously after
17261
+ // this synchronous expand completes, re-rendering the window from a stale
17262
+ // scrollTop and moving the parent node to the wrong position. Cancel it now
17263
+ // so the expand render is authoritative.
17264
+ if (this.virtualScrollRAF !== null) {
17265
+ cancelAnimationFrame(this.virtualScrollRAF);
17266
+ this.virtualScrollRAF = null;
17267
+ }
17268
+ if (this.expandedNodes.indexOf(id) === -1) {
17269
+ this.expandedNodes.push(id);
17270
+ }
17271
+ // Also update virtualExpandedNodesSet to keep it in sync
17272
+ if (this.virtualExpandedNodesSet) {
17273
+ this.virtualExpandedNodesSet.add(id);
17274
+ }
17275
+ if (this.treeData) {
17276
+ var acutalRootData = this.findNodeInTreeData(id);
17277
+ if (acutalRootData && this.fields && this.fields.expanded) {
17278
+ setValue(this.fields.expanded, true, acutalRootData);
17279
+ }
17280
+ }
17281
+ // ── Fix: Pin focused ID to the expanding node for keyboard navigation ──
17282
+ // When keyboard-triggered, the expanded node IS definitively the focused node
17283
+ // (`id` comes from currLi.getAttribute('data-uid') in expandNode, where currLi
17284
+ // is getFocusedNode()). Set unconditionally — the old conditional guard
17285
+ // `!this.virtualFocusedId` could leave a stale or null value if a previous
17286
+ // RAF render (from arrow-key scroll) hadn't yet completed, causing branch 3
17287
+ // (first item) to fire in renderVirtualItems() instead of branch 2.
17288
+ // For mouse clicks (isKeyboardNav=false), clear virtualFocusedId so that
17289
+ // renderVirtualItems() Branch 2 does NOT apply the FOCUS/FOCUSED classes —
17290
+ // mouse-based expand/collapse should never add the e-focused class.
17291
+ if (isKeyboardNav) {
17292
+ this.virtualFocusedId = id;
17293
+ }
17294
+ else {
17295
+ this.virtualFocusedId = null;
17296
+ }
17297
+ this.virtualFlatNodes = this.flattenVisibleNodes();
17298
+ this.rebuildVirtualNodeMap();
17299
+ // ── Snapshot scrollTop before computing the window ────────────────────────
17300
+ // Read scrollTop once here and pass it into getVirtualWindow so that any
17301
+ // browser scroll-anchoring adjustment that happens when the padding values
17302
+ // change cannot affect which window we render.
17303
+ var scrollTopSnapshot = this.element.scrollTop;
17304
+ var win = this.getVirtualWindowAt(scrollTopSnapshot);
17305
+ this.virtualStartIndex = win.startIndex;
17306
+ this.virtualEndIndex = win.endIndex;
17307
+ // ── Block ALL scroll-triggered re-renders during this expand ─────────────
17308
+ // renderVirtualItems() calls li.focus() in branch 2 (keyboard focus restore).
17309
+ // The post-render focus() call below can also trigger browser scroll. Both
17310
+ // can fire the 'scroll' event and cause onVirtualScroll() to queue a RAF.
17311
+ // We set the skip flag ONCE here; it is consumed by the first scroll event.
17312
+ // To cover the second potential scroll (from the focus() call below) we use
17313
+ // preventScroll:true on that call so it never moves the container at all.
17314
+ this.virtualSkipNextScrollRender = true;
17315
+ this.renderVirtualItems();
17316
+ // renderVirtualItems() replaces all <li> elements so the old icon element that
17317
+ // had the PROCESS class is gone. Remove PROCESS from the newly rendered icon
17318
+ // so the node can be collapsed/expanded again without being blocked.
17319
+ var newLi = this.getElement(id);
17320
+ if (newLi) {
17321
+ var icon = select('.' + ICON, newLi);
17322
+ if (icon) {
17323
+ removeClass([icon], PROCESS);
17324
+ }
17325
+ // ── Use preventScroll:true ────────────────────────────────────────────
17326
+ // Without preventScroll the browser scrolls the container to bring this
17327
+ // node into view, which fires a second 'scroll' event. The first scroll
17328
+ // was already consumed by virtualSkipNextScrollRender (set above), so the
17329
+ // second scroll would queue a new RAF that re-renders from the
17330
+ // focus-adjusted scrollTop — moving the parent node to the wrong position.
17331
+ newLi.focus({ preventScroll: true });
17332
+ }
17333
+ if (this.isLoaded) {
17334
+ var currLi = this.getElement(id);
17335
+ if (currLi) {
17336
+ var expandArgs = this.getExpandEvent(currLi, null);
17337
+ expandArgs.isInteracted = this.isInteracted;
17338
+ this.trigger('nodeExpanded', expandArgs);
17339
+ }
17340
+ }
17341
+ };
17342
+ TreeView.prototype.handleVirtualCollapse = function (id, isKeyboardNav) {
17343
+ // ── Cancel any pending scroll RAF ─────────────────────────────────────────
17344
+ // Mirrors handleVirtualExpand: a queued RAF from a previous scroll event
17345
+ // must not override the collapse render with a stale window.
17346
+ if (this.virtualScrollRAF !== null) {
17347
+ cancelAnimationFrame(this.virtualScrollRAF);
17348
+ this.virtualScrollRAF = null;
17349
+ }
17350
+ var idx = this.expandedNodes.indexOf(id);
17351
+ if (idx !== -1) {
17352
+ this.expandedNodes.splice(idx, 1);
17353
+ }
17354
+ // Also update virtualExpandedNodesSet to keep it in sync
17355
+ if (this.virtualExpandedNodesSet) {
17356
+ this.virtualExpandedNodesSet.delete(id);
17357
+ }
17358
+ if (this.treeData) {
17359
+ var acutalRootData = this.findNodeInTreeData(id);
17360
+ if (acutalRootData && this.fields && this.fields.expanded) {
17361
+ setValue(this.fields.expanded, false, acutalRootData);
17362
+ }
17363
+ }
17364
+ // ── Fix: Pin focused ID to the collapsing node for keyboard navigation ──
17365
+ // Same reasoning as handleVirtualExpand: when keyboard-triggered, `id` is
17366
+ // definitively the focused node. Set unconditionally to avoid the stale/null
17367
+ // state that could cause the RAF re-render to fall through to branch 3.
17368
+ // For mouse clicks (isKeyboardNav=false), clear virtualFocusedId so that
17369
+ // renderVirtualItems() Branch 2 does NOT apply the FOCUS/FOCUSED classes —
17370
+ // mouse-based expand/collapse should never add the e-focused class.
17371
+ if (isKeyboardNav) {
17372
+ this.virtualFocusedId = id;
17373
+ }
17374
+ else {
17375
+ this.virtualFocusedId = null;
17376
+ }
17377
+ this.virtualFlatNodes = this.flattenVisibleNodes();
17378
+ this.rebuildVirtualNodeMap();
17379
+ // ── Snapshot scrollTop before computing the window ────────────────────────
17380
+ // Same rationale as handleVirtualExpand: capture scrollTop before any focus()
17381
+ // call or padding change can cause the browser to auto-scroll.
17382
+ var scrollTopSnapshot = this.element.scrollTop;
17383
+ var win = this.getVirtualWindowAt(scrollTopSnapshot);
17384
+ this.virtualStartIndex = win.startIndex;
17385
+ this.virtualEndIndex = win.endIndex;
17386
+ // ── Block ALL scroll-triggered re-renders during this collapse ────────────
17387
+ // Same as handleVirtualExpand: use preventScroll:true on the focus() call
17388
+ // below so the second potential scroll event is never fired.
17389
+ this.virtualSkipNextScrollRender = true;
17390
+ this.renderVirtualItems();
17391
+ // renderVirtualItems() replaces all <li> elements so the old icon element that
17392
+ // had the PROCESS class is gone. Remove PROCESS from the newly rendered icon
17393
+ // so the node can be expanded/collapsed again without being blocked.
17394
+ var newLi = this.getElement(id);
17395
+ if (newLi) {
17396
+ var icon = select('.' + ICON, newLi);
17397
+ if (icon) {
17398
+ removeClass([icon], PROCESS);
17399
+ }
17400
+ // ── Use preventScroll:true to avoid consuming virtualSkipNextScrollRender ─
17401
+ newLi.focus({ preventScroll: true });
17402
+ }
17403
+ if (this.isLoaded) {
17404
+ var currLi = this.getElement(id);
17405
+ if (currLi) {
17406
+ var colArgs = this.getExpandEvent(currLi, null);
17407
+ colArgs.isInteracted = this.isInteracted;
17408
+ this.trigger('nodeCollapsed', colArgs);
17409
+ }
17410
+ }
17411
+ };
17412
+ /**
17413
+ * Updates the selected state of a node in `virtualFlatNodes` and re-renders the window.
17414
+ *
17415
+ * @param {string} id - The `data-uid` of the target node.
17416
+ * @param {boolean} selected - Whether the node is now selected.
17417
+ * @returns {void}
17418
+ * @private
17419
+ */
17420
+ TreeView.prototype.updateVirtualNodeSelection = function (id, selected) {
17421
+ var idx = this.virtualNodeMap.get(id);
17422
+ if (!isNullOrUndefined(idx)) {
17423
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].selected = selected;
17424
+ this.renderVirtualItems();
17425
+ }
17426
+ };
17427
+ /**
17428
+ * Updates the checkbox state of a node in `virtualFlatNodes` and re-renders the window.
17429
+ *
17430
+ * @param {string} id - The `data-uid` of the target node.
17431
+ * @param {'check' | 'uncheck' | 'indeterminate'} state - The new check state.
17432
+ * @returns {void}
17433
+ * @private
17434
+ */
17435
+ TreeView.prototype.updateVirtualNodeCheckState = function (id, state) {
17436
+ var idx = this.virtualNodeMap.get(id);
17437
+ if (!isNullOrUndefined(idx)) {
17438
+ this.virtualFlatNodes[parseInt(idx.toString(), 10)].isChecked =
17439
+ state === 'check' ? true : state === 'indeterminate' ? 'indeterminate' : false;
17440
+ // Keep the persistent indeterminate set in sync so flattenVisibleNodes()
17441
+ // correctly restores state after any full list rebuild (e.g. collapse).
17442
+ if (state === 'indeterminate') {
17443
+ this.virtualIndeterminateNodesSet.add(id);
17444
+ }
17445
+ else {
17446
+ this.virtualIndeterminateNodesSet.delete(id);
17447
+ }
17448
+ this.renderVirtualItems();
17449
+ }
17450
+ };
17451
+ // ═══════════════════════════════════════════════════════════════════════════════
17452
+ // END VIRTUALIZATION
17453
+ // ═══════════════════════════════════════════════════════════════════════════════
15983
17454
  /**
15984
17455
  * Called internally if any of the property value changed.
15985
17456
  *
@@ -16117,6 +17588,33 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
16117
17588
  this.listBaseOption.itemNavigable = newProp.fullRowNavigable;
16118
17589
  this.reRenderNodes();
16119
17590
  break;
17591
+ case 'enableVirtualization':
17592
+ if (this.enableVirtualization) {
17593
+ if (!isNullOrUndefined(this.height)) {
17594
+ this.initVirtualization();
17595
+ }
17596
+ }
17597
+ else {
17598
+ // Tear down virtualization and do a full re-render
17599
+ this.unwireVirtualScrollEvents();
17600
+ this.destroyVirtualMask();
17601
+ removeClass([this.element], VIRTUALIZATION);
17602
+ this.element.style.height = '';
17603
+ this.element.style.overflowY = '';
17604
+ this.reRenderNodes();
17605
+ }
17606
+ break;
17607
+ case 'height':
17608
+ if (this.enableVirtualization) {
17609
+ // Re-initialise so the new height takes effect
17610
+ this.unwireVirtualScrollEvents();
17611
+ this.initVirtualization();
17612
+ }
17613
+ else {
17614
+ var px = this.parseHeightToPixels();
17615
+ this.element.style.height = px > 0 ? px + 'px' : '';
17616
+ }
17617
+ break;
16120
17618
  }
16121
17619
  }
16122
17620
  };
@@ -16126,6 +17624,13 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
16126
17624
  * @returns {void}
16127
17625
  */
16128
17626
  TreeView.prototype.destroy = function () {
17627
+ // Tear down virtualization before standard cleanup
17628
+ if (this.enableVirtualization) {
17629
+ this.unwireVirtualScrollEvents();
17630
+ this.destroyVirtualMask();
17631
+ this.virtualFlatNodes = [];
17632
+ this.virtualNodeMap.clear();
17633
+ }
16129
17634
  this.clearTemplate();
16130
17635
  this.element.removeAttribute('aria-activedescendant');
16131
17636
  this.unWireEvents();
@@ -16210,9 +17715,25 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
16210
17715
  if (isNullOrUndefined(fNode)) {
16211
17716
  this.setNodeFocusable();
16212
17717
  }
16213
- this.updateCheckedStateFromDS();
16214
- if (this.showCheckBox && dropLi && this.autoCheck) {
16215
- this.ensureParentCheckState(dropLi);
17718
+ // In virtual mode (with a fixed height), rebuild the flat node list and re-render
17719
+ // the window so the newly added nodes appear in the correct hierarchical position.
17720
+ // When height is not provided, the tree uses normal rendering and the DOM is
17721
+ // already updated by addGivenNodes() above.
17722
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
17723
+ this.setRootData();
17724
+ this.rebuildVirtualCheckCache();
17725
+ this.virtualFlatNodes = this.flattenVisibleNodes();
17726
+ this.rebuildVirtualNodeMap();
17727
+ var win = this.getVirtualWindow();
17728
+ this.virtualStartIndex = win.startIndex;
17729
+ this.virtualEndIndex = win.endIndex;
17730
+ this.renderVirtualItems();
17731
+ }
17732
+ else {
17733
+ this.updateCheckedStateFromDS();
17734
+ if (this.showCheckBox && dropLi && this.autoCheck) {
17735
+ this.ensureParentCheckState(dropLi);
17736
+ }
16216
17737
  }
16217
17738
  if ((this.fields.dataSource instanceof DataManager === false)) {
16218
17739
  this.preventExpand = false;
@@ -16257,6 +17778,25 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
16257
17778
  * @returns {void}
16258
17779
  */
16259
17780
  TreeView.prototype.collapseAll = function (nodes, level, excludeHiddenNodes) {
17781
+ var _this = this;
17782
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
17783
+ this.virtualExpandedNodesSet.clear();
17784
+ this.setProperties({ expandedNodes: [] }, true);
17785
+ var setAllCollapsed_1 = function (nodes) {
17786
+ if (!nodes) {
17787
+ return;
17788
+ }
17789
+ for (var i = 0; i < nodes.length; i++) {
17790
+ var node = nodes[parseInt(i.toString(), 10)];
17791
+ node['expanded'] = false;
17792
+ var children = getValue(_this.fields.child.toString(), node);
17793
+ if (children && Array.isArray(children)) {
17794
+ setAllCollapsed_1(children);
17795
+ }
17796
+ }
17797
+ };
17798
+ setAllCollapsed_1(this.treeData);
17799
+ }
16260
17800
  if (!isNullOrUndefined(nodes)) {
16261
17801
  this.doGivenAction(nodes, COLLAPSIBLE, false);
16262
17802
  }
@@ -16334,6 +17874,33 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
16334
17874
  */
16335
17875
  TreeView.prototype.expandAll = function (nodes, level, excludeHiddenNodes, preventAnimation) {
16336
17876
  this.isAnimate = !preventAnimation;
17877
+ if (this.enableVirtualization && !isNullOrUndefined(this.height)) {
17878
+ var allParentIds_1 = [];
17879
+ var idFieldName_1 = this.fields.id;
17880
+ var childFieldName = this.fields.child.toString();
17881
+ var isDataTypeFlat = this.dataType === 1;
17882
+ var processAllParents = function (nodeList) {
17883
+ if (!nodeList) {
17884
+ return;
17885
+ }
17886
+ for (var i = 0; i < nodeList.length; i++) {
17887
+ var node = nodeList[parseInt(i.toString(), 10)];
17888
+ var nodeId = getValue(idFieldName_1, node);
17889
+ if (node.hasChild) {
17890
+ allParentIds_1.push(nodeId);
17891
+ node.expanded = true;
17892
+ }
17893
+ }
17894
+ };
17895
+ processAllParents(this.treeData);
17896
+ for (var i = 0; i < allParentIds_1.length; i++) {
17897
+ var id = allParentIds_1[parseInt(i.toString(), 10)];
17898
+ if (!this.virtualExpandedNodesSet.has(id)) {
17899
+ this.virtualExpandedNodesSet.add(id);
17900
+ }
17901
+ }
17902
+ this.setProperties({ expandedNodes: allParentIds_1 }, true);
17903
+ }
16337
17904
  if (!isNullOrUndefined(nodes)) {
16338
17905
  this.doGivenAction(nodes, EXPANDABLE, true);
16339
17906
  }
@@ -16636,6 +18203,12 @@ var TreeView = /** @__PURE__ @class */ (function (_super) {
16636
18203
  __decorate$8([
16637
18204
  Property(false)
16638
18205
  ], TreeView.prototype, "allowTextWrap", void 0);
18206
+ __decorate$8([
18207
+ Property(false)
18208
+ ], TreeView.prototype, "enableVirtualization", void 0);
18209
+ __decorate$8([
18210
+ Property()
18211
+ ], TreeView.prototype, "height", void 0);
16639
18212
  __decorate$8([
16640
18213
  Complex({}, NodeAnimationSettings)
16641
18214
  ], TreeView.prototype, "animation", void 0);