tree-element 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +22 -20
  2. package/lib/dataLoader.d.ts.map +1 -1
  3. package/lib/dataLoader.js +4 -1
  4. package/lib/dataLoader.js.map +1 -1
  5. package/lib/dragAndDropHandler/generateHitAreas.d.ts.map +1 -1
  6. package/lib/dragAndDropHandler/generateHitAreas.js +3 -2
  7. package/lib/dragAndDropHandler/generateHitAreas.js.map +1 -1
  8. package/lib/dragAndDropHandler/index.d.ts +2 -4
  9. package/lib/dragAndDropHandler/index.d.ts.map +1 -1
  10. package/lib/dragAndDropHandler/index.js +2 -9
  11. package/lib/dragAndDropHandler/index.js.map +1 -1
  12. package/lib/elementsRenderer.d.ts +7 -2
  13. package/lib/elementsRenderer.d.ts.map +1 -1
  14. package/lib/elementsRenderer.js +70 -23
  15. package/lib/elementsRenderer.js.map +1 -1
  16. package/lib/events.d.ts +13 -7
  17. package/lib/events.d.ts.map +1 -1
  18. package/lib/folderOpenState.d.ts +8 -0
  19. package/lib/folderOpenState.d.ts.map +1 -0
  20. package/lib/folderOpenState.js +22 -0
  21. package/lib/folderOpenState.js.map +1 -0
  22. package/lib/index.d.ts +17 -16
  23. package/lib/index.d.ts.map +1 -1
  24. package/lib/index.js +93 -49
  25. package/lib/index.js.map +1 -1
  26. package/lib/keyHandler.d.ts +2 -0
  27. package/lib/keyHandler.d.ts.map +1 -1
  28. package/lib/keyHandler.js +18 -0
  29. package/lib/keyHandler.js.map +1 -1
  30. package/lib/mouseHandler.d.ts.map +1 -1
  31. package/lib/mouseHandler.js +12 -12
  32. package/lib/mouseHandler.js.map +1 -1
  33. package/lib/node.d.ts +12 -10
  34. package/lib/node.d.ts.map +1 -1
  35. package/lib/node.js +12 -10
  36. package/lib/node.js.map +1 -1
  37. package/lib/nodeElement/folderElement.d.ts +7 -5
  38. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  39. package/lib/nodeElement/folderElement.js +33 -53
  40. package/lib/nodeElement/folderElement.js.map +1 -1
  41. package/lib/options.d.ts +19 -17
  42. package/lib/options.d.ts.map +1 -1
  43. package/lib/options.js +12 -0
  44. package/lib/options.js.map +1 -0
  45. package/lib/saveStateHandler.d.ts.map +1 -1
  46. package/lib/saveStateHandler.js +6 -6
  47. package/lib/saveStateHandler.js.map +1 -1
  48. package/lib/selectNodeHandler.d.ts.map +1 -1
  49. package/lib/selectNodeHandler.js +13 -1
  50. package/lib/selectNodeHandler.js.map +1 -1
  51. package/lib/version.d.ts +1 -1
  52. package/lib/version.js +1 -1
  53. package/lib/version.js.map +1 -1
  54. package/package.json +35 -29
  55. package/src/dataLoader.ts +3 -1
  56. package/src/dragAndDropHandler/generateHitAreas.ts +3 -2
  57. package/src/dragAndDropHandler/index.ts +3 -17
  58. package/src/elementsRenderer.ts +84 -33
  59. package/src/events.ts +13 -7
  60. package/src/folderOpenState.ts +38 -0
  61. package/src/index.ts +109 -59
  62. package/src/keyHandler.ts +20 -0
  63. package/src/mouseHandler.ts +15 -13
  64. package/src/node.ts +12 -10
  65. package/src/nodeElement/folderElement.ts +45 -70
  66. package/src/options.ts +36 -17
  67. package/src/saveStateHandler.ts +5 -8
  68. package/src/selectNodeHandler.ts +12 -1
  69. package/src/version.ts +1 -1
  70. package/tree_element.css +32 -5
  71. package/tree_element.debug.js +291 -165
  72. package/tree_element.debug.js.map +1 -1
  73. package/tree_element.js +2 -2
  74. package/tree_element.js.map +1 -1
