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