@@ -1,5 +1,5 @@
1
1
  /*
2
- Tree-element 0.4.0
2
+ Tree-element 0.4.1
3
3
 
4
4
  Copyright 2026 Marco Braak
5
5
 
@@ -111,9 +111,12 @@ var TreeElement = (function () {
111
111
  };
112
112
  let signal = this._abortController.signal;
113
113
  url._setSearchParam("_", Date.now().toString());
114
+
115
+ // Accept, not Content-Type: the request has no body. A cross-origin request
116
+ // with only an Accept header also needs no CORS preflight.
114
117
  return fetch(url._value(), {
115
118
  headers: {
116
- "Content-Type": "application/json"
119
+ Accept: "application/json"
117
120
  },
118
121
  signal
119
122
  }).then(handleResponse).catch(error => {
@@ -262,8 +265,9 @@ var TreeElement = (function () {
262
265
  lastTop = top;
263
266
  };
264
267
  let handleAfterOpenFolder = (node, nextNode) => {
265
- if (node === currentNode || nextNode === currentNode) {
266
- // Cannot move before or after current item
268
+ // Not called for the current node: handleOpenFolder stops iterating there
269
+ if (nextNode === currentNode) {
270
+ // Dropping the current node after this folder would leave it where it is
267
271
  addHitPosition(node, null, lastTop);
268
272
  } else {
269
273
  addHitPosition(node, "after", lastTop);
@@ -391,7 +395,6 @@ var TreeElement = (function () {
391
395
  _openFolderDelay: openFolderDelay,
392
396
  _openNode: openNode,
393
397
  _refreshElements: refreshElements,
394
- _slide: slide,
395
398
  _treeElement: treeElement,
396
399
  _triggerEvent: triggerEvent
397
400
  }) {
@@ -409,7 +412,6 @@ var TreeElement = (function () {
409
412
  this._openFolderDelay = openFolderDelay;
410
413
  this._openNode = openNode;
411
414
  this._refreshElements = refreshElements;
412
- this._slide = slide;
413
415
  this._treeElement = treeElement;
414
416
  this._triggerEvent = triggerEvent;
415
417
  this._hoveredArea = null;
@@ -442,9 +444,6 @@ var TreeElement = (function () {
442
444
  this._dragElement._move(positionInfo.pageX, positionInfo.pageY);
443
445
  let area = this._findHoveredArea(positionInfo.pageX, positionInfo.pageY);
444
446
  if (area && this._canMoveToArea(area, this._currentItem)) {
445
- if (!area.node.isFolder()) {
446
- this._stopOpenFolderTimer();
447
- }
448
447
  if (this._hoveredArea !== area) {
449
448
  this._hoveredArea = area;
450
449
 
@@ -503,7 +502,6 @@ var TreeElement = (function () {
503
502
  if (!this._hoveredArea && currentItem && this._onDragStop) {
504
503
  this._onDragStop(currentItem._node, positionInfo.originalEvent);
505
504
  }
506
- return false;
507
505
  }
508
506
  _refresh() {
509
507
  this._removeHitAreas();
@@ -594,8 +592,7 @@ var TreeElement = (function () {
594
592
  }
595
593
  }
596
594
  _mustCaptureElement(element) {
597
- let nodeName = element.nodeName;
598
- return nodeName !== "INPUT" && nodeName !== "SELECT" && nodeName !== "TEXTAREA";
595
+ return !["INPUT", "SELECT", "TEXTAREA"].includes(element.nodeName);
599
596
  }
600
597
  _mustOpenFolderTimer(area) {
601
598
  let node = area.node;
@@ -614,7 +611,7 @@ var TreeElement = (function () {
614
611
  }
615
612
  _startOpenFolderTimer(folder) {
616
613
  let openFolder = () => {
617
- void this._openNode(folder, this._slide).then(() => {
614
+ void this._openNode(folder).then(() => {
618
615
  this._refresh();
619
616
  this._updateDropHint();
620
617
  });
@@ -649,6 +646,24 @@ var TreeElement = (function () {
649
646
  let isInt = n => Number.isInteger(n);
650
647
  let getBoolString = value => value ? "true" : "false";
651
648
 
649
+ /* The open state of a folder is shown on the toggler and on the <li>.
650
+ * These are used both when a folder is rendered and when it is opened or
651
+ * closed, so a rendered folder looks the same as a toggled one.
652
+ */
653
+
654
+ let setTogglerOpenState = (toggler, isOpen, classNames, icons) => {
655
+ toggler.classList.toggle(classNames.closed, !isOpen);
656
+ toggler.replaceChildren();
657
+ let iconElement = isOpen ? icons.opened : icons.closed;
658
+ if (iconElement) {
659
+ toggler.append(iconElement.cloneNode(true));
660
+ }
661
+ };
662
+ let setFolderLiOpenState = (li, isOpen, classNames) => {
663
+ li.classList.toggle(classNames.closed, !isOpen);
664
+ li.setAttribute("aria-expanded", getBoolString(isOpen));
665
+ };
666
+
652
667
  /* Renders the tree to html.
653
668
  *
654
669
  * The children of a closed folder are not rendered. They are rendered when
@@ -695,13 +710,16 @@ var TreeElement = (function () {
695
710
  this._setNodeElement = setNodeElement;
696
711
  this._showEmptyFolder = showEmptyFolder;
697
712
  this._tabIndex = tabIndex;
698
- this._openedIconElement = this._createButtonElement(openedIcon);
699
- this._closedIconElement = this._createButtonElement(closedIcon);
700
- this._rootUlTemplate = this._createUlTemplate(true);
701
- this._groupUlTemplate = this._createUlTemplate(false);
702
- this._nodeTemplate = this._createNodeTemplate();
703
- this._openedFolderTemplate = this._createFolderTemplate(true);
704
- this._closedFolderTemplate = this._createFolderTemplate(false);
713
+ this._icons = {
714
+ closed: this._createButtonElement(closedIcon),
715
+ opened: this._createButtonElement(openedIcon)
716
+ };
717
+ let templates = this._createTemplates();
718
+ this._closedFolderTemplate = templates.closedFolderTemplate;
719
+ this._groupUlTemplate = templates.groupUlTemplate;
720
+ this._nodeTemplate = templates.nodeTemplate;
721
+ this._openedFolderTemplate = templates.openedFolderTemplate;
722
+ this._rootUlTemplate = templates.rootUlTemplate;
705
723
  }
706
724
  _render(fromNode) {
707
725
  if (fromNode?.parent) {
@@ -735,6 +753,43 @@ var TreeElement = (function () {
735
753
  this._createDomElements(this._htmlElement, tree.children, true, 1);
736
754
  }
737
755
  }
756
+
757
+ /* Change dragAndDrop after construction. The rendered lists and the
758
+ * templates for new lists get or lose the dnd class.
759
+ */
760
+ _setDragAndDrop(dragAndDrop) {
761
+ this._dragAndDrop = dragAndDrop;
762
+ this._updateTemplates();
763
+ for (let ul of this._htmlElement.querySelectorAll(`ul.${this._classNames.common}`)) {
764
+ ul.classList.toggle(this._classNames.dnd, dragAndDrop);
765
+ }
766
+ }
767
+
768
+ /* Change multiSelect after construction. Only the templates are updated;
769
+ * the tree must be rendered again.
770
+ */
771
+ _setMultiSelect(multiSelect) {
772
+ this._multiSelect = multiSelect;
773
+ this._updateTemplates();
774
+ }
775
+
776
+ /* Change tabIndex after construction. The rendered nodes and the
777
+ * templates for new nodes get the new tabindex. The tab stop is updated by
778
+ * TreeElement.updateTabStop.
779
+ */
780
+ _setTabIndex(tabIndex) {
781
+ this._tabIndex = tabIndex;
782
+ this._updateTemplates();
783
+ for (let li of this._htmlElement.querySelectorAll("li[role=treeitem]")) {
784
+ if (tabIndex === undefined) {
785
+ li.removeAttribute("tabindex");
786
+ } else if (!this._multiSelect && li.getAttribute("aria-selected") === "true") {
787
+ li.setAttribute("tabindex", String(tabIndex));
788
+ } else {
789
+ li.setAttribute("tabindex", "-1");
790
+ }
791
+ }
792
+ }
738
793
  _attachNodeData(node, li) {
739
794
  node.element = li;
740
795
  this._setNodeElement(li, node);
@@ -790,26 +845,19 @@ var TreeElement = (function () {
790
845
  * state, so there is a template for each.
791
846
  */
792
847
  _createFolderTemplate(isOpen) {
793
- let liClasses = [this._classNames.common, this._classNames.folder];
794
- if (!isOpen) {
795
- liClasses.push(this._classNames.closed);
796
- }
797
- let li = this._createLiTemplate(liClasses.join(" "));
848
+ let li = this._createLiTemplate(`${this._classNames.common} ${this._classNames.folder}`);
849
+ setFolderLiOpenState(li, isOpen, this._classNames);
798
850
  let div = li.firstChild;
799
851
 
800
852
  // The toggler is hidden from screen readers: the icon is decoration, and
801
853
  // aria-expanded on the treeitem has the open state.
802
854
  let toggler = document.createElement("span");
803
- toggler.className = this._getButtonClasses(isOpen);
855
+ toggler.className = this._getButtonClasses();
804
856
  toggler.setAttribute("aria-hidden", "true");
805
- let iconElement = isOpen ? this._openedIconElement : this._closedIconElement;
806
- if (iconElement) {
807
- toggler.append(iconElement.cloneNode(true));
808
- }
857
+ setTogglerOpenState(toggler, isOpen, this._classNames, this._icons);
809
858
  if (this._buttonLeft) {
810
859
  div.append(toggler);
811
860
  }
812
- li.setAttribute("aria-expanded", getBoolString(isOpen));
813
861
 
814
862
  // title span
815
863
  div.append(this._createTitleSpanTemplate(true));
@@ -880,6 +928,15 @@ var TreeElement = (function () {
880
928
  div.append(this._createTitleSpanTemplate(false));
881
929
  return li;
882
930
  }
931
+ _createTemplates() {
932
+ return {
933
+ closedFolderTemplate: this._createFolderTemplate(false),
934
+ groupUlTemplate: this._createUlTemplate(false),
935
+ nodeTemplate: this._createNodeTemplate(),
936
+ openedFolderTemplate: this._createFolderTemplate(true),
937
+ rootUlTemplate: this._createUlTemplate(true)
938
+ };
939
+ }
883
940
  _createTitleSpanTemplate(isFolder) {
884
941
  let titleSpan = document.createElement("span");
885
942
  let classes = `${this._classNames.title} ${this._classNames.common}`;
@@ -948,11 +1005,8 @@ var TreeElement = (function () {
948
1005
  // is false. Otherwise the name would contain everything in the row.
949
1006
  li.setAttribute("aria-label", titleSpan.textContent);
950
1007
  }
951
- _getButtonClasses(isOpen) {
1008
+ _getButtonClasses() {
952
1009
  let classes = [this._classNames.toggler, this._classNames.common];
953
- if (!isOpen) {
954
- classes.push(this._classNames.closed);
955
- }
956
1010
  if (this._buttonLeft) {
957
1011
  classes.push(this._classNames.togglerLeft);
958
1012
  } else {
@@ -967,6 +1021,14 @@ var TreeElement = (function () {
967
1021
  _mustRenderChildren(node) {
968
1022
  return node.hasChildren() && node.is_open === true;
969
1023
  }
1024
+ _updateTemplates() {
1025
+ let templates = this._createTemplates();
1026
+ this._closedFolderTemplate = templates.closedFolderTemplate;
1027
+ this._groupUlTemplate = templates.groupUlTemplate;
1028
+ this._nodeTemplate = templates.nodeTemplate;
1029
+ this._openedFolderTemplate = templates.openedFolderTemplate;
1030
+ this._rootUlTemplate = templates.rootUlTemplate;
1031
+ }
970
1032
  }
971
1033
 
972
1034
  /* The keys move from the selected node and select the node they move to.
@@ -1017,6 +1079,24 @@ var TreeElement = (function () {
1017
1079
  return this._goToNode(this._getCurrentNode()?.getPreviousVisibleNode());
1018
1080
  }
1019
1081
 
1082
+ // Change keyboardSupport after construction
1083
+ _setKeyboardSupport(keyboardSupport) {
1084
+ if (keyboardSupport === this._keyboardSupport) {
1085
+ return;
1086
+ }
1087
+ this._keyboardSupport = keyboardSupport;
1088
+ if (keyboardSupport) {
1089
+ document.addEventListener("keydown", this._handleKeyDown);
1090
+ } else {
1091
+ document.removeEventListener("keydown", this._handleKeyDown);
1092
+ }
1093
+ }
1094
+
1095
+ // Change multiSelect after construction
1096
+ _setMultiSelect(multiSelect) {
1097
+ this._multiSelect = multiSelect;
1098
+ }
1099
+
1020
1100
  // Activate the node, for example to open its edit page.
1021
1101
  _activate(node, originalEvent) {
1022
1102
  this._triggerEvent("tree.activate", {
@@ -1281,19 +1361,19 @@ var TreeElement = (function () {
1281
1361
  return;
1282
1362
  }
1283
1363
  let div = e.target.closest(`ul.${this._classNames.tree} .${this._classNames.element}`);
1284
- if (div) {
1285
- let node = this._getNode(div);
1286
- if (node) {
1287
- e.preventDefault();
1288
- e.stopPropagation();
1289
- this._triggerEvent("tree.contextmenu", {
1290
- node,
1291
- originalEvent: e
1292
- });
1293
- return false;
1294
- }
1364
+ if (!div) {
1365
+ return;
1295
1366
  }
1296
- return null;
1367
+ let node = this._getNode(div);
1368
+ if (!node) {
1369
+ return;
1370
+ }
1371
+ e.preventDefault();
1372
+ e.stopPropagation();
1373
+ this._triggerEvent("tree.contextmenu", {
1374
+ node,
1375
+ originalEvent: e
1376
+ });
1297
1377
  };
1298
1378
  _handleDblclick = e => {
1299
1379
  if (!e.target) {
@@ -1453,14 +1533,14 @@ var TreeElement = (function () {
1453
1533
  *
1454
1534
  * @groupDescription Searching
1455
1535
  * These work on any node, and search that node's subtree. Called on the root
1456
- * node, they search the whole tree — which is what the tree's own methods of
1536
+ * node, they search the whole tree—which is what the tree's own methods of
1457
1537
  * the same name do.
1458
1538
  *
1459
1539
  * @groupDescription Changing the tree
1460
1540
  * ::: warning
1461
1541
  * These methods change the data without re-rendering. The tree's
1462
- * [methods](/reference/methods) — `appendNode`, `removeNode`, `moveNode` and
1463
- * friends — do the same and refresh the display, so prefer those. If you do
1542
+ * [methods](/reference/methods)—`appendNode`, `removeNode`, `moveNode` and
1543
+ * friends—do the same and refresh the display, so prefer those. If you do
1464
1544
  * use these, call `tree.refresh()` afterwards.
1465
1545
  * :::
1466
1546
  */
@@ -1509,7 +1589,7 @@ var TreeElement = (function () {
1509
1589
  */
1510
1590
  nodeClass;
1511
1591
  /**
1512
- The parent; `null` for the root node.
1592
+ The parent, or `null` for the root node.
1513
1593
  */
1514
1594
  parent;
1515
1595
  /**
@@ -1601,7 +1681,9 @@ var TreeElement = (function () {
1601
1681
  }
1602
1682
 
1603
1683
  /**
1604
- * Inserts a new parent between this node and its current parent.
1684
+ * Inserts a new parent between this node and its current parent. Its
1685
+ * siblings move along: the new node takes all children of the current
1686
+ * parent, and becomes its only child.
1605
1687
  *
1606
1688
  * @returns The new node, or `null` when the node has no parent.
1607
1689
  * @group Changing the tree
@@ -1671,7 +1753,7 @@ var TreeElement = (function () {
1671
1753
  * Returns the subtree as plain data, ready for `JSON.stringify`.
1672
1754
  * Internal properties (`parent`, `children`, `element`, `tree`,
1673
1755
  * `idMapping`, `nodeClass`, `load_on_demand`, `isEmptyFolder`) are left
1674
- * out; your own properties are kept.
1756
+ * out. Your own properties are kept.
1675
1757
  *
1676
1758
  * ```js
1677
1759
  * tree.getTree().getData();
@@ -1763,7 +1845,7 @@ var TreeElement = (function () {
1763
1845
  }
1764
1846
 
1765
1847
  /**
1766
- * Like `getNextNode`, but skipping nodes inside closed folders — this is
1848
+ * Like `getNextNode`, but skipping nodes inside closed folders. This is
1767
1849
  * what the arrow keys use.
1768
1850
  *
1769
1851
  * @group Moving around the tree
@@ -1847,7 +1929,7 @@ var TreeElement = (function () {
1847
1929
  }
1848
1930
 
1849
1931
  /**
1850
- * Returns the parent: `null` for a top-level node — the root node is not
1932
+ * Returns the parent, or `null` for a top-level node: the root node is not
1851
1933
  * returned.
1852
1934
  *
1853
1935
  * @group Moving around the tree
@@ -1895,8 +1977,8 @@ var TreeElement = (function () {
1895
1977
  }
1896
1978
 
1897
1979
  /**
1898
- * Like `getPreviousNode`, but skipping nodes inside closed folders —
1899
- * this is what the arrow keys use.
1980
+ * Like `getPreviousNode`, but skipping nodes inside closed folders.
1981
+ * This is what the arrow keys use.
1900
1982
  *
1901
1983
  * @group Moving around the tree
1902
1984
  */
@@ -2372,11 +2454,10 @@ var TreeElement = (function () {
2372
2454
  class FolderElement extends NodeElement {
2373
2455
  constructor({
2374
2456
  _classNames: classNames,
2375
- _closedIconElement: closedIconElement,
2376
2457
  _getScrollLeft: getScrollLeft,
2458
+ _icons: icons,
2377
2459
  _multiSelect: multiSelect,
2378
2460
  _node: node,
2379
- _openedIconElement: openedIconElement,
2380
2461
  _renderChildren: renderChildren,
2381
2462
  _tabIndex: tabIndex,
2382
2463
  _treeElement: treeElement,
@@ -2390,8 +2471,7 @@ var TreeElement = (function () {
2390
2471
  _tabIndex: tabIndex,
2391
2472
  _treeElement: treeElement
2392
2473
  });
2393
- this._closedIconElement = closedIconElement;
2394
- this._openedIconElement = openedIconElement;
2474
+ this._icons = icons;
2395
2475
  this._renderChildren = renderChildren;
2396
2476
  this._triggerEvent = triggerEvent;
2397
2477
  }
@@ -2400,30 +2480,11 @@ var TreeElement = (function () {
2400
2480
  return;
2401
2481
  }
2402
2482
  this._node.is_open = false;
2403
- if (!this._isRendered()) {
2404
- this._triggerEvent("tree.close", {
2405
- node: this._node
2406
- });
2407
- return;
2408
- }
2409
- let button = this._getButton();
2410
- button.classList.add(this._classNames.closed);
2411
- button.replaceChildren();
2412
- let closedIconElement = this._closedIconElement;
2413
- if (closedIconElement) {
2414
- let icon = closedIconElement.cloneNode(true);
2415
- button.append(icon);
2416
- }
2417
- let ul = this._getUl();
2418
- if (ul) {
2419
- if (slide) {
2420
- await slideUp(ul, animationSpeed);
2421
- } else {
2422
- ul.style.display = "none";
2423
- }
2483
+ if (this._isRendered()) {
2484
+ this._setButtonState(false);
2485
+ await this._setUlState(this._getUl(), false, slide, animationSpeed);
2486
+ this._setElementState(false);
2424
2487
  }
2425
- this._element.classList.add(this._classNames.closed);
2426
- this._element.setAttribute("aria-expanded", "false");
2427
2488
  this._triggerEvent("tree.close", {
2428
2489
  node: this._node
2429
2490
  });
@@ -2433,33 +2494,17 @@ var TreeElement = (function () {
2433
2494
  return;
2434
2495
  }
2435
2496
  this._node.is_open = true;
2436
- if (!this._isRendered()) {
2437
- // The folder is rendered open when its parent is opened
2438
- this._triggerEvent("tree.open", {
2439
- node: this._node
2440
- });
2441
- return;
2442
- }
2443
- let button = this._getButton();
2444
- button.classList.remove(this._classNames.closed);
2445
- button.replaceChildren();
2446
- let openedIconElement = this._openedIconElement;
2447
- if (openedIconElement) {
2448
- let icon = openedIconElement.cloneNode(true);
2449
- button.append(icon);
2450
- }
2451
2497
 
2452
- // The children are rendered the first time the folder is opened
2453
- let ul = this._getUl() ?? this._renderChildren(this._node);
2454
- if (ul) {
2455
- if (slide) {
2456
- await slideDown(ul, animationSpeed);
2457
- } else {
2458
- ul.style.display = "block";
2459
- }
2498
+ // A folder that is not rendered is rendered open when its parent is
2499
+ // opened
2500
+ if (this._isRendered()) {
2501
+ this._setButtonState(true);
2502
+
2503
+ // The children are rendered the first time the folder is opened
2504
+ let ul = this._getUl() ?? this._renderChildren(this._node);
2505
+ await this._setUlState(ul, true, slide, animationSpeed);
2506
+ this._setElementState(true);
2460
2507
  }
2461
- this._element.classList.remove(this._classNames.closed);
2462
- this._element.setAttribute("aria-expanded", "true");
2463
2508
  this._triggerEvent("tree.open", {
2464
2509
  node: this._node
2465
2510
  });
@@ -2470,8 +2515,34 @@ var TreeElement = (function () {
2470
2515
  _getButton() {
2471
2516
  return this._element.querySelector(`:scope > .${this._classNames.element} > .${this._classNames.toggler}`);
2472
2517
  }
2518
+ _setButtonState(isOpen) {
2519
+ setTogglerOpenState(this._getButton(), isOpen, this._classNames, this._icons);
2520
+ }
2521
+ _setElementState(isOpen) {
2522
+ setFolderLiOpenState(this._element, isOpen, this._classNames);
2523
+ }
2524
+ async _setUlState(ul, isOpen, slide, animationSpeed) {
2525
+ if (!ul) {
2526
+ return;
2527
+ }
2528
+ if (slide) {
2529
+ await (isOpen ? slideDown : slideUp)(ul, animationSpeed);
2530
+ } else {
2531
+ ul.style.display = isOpen ? "block" : "none";
2532
+ }
2533
+ }
2473
2534
  }
2474
2535
 
2536
+ /* The options that setOption can change. The others are read once, in the
2537
+ * constructor, so changing them later would have no effect or leave the tree
2538
+ * in an inconsistent state.
2539
+ */
2540
+ let changeableOptions = ["animationSpeed", "dataUrl", "dragAndDrop", "keyboardSupport", "multiSelect", "onCanSelectNode", "selectable", "slide", "startDndDelay", "tabIndex"];
2541
+
2542
+ /**
2543
+ * The options that `setOption` can change.
2544
+ */
2545
+
2475
2546
  // Url of a request. A relative url is resolved against the page, like a link
2476
2547
  // or `fetch` would, so it also works when the page is in a subdirectory.
2477
2548
 
@@ -2512,9 +2583,7 @@ var TreeElement = (function () {
2512
2583
  this._saveStateOption = saveState;
2513
2584
  }
2514
2585
  _getNodeIdToBeSelected() {
2515
- if (!this._saveStateOption) {
2516
- return null;
2517
- }
2586
+ // Without the saveState option there is no state
2518
2587
  let state = this._getStateFromStorage();
2519
2588
  return state?.selected_node ? state.selected_node[0] ?? null : null;
2520
2589
  }
@@ -2643,17 +2712,19 @@ var TreeElement = (function () {
2643
2712
  this._removeFromSelection(node);
2644
2713
  }
2645
2714
  }
2715
+
2716
+ // result: is a node selected?
2646
2717
  _selectInitialNodes(nodeIds) {
2647
- let selectCount = 0;
2718
+ let isNodeSelected = false;
2648
2719
  for (let nodeId of nodeIds) {
2649
2720
  let node = this._getNodeById(nodeId);
2650
2721
  if (!node) {
2651
2722
  continue;
2652
2723
  }
2653
- selectCount += 1;
2654
2724
  this._addToSelection(node);
2725
+ isNodeSelected = true;
2655
2726
  }
2656
- return selectCount !== 0;
2727
+ return isNodeSelected;
2657
2728
  }
2658
2729
  }
2659
2730
 
@@ -3006,6 +3077,11 @@ var TreeElement = (function () {
3006
3077
  for (let otherNode of otherSelectedNodes) {
3007
3078
  this._deselectNode(otherNode);
3008
3079
  }
3080
+ for (let otherNode of otherSelectedNodes) {
3081
+ this._triggerEvent("tree.deselect", {
3082
+ node: otherNode
3083
+ });
3084
+ }
3009
3085
  } else if (selectOptions.mustToggle) {
3010
3086
  this._deselectNode(node);
3011
3087
  this._triggerEvent("tree.deselect", {
@@ -3013,13 +3089,20 @@ var TreeElement = (function () {
3013
3089
  });
3014
3090
  }
3015
3091
  } else {
3016
- let deselectedNode = otherSelectedNodes[0] ?? null;
3092
+ // tree.select reports the first deselected node; the others, from a
3093
+ // multiple selection, get a tree.deselect.
3094
+ let [deselectedNode = null, ...alsoDeselectedNodes] = otherSelectedNodes;
3017
3095
  for (let otherNode of otherSelectedNodes) {
3018
3096
  this._deselectNode(otherNode);
3019
3097
  }
3020
3098
  this._addToSelection(node);
3021
3099
  this._openParents(node);
3022
3100
  this._getNodeElementForNode(node)._select(selectOptions.mustSetFocus);
3101
+ for (let otherNode of alsoDeselectedNodes) {
3102
+ this._triggerEvent("tree.deselect", {
3103
+ node: otherNode
3104
+ });
3105
+ }
3023
3106
  this._triggerEvent("tree.select", {
3024
3107
  deselectedNode,
3025
3108
  node
@@ -3157,7 +3240,7 @@ var TreeElement = (function () {
3157
3240
  };
3158
3241
 
3159
3242
  // Generated by config/generateVersion.mjs. Do not edit.
3160
- let version = "0.4.0";
3243
+ let version = "0.4.1";
3161
3244
 
3162
3245
  // The types that appear in the public api. Consumers cannot import them from
3163
3246
  // the submodules directly, because those are not exposed in package.json.
@@ -3224,7 +3307,6 @@ var TreeElement = (function () {
3224
3307
  rtl,
3225
3308
  saveState: saveStateOption,
3226
3309
  showEmptyFolder,
3227
- slide,
3228
3310
  tabIndex
3229
3311
  } = this._options;
3230
3312
  let classNames = this._classNames;
@@ -3313,7 +3395,6 @@ var TreeElement = (function () {
3313
3395
  _openFolderDelay: openFolderDelay,
3314
3396
  _openNode: openNode,
3315
3397
  _refreshElements: refreshElements,
3316
- _slide: slide,
3317
3398
  _treeElement: treeElement,
3318
3399
  _triggerEvent: triggerEvent
3319
3400
  });
@@ -3412,8 +3493,9 @@ var TreeElement = (function () {
3412
3493
  }
3413
3494
 
3414
3495
  /**
3415
- * Inserts a new node between a node and its parent, taking the node as its
3416
- * child.
3496
+ * Inserts a new node between a node and its parent. The siblings of the node
3497
+ * move along: the new node takes all children of the parent, and becomes its
3498
+ * only child.
3417
3499
  *
3418
3500
  * @returns The new node, or `null` when the node has no parent.
3419
3501
  * @group Changing the tree
@@ -3583,7 +3665,7 @@ var TreeElement = (function () {
3583
3665
  }
3584
3666
 
3585
3667
  /**
3586
- * Returns the current state — `open_nodes` and `selected_node` — whether or
3668
+ * Returns the current state—`open_nodes` and `selected_node`—whether or
3587
3669
  * not `saveState` is enabled.
3588
3670
  *
3589
3671
  * @group State
@@ -3602,7 +3684,7 @@ var TreeElement = (function () {
3602
3684
  }
3603
3685
 
3604
3686
  /**
3605
- * Returns the root node. It is not rendered; its `children` are the
3687
+ * Returns the root node. It is not rendered, and its `children` are the
3606
3688
  * top-level nodes. Also available as the `tree` property.
3607
3689
  *
3608
3690
  * @group Finding nodes
@@ -3713,7 +3795,7 @@ var TreeElement = (function () {
3713
3795
  }
3714
3796
 
3715
3797
  /**
3716
- * Opens a folder, and the folders above it. A node marked `load_on_demand`
3798
+ * Opens a folder, and the folders that contain it. A node marked `load_on_demand`
3717
3799
  * is fetched first, so await the promise when you need to know it is really
3718
3800
  * open.
3719
3801
  *
@@ -3729,7 +3811,7 @@ var TreeElement = (function () {
3729
3811
  async openNode(node, slide) {
3730
3812
  let mustSlide = slide ?? this._options.slide;
3731
3813
  let doOpenNode = async (openedNode, slideOption) => {
3732
- if (!node.children.length) {
3814
+ if (!openedNode.children.length) {
3733
3815
  return;
3734
3816
  }
3735
3817
  let folderElement = this._createFolderElement(openedNode);
@@ -3848,13 +3930,43 @@ var TreeElement = (function () {
3848
3930
  }
3849
3931
 
3850
3932
  /**
3851
- * Changes an option after construction. See
3852
- * [Options](/reference/options#changing-an-option-later) for the caveats.
3933
+ * Changes an option after construction. Only some options can be changed:
3934
+ * the others throw an error. See
3935
+ * [Options](/reference/options#changing-an-option-later).
3853
3936
  *
3854
3937
  * @group Other
3855
3938
  */
3856
3939
  setOption(option, value) {
3940
+ if (!changeableOptions.includes(option)) {
3941
+ throw new Error(`The option ${option} can only be set in the constructor`);
3942
+ }
3857
3943
  this._options[option] = value;
3944
+ switch (option) {
3945
+ case "dragAndDrop":
3946
+ {
3947
+ this._renderer._setDragAndDrop(Boolean(value));
3948
+ break;
3949
+ }
3950
+ case "keyboardSupport":
3951
+ {
3952
+ this._keyHandler._setKeyboardSupport(Boolean(value));
3953
+ break;
3954
+ }
3955
+ case "multiSelect":
3956
+ {
3957
+ this._setMultiSelect(Boolean(value));
3958
+ break;
3959
+ }
3960
+ case "tabIndex":
3961
+ {
3962
+ this._options.tabIndex = value ?? undefined;
3963
+ this._renderer._setTabIndex(this._options.tabIndex);
3964
+ this._fallbackTabStop = null;
3965
+ this._updateTabStop();
3966
+ break;
3967
+ }
3968
+ // No default
3969
+ }
3858
3970
  }
3859
3971
 
3860
3972
  /**
@@ -3887,8 +3999,8 @@ var TreeElement = (function () {
3887
3999
  }
3888
4000
 
3889
4001
  /**
3890
- * Returns the tree as a json string, including changes made through the
3891
- * api.
4002
+ * Returns the tree as a JSON string, including changes made through the
4003
+ * API.
3892
4004
  *
3893
4005
  * @group Other
3894
4006
  */
@@ -3901,7 +4013,7 @@ var TreeElement = (function () {
3901
4013
  * name.
3902
4014
  *
3903
4015
  * Changing the `id` re-indexes the node. `children` and `parent` are
3904
- * ignored — use `loadData` or `moveNode` for those.
4016
+ * ignored—use `loadData` or `moveNode` for those.
3905
4017
  *
3906
4018
  * @example
3907
4019
  * ```js
@@ -3952,9 +4064,7 @@ var TreeElement = (function () {
3952
4064
  }
3953
4065
  _createFolderElement(node) {
3954
4066
  let classNames = this._classNames;
3955
- let closedIconElement = this._renderer._closedIconElement;
3956
4067
  let getScrollLeft = this._scrollHandler._getScrollLeft.bind(this._scrollHandler);
3957
- let openedIconElement = this._renderer._openedIconElement;
3958
4068
  let tabIndex = this._options.tabIndex;
3959
4069
  let treeElement = this._htmlElement;
3960
4070
  let triggerEvent = this._triggerEvent.bind(this);
@@ -3969,11 +4079,10 @@ var TreeElement = (function () {
3969
4079
  };
3970
4080
  return new FolderElement({
3971
4081
  _classNames: classNames,
3972
- _closedIconElement: closedIconElement,
3973
4082
  _getScrollLeft: getScrollLeft,
4083
+ _icons: this._renderer._icons,
3974
4084
  _multiSelect: this._options.multiSelect,
3975
4085
  _node: node,
3976
- _openedIconElement: openedIconElement,
3977
4086
  _renderChildren: renderChildren,
3978
4087
  _tabIndex: tabIndex,
3979
4088
  _treeElement: treeElement,
@@ -4069,14 +4178,8 @@ var TreeElement = (function () {
4069
4178
  _getFocusedNode() {
4070
4179
  let activeElement = document.activeElement;
4071
4180
 
4072
- /* istanbul ignore if */
4073
- if (!activeElement) {
4074
- return null;
4075
- }
4076
-
4077
- // The keyboard must still work for input elements.
4078
- let tagName = activeElement.tagName;
4079
- if (tagName !== "A" && tagName !== "LI" && tagName !== "SPAN") {
4181
+ // An active element must exist and the keyboard must still work for input elements.
4182
+ if (!activeElement || !["A", "LI", "SPAN"].includes(activeElement.tagName)) {
4080
4183
  return null;
4081
4184
  }
4082
4185
  let node = this.getNode(activeElement);
@@ -4125,11 +4228,17 @@ var TreeElement = (function () {
4125
4228
  this._isInitialized = true;
4126
4229
  this._triggerEvent("tree.init");
4127
4230
  };
4231
+
4232
+ // Before the tree is replaced: then the focused node is no longer in it
4233
+ let focusedNode = this._getFocusedNode();
4128
4234
  this._tree = new this._options.nodeClass(null, true, this._options.nodeClass);
4129
4235
  this._selectNodeHandler._clear();
4130
4236
  this._tree.loadFromData(data);
4131
4237
  let mustLoadOnDemand = this._setInitialState();
4132
4238
  this._refreshElements(null);
4239
+ if (focusedNode) {
4240
+ this._restoreFocus(focusedNode);
4241
+ }
4133
4242
  if (mustLoadOnDemand) {
4134
4243
  // Load data on demand and then init the tree
4135
4244
  void this._setInitialStateOnDemand().then(doInit);
@@ -4137,15 +4246,6 @@ var TreeElement = (function () {
4137
4246
  doInit();
4138
4247
  }
4139
4248
  }
4140
-
4141
- // Does an HTML element of the tree have the focus?
4142
- _isFocusOnTree() {
4143
- return this._getFocusedNode() !== null;
4144
- }
4145
- _isSelectedNodeInSubtree(subtree) {
4146
- let selectedNode = this.getSelectedNode();
4147
- return selectedNode ? subtree === selectedNode || subtree.isParentOf(selectedNode) : false;
4148
- }
4149
4249
  async _loadFolderOnDemand(node, slide) {
4150
4250
  node.is_loading = true;
4151
4251
  await this.loadDataFromUrl(undefined, node);
@@ -4186,10 +4286,10 @@ var TreeElement = (function () {
4186
4286
  _mouseStop(positionInfo) {
4187
4287
  /* istanbul ignore if */
4188
4288
  if (!this._options.dragAndDrop) {
4189
- return false;
4289
+ return;
4190
4290
  }
4191
4291
  this._scrollHandler._stopScrolling();
4192
- return this._dndHandler._mouseStop(positionInfo);
4292
+ this._dndHandler._mouseStop(positionInfo);
4193
4293
  }
4194
4294
  _openParents(node) {
4195
4295
  let parent = node.parent;
@@ -4203,25 +4303,32 @@ var TreeElement = (function () {
4203
4303
  from_node: redraw this subtree
4204
4304
  */
4205
4305
  _refreshElements(fromNode) {
4206
- let mustSetFocus = this._isFocusOnTree();
4207
- let mustSelect = fromNode ? this._isSelectedNodeInSubtree(fromNode) : false;
4306
+ let focusedNode = this._getFocusedNode();
4307
+
4308
+ // The renderer marks the selected nodes, so only the focus needs restoring
4208
4309
  this._renderer._render(fromNode);
4209
- if (mustSelect) {
4210
- this._selectCurrentNode(mustSetFocus);
4310
+ if (focusedNode) {
4311
+ this._restoreFocus(focusedNode);
4211
4312
  }
4212
4313
  this._updateTabStop();
4213
4314
  this._triggerEvent("tree.refresh");
4214
4315
  }
4215
- _saveState() {
4216
- this._saveStateHandler._saveState();
4217
- }
4218
- _selectCurrentNode(mustSetFocus) {
4219
- let node = this.getSelectedNode();
4220
- if (!node) {
4316
+
4317
+ // Rendering replaces the elements of the nodes, so a focused element can be
4318
+ // gone. Then move the focus to the new element of the same node. After
4319
+ // loadData the nodes are new objects, so look the node up by its id.
4320
+ _restoreFocus(focusedNode) {
4321
+ if (this._htmlElement.contains(document.activeElement)) {
4221
4322
  return;
4222
4323
  }
4223
- let nodeElement = this._getNodeElementForNode(node);
4224
- nodeElement._select(mustSetFocus);
4324
+ let node = (focusedNode.id == null ? null : this.getNodeById(focusedNode.id)) ?? focusedNode;
4325
+ let element = node.element;
4326
+ if (element && this._htmlElement.contains(element)) {
4327
+ element.focus();
4328
+ }
4329
+ }
4330
+ _saveState() {
4331
+ this._saveStateHandler._saveState();
4225
4332
  }
4226
4333
 
4227
4334
  // Set initial state, either by restoring the state or auto-opening nodes
@@ -4276,6 +4383,25 @@ var TreeElement = (function () {
4276
4383
  }
4277
4384
  }
4278
4385
 
4386
+ /* Change multiSelect after construction. The attributes of the nodes
4387
+ * depend on it, so the tree is rendered again. The selection is kept: a
4388
+ * tree without multiSelect can also have several selected nodes, see
4389
+ * addToSelection.
4390
+ */
4391
+ _setMultiSelect(multiSelect) {
4392
+ let focusedNode = this._getFocusedNode();
4393
+ this._keyHandler._setMultiSelect(multiSelect);
4394
+ this._renderer._setMultiSelect(multiSelect);
4395
+ if (multiSelect) {
4396
+ this._htmlElement.addEventListener("focusin", this._handleFocusIn);
4397
+ } else {
4398
+ this._htmlElement.removeEventListener("focusin", this._handleFocusIn);
4399
+ }
4400
+ this._activeNode = multiSelect ? focusedNode : null;
4401
+ this._fallbackTabStop = null;
4402
+ this._refreshElements(null);
4403
+ }
4404
+
4279
4405
  // Set this HTML element to this node in the node map.
4280
4406
  _setNodeElement(element, node) {
4281
4407
  this._nodeMap.set(element, node);