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
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "tree-element",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
+ "packageManager": "pnpm@12.16.1",
4
5
  "description": "Tree widget in plain javascript",
5
6
  "keywords": [
6
7
  "tree"
@@ -43,6 +44,7 @@
43
44
  "vitest": "pnpm build-debug && vitest run --coverage --config config/vitest.config.mjs",
44
45
  "vitest-watch": "vitest --config config/vitest.config.mjs",
45
46
  "lint": "eslint css src playwright test",
47
+ "lint-docs": "pnpm docs-api && vale --glob='!docs/.vitepress/**' README.md CHANGELOG.md docs",
46
48
  "production": "./config/production",
47
49
  "devserver": "SERVE=true rollup --config config/rollup.config.mjs --watch",
48
50
  "docs-api": "typedoc && node docs/scripts/fixApiLinks.mjs",
@@ -58,66 +60,70 @@
58
60
  "release": "node config/release.mjs",
59
61
  "playwright": "pnpm build-with-coverage && playwright test --config config/playwright.config.js",
60
62
  "test": "pnpm vitest && pnpm playwright",
61
- "docs-check": "kiira check"
63
+ "docs-check": "kiira check",
64
+ "mutation": "stryker run config/stryker.config.mjs"
62
65
  },
63
66
  "devDependencies": {
64
67
  "@axe-core/playwright": "^4.14.0",
65
- "@babel/cli": "^8.0.6",
66
- "@babel/core": "^8.0.7",
68
+ "@babel/cli": "^8.1.0",
69
+ "@babel/core": "^8.1.2",
67
70
  "@babel/preset-env": "^8.0.7",
68
71
  "@babel/preset-typescript": "^8.0.7",
69
- "@cspell/eslint-plugin": "^10.4.0",
72
+ "@cspell/eslint-plugin": "^10.4.1",
70
73
  "@eslint/css": "^2.0.1",
71
74
  "@eslint/js": "^10.0.1",
72
- "@playwright/test": "^1.64.1",
75
+ "@playwright/test": "^1.65.0",
73
76
  "@rollup/plugin-babel": "^7.1.0",
74
77
  "@rollup/plugin-node-resolve": "^16.0.3",
75
- "@rollup/plugin-terser": "^1.0.0",
78
+ "@rollup/plugin-terser": "^1.0.2",
79
+ "@stryker-mutator/core": "^10.1.1",
80
+ "@stryker-mutator/typescript-checker": "^10.1.1",
81
+ "@stryker-mutator/vitest-runner": "^10.1.1",
76
82
  "@testing-library/dom": "^10.4.2",
77
83
  "@testing-library/jest-dom": "^7.0.1",
78
- "@testing-library/user-event": "^14.6.8",
84
+ "@testing-library/user-event": "^14.6.10",
79
85
  "@types/debug": "^4.1.13",
80
- "@types/node": "^26.6.5",
81
- "@vitest/coverage-istanbul": "^5.0.5",
82
- "@vitest/eslint-plugin": "^1.6.27",
86
+ "@types/node": "^26.6.6",
87
+ "@vitest/coverage-istanbul": "^5.1.0",
88
+ "@vitest/eslint-plugin": "^1.6.28",
83
89
  "axe-core": "^4.14.0",
84
90
  "babel-plugin-istanbul": "^8.0.0",
85
91
  "babel-plugin-prefix-private-members": "^0.3.1",
86
- "eslint": "^10.13.0",
92
+ "eslint": "^10.14.0",
87
93
  "eslint-import-resolver-typescript": "^4.4.5",
88
94
  "eslint-plugin-compat": "^7.0.2",
89
- "eslint-plugin-import-x": "^4.17.1",
90
- "eslint-plugin-jest-dom": "^5.10.1",
95
+ "eslint-plugin-import-x": "^4.18.1",
96
+ "eslint-plugin-jest-dom": "^5.10.2",
91
97
  "eslint-plugin-jest-extended": "github:mbraak/eslint-plugin-jest-extended#modernize",
92
98
  "eslint-plugin-no-unsanitized": "^4.1.5",
93
- "eslint-plugin-perfectionist": "^5.12.1",
94
- "eslint-plugin-playwright": "^2.12.1",
99
+ "eslint-plugin-perfectionist": "^5.13.0",
100
+ "eslint-plugin-playwright": "^2.12.2",
95
101
  "eslint-plugin-testing-library": "^7.16.2",
96
102
  "eslint-plugin-tsdoc": "^0.5.4",
97
- "eslint-plugin-unicorn": "^78.0.0",
103
+ "eslint-plugin-unicorn": "^79.0.0",
98
104
  "jest-extended": "^7.0.0",
99
- "jsdom": "^30.1.3",
105
+ "jsdom": "^31.0.1",
100
106
  "jsdom-testing-mocks": "^1.16.0",
101
107
  "jsonfile": "^6.2.1",
102
- "kiira": "^0.7.0",
103
- "kiira-core": "^0.7.0",
108
+ "kiira": "^0.7.1",
109
+ "kiira-core": "^0.7.1",
104
110
  "lodash": "^4.18.1",
105
- "msw": "^3.0.3",
106
- "postcss": "^8.5.30",
111
+ "msw": "^3.0.5",
112
+ "postcss": "^8.5.32",
107
113
  "postcss-cli": "^12.0.0",
108
114
  "postcss-import": "^17.0.1",
109
115
  "postcss-load-config": "^6.0.1",
110
- "postcss-nested": "^9.0.0",
111
- "prettier": "^3.9.10",
112
- "rollup": "^4.64.7",
116
+ "postcss-nested": "^9.0.1",
117
+ "prettier": "^3.10.1",
118
+ "rollup": "^4.68.2",
113
119
  "rollup-plugin-serve": "^3.0.0",
114
120
  "rollup-plugin-tsconfig-paths": "^1.5.2",
115
- "tslib": "^2.8.1",
121
+ "tslib": "^2.9.1",
116
122
  "typedoc": "^0.28.20",
117
- "typedoc-plugin-markdown": "^4.14.0",
123
+ "typedoc-plugin-markdown": "^4.14.1",
118
124
  "typescript": "^6.0.3",
119
- "typescript-eslint": "^8.71.2",
125
+ "typescript-eslint": "^8.72.2",
120
126
  "vitepress": "^1.6.4",
121
- "vitest": "^5.0.5"
127
+ "vitest": "^5.1.0"
122
128
  }
123
129
  }
package/src/dataLoader.ts CHANGED
@@ -80,8 +80,10 @@ export default class DataLoader {
80
80
  const signal = this.abortController.signal;
81
81
  url.setSearchParam("_", Date.now().toString());
82
82
 
83
+ // Accept, not Content-Type: the request has no body. A cross-origin request
84
+ // with only an Accept header also needs no CORS preflight.
83
85
  return fetch(url.value(), {
84
- headers: { "Content-Type": "application/json" },
86
+ headers: { Accept: "application/json" },
85
87
  signal,
86
88
  })
87
89
  .then(handleResponse)
@@ -31,8 +31,9 @@ export const generateHitPositions = (
31
31
  };
32
32
 
33
33
  const handleAfterOpenFolder = (node: Node, nextNode: Node | null) => {
34
- if (node === currentNode || nextNode === currentNode) {
35
- // Cannot move before or after current item
34
+ // Not called for the current node: handleOpenFolder stops iterating there
35
+ if (nextNode === currentNode) {
36
+ // Dropping the current node after this folder would leave it where it is
36
37
  addHitPosition(node, null, lastTop);
37
38
  } else {
38
39
  addHitPosition(node, "after", lastTop);
@@ -44,7 +44,6 @@ interface DragAndDropHandlerParams {
44
44
  openFolderDelay: false | number;
45
45
  openNode: OpenNode;
46
46
  refreshElements: RefreshElements;
47
- slide: boolean;
48
47
  treeElement: HTMLElement;
49
48
  triggerEvent: TriggerEvent;
50
49
  }
@@ -75,7 +74,6 @@ export class DragAndDropHandler {
75
74
  private openNode: OpenNode;
76
75
  private previousGhost: DropHint | null;
77
76
  private refreshElements: RefreshElements;
78
- private slide: boolean;
79
77
  private treeElement: HTMLElement;
80
78
  private triggerEvent: TriggerEvent;
81
79
 
@@ -94,7 +92,6 @@ export class DragAndDropHandler {
94
92
  openFolderDelay,
95
93
  openNode,
96
94
  refreshElements,
97
- slide,
98
95
  treeElement,
99
96
  triggerEvent,
100
97
  }: DragAndDropHandlerParams) {
@@ -112,7 +109,6 @@ export class DragAndDropHandler {
112
109
  this.openFolderDelay = openFolderDelay;
113
110
  this.openNode = openNode;
114
111
  this.refreshElements = refreshElements;
115
- this.slide = slide;
116
112
  this.treeElement = treeElement;
117
113
  this.triggerEvent = triggerEvent;
118
114
 
@@ -157,10 +153,6 @@ export class DragAndDropHandler {
157
153
  const area = this.findHoveredArea(positionInfo.pageX, positionInfo.pageY);
158
154
 
159
155
  if (area && this.canMoveToArea(area, this.currentItem)) {
160
- if (!area.node.isFolder()) {
161
- this.stopOpenFolderTimer();
162
- }
163
-
164
156
  if (this.hoveredArea !== area) {
165
157
  this.hoveredArea = area;
166
158
 
@@ -212,7 +204,7 @@ export class DragAndDropHandler {
212
204
  return true;
213
205
  }
214
206
 
215
- public mouseStop(positionInfo: PositionInfo): boolean {
207
+ public mouseStop(positionInfo: PositionInfo): void {
216
208
  this.moveItem(positionInfo);
217
209
  this.clear();
218
210
  this.removeHover();
@@ -231,8 +223,6 @@ export class DragAndDropHandler {
231
223
  if (!this.hoveredArea && currentItem && this.onDragStop) {
232
224
  this.onDragStop(currentItem.node, positionInfo.originalEvent);
233
225
  }
234
-
235
- return false;
236
226
  }
237
227
 
238
228
  public refresh(): void {
@@ -360,11 +350,7 @@ export class DragAndDropHandler {
360
350
  }
361
351
 
362
352
  private mustCaptureElement(element: HTMLElement): boolean {
363
- const nodeName = element.nodeName;
364
-
365
- return (
366
- nodeName !== "INPUT" && nodeName !== "SELECT" && nodeName !== "TEXTAREA"
367
- );
353
+ return !["INPUT", "SELECT", "TEXTAREA"].includes(element.nodeName);
368
354
  }
369
355
 
370
356
  private mustOpenFolderTimer(area: HitArea): boolean {
@@ -389,7 +375,7 @@ export class DragAndDropHandler {
389
375
 
390
376
  private startOpenFolderTimer(folder: Node): void {
391
377
  const openFolder = (): void => {
392
- void this.openNode(folder, this.slide).then(() => {
378
+ void this.openNode(folder).then(() => {
393
379
  this.refresh();
394
380
  this.updateDropHint();
395
381
  });
@@ -1,9 +1,10 @@
1
1
  import type { ClassNames } from "./classNames";
2
+ import type { FolderIcons } from "./folderOpenState";
2
3
  import type { GetTree, IsNodeSelected } from "./methodTypes";
3
4
  import type { Node } from "./node";
4
5
  import type { IconElement, OnCreateLi } from "./options";
5
6
 
6
- import { getBoolString } from "./util";
7
+ import { setFolderLiOpenState, setTogglerOpenState } from "./folderOpenState";
7
8
 
8
9
  interface ElementsRendererParams {
9
10
  ariaLabel?: string;
@@ -37,8 +38,7 @@ interface ElementsRendererParams {
37
38
  * selected and loading classes.
38
39
  */
39
40
  export default class ElementsRenderer {
40
- public closedIconElement?: globalThis.Node;
41
- public openedIconElement?: globalThis.Node;
41
+ public icons: FolderIcons;
42
42
  private ariaLabel?: string;
43
43
  private ariaLabelledBy?: string;
44
44
  private autoEscape: boolean;
@@ -94,14 +94,17 @@ export default class ElementsRenderer {
94
94
  this.setNodeElement = setNodeElement;
95
95
  this.showEmptyFolder = showEmptyFolder;
96
96
  this.tabIndex = tabIndex;
97
- this.openedIconElement = this.createButtonElement(openedIcon);
98
- this.closedIconElement = this.createButtonElement(closedIcon);
99
-
100
- this.rootUlTemplate = this.createUlTemplate(true);
101
- this.groupUlTemplate = this.createUlTemplate(false);
102
- this.nodeTemplate = this.createNodeTemplate();
103
- this.openedFolderTemplate = this.createFolderTemplate(true);
104
- this.closedFolderTemplate = this.createFolderTemplate(false);
97
+ this.icons = {
98
+ closed: this.createButtonElement(closedIcon),
99
+ opened: this.createButtonElement(openedIcon),
100
+ };
101
+
102
+ const templates = this.createTemplates();
103
+ this.closedFolderTemplate = templates.closedFolderTemplate;
104
+ this.groupUlTemplate = templates.groupUlTemplate;
105
+ this.nodeTemplate = templates.nodeTemplate;
106
+ this.openedFolderTemplate = templates.openedFolderTemplate;
107
+ this.rootUlTemplate = templates.rootUlTemplate;
105
108
  }
106
109
 
107
110
  public render(fromNode: Node | null): void {
@@ -150,6 +153,50 @@ export default class ElementsRenderer {
150
153
  }
151
154
  }
152
155
 
156
+ /* Change dragAndDrop after construction. The rendered lists and the
157
+ * templates for new lists get or lose the dnd class.
158
+ */
159
+ public setDragAndDrop(dragAndDrop: boolean): void {
160
+ this.dragAndDrop = dragAndDrop;
161
+ this.updateTemplates();
162
+
163
+ for (const ul of this.htmlElement.querySelectorAll(
164
+ `ul.${this.classNames.common}`,
165
+ )) {
166
+ ul.classList.toggle(this.classNames.dnd, dragAndDrop);
167
+ }
168
+ }
169
+
170
+ /* Change multiSelect after construction. Only the templates are updated;
171
+ * the tree must be rendered again.
172
+ */
173
+ public setMultiSelect(multiSelect: boolean): void {
174
+ this.multiSelect = multiSelect;
175
+ this.updateTemplates();
176
+ }
177
+
178
+ /* Change tabIndex after construction. The rendered nodes and the
179
+ * templates for new nodes get the new tabindex. The tab stop is updated by
180
+ * TreeElement.updateTabStop.
181
+ */
182
+ public setTabIndex(tabIndex: number | undefined): void {
183
+ this.tabIndex = tabIndex;
184
+ this.updateTemplates();
185
+
186
+ for (const li of this.htmlElement.querySelectorAll("li[role=treeitem]")) {
187
+ if (tabIndex === undefined) {
188
+ li.removeAttribute("tabindex");
189
+ } else if (
190
+ !this.multiSelect &&
191
+ li.getAttribute("aria-selected") === "true"
192
+ ) {
193
+ li.setAttribute("tabindex", String(tabIndex));
194
+ } else {
195
+ li.setAttribute("tabindex", "-1");
196
+ }
197
+ }
198
+ }
199
+
153
200
  private attachNodeData(node: Node, li: HTMLElement): void {
154
201
  node.element = li;
155
202
  this.setNodeElement(li, node);
@@ -228,35 +275,24 @@ export default class ElementsRenderer {
228
275
  * state, so there is a template for each.
229
276
  */
230
277
  private createFolderTemplate(isOpen: boolean): HTMLLIElement {
231
- const liClasses = [this.classNames.common, this.classNames.folder];
232
-
233
- if (!isOpen) {
234
- liClasses.push(this.classNames.closed);
235
- }
278
+ const li = this.createLiTemplate(
279
+ `${this.classNames.common} ${this.classNames.folder}`,
280
+ );
281
+ setFolderLiOpenState(li, isOpen, this.classNames);
236
282
 
237
- const li = this.createLiTemplate(liClasses.join(" "));
238
283
  const div = li.firstChild as HTMLDivElement;
239
284
 
240
285
  // The toggler is hidden from screen readers: the icon is decoration, and
241
286
  // aria-expanded on the treeitem has the open state.
242
287
  const toggler = document.createElement("span");
243
- toggler.className = this.getButtonClasses(isOpen);
288
+ toggler.className = this.getButtonClasses();
244
289
  toggler.setAttribute("aria-hidden", "true");
245
-
246
- const iconElement = isOpen
247
- ? this.openedIconElement
248
- : this.closedIconElement;
249
-
250
- if (iconElement) {
251
- toggler.append(iconElement.cloneNode(true));
252
- }
290
+ setTogglerOpenState(toggler, isOpen, this.classNames, this.icons);
253
291
 
254
292
  if (this.buttonLeft) {
255
293
  div.append(toggler);
256
294
  }
257
295
 
258
- li.setAttribute("aria-expanded", getBoolString(isOpen));
259
-
260
296
  // title span
261
297
  div.append(this.createTitleSpanTemplate(true));
262
298
 
@@ -350,6 +386,16 @@ export default class ElementsRenderer {
350
386
  return li;
351
387
  }
352
388
 
389
+ private createTemplates() {
390
+ return {
391
+ closedFolderTemplate: this.createFolderTemplate(false),
392
+ groupUlTemplate: this.createUlTemplate(false),
393
+ nodeTemplate: this.createNodeTemplate(),
394
+ openedFolderTemplate: this.createFolderTemplate(true),
395
+ rootUlTemplate: this.createUlTemplate(true),
396
+ };
397
+ }
398
+
353
399
  private createTitleSpanTemplate(isFolder: boolean): HTMLSpanElement {
354
400
  const titleSpan = document.createElement("span");
355
401
 
@@ -443,13 +489,9 @@ export default class ElementsRenderer {
443
489
  li.setAttribute("aria-label", titleSpan.textContent);
444
490
  }
445
491
 
446
- private getButtonClasses(isOpen: boolean): string {
492
+ private getButtonClasses(): string {
447
493
  const classes = [this.classNames.toggler, this.classNames.common];
448
494
 
449
- if (!isOpen) {
450
- classes.push(this.classNames.closed);
451
- }
452
-
453
495
  if (this.buttonLeft) {
454
496
  classes.push(this.classNames.togglerLeft);
455
497
  } else {
@@ -465,4 +507,13 @@ export default class ElementsRenderer {
465
507
  private mustRenderChildren(node: Node): boolean {
466
508
  return node.hasChildren() && node.is_open === true;
467
509
  }
510
+
511
+ private updateTemplates(): void {
512
+ const templates = this.createTemplates();
513
+ this.closedFolderTemplate = templates.closedFolderTemplate;
514
+ this.groupUlTemplate = templates.groupUlTemplate;
515
+ this.nodeTemplate = templates.nodeTemplate;
516
+ this.openedFolderTemplate = templates.openedFolderTemplate;
517
+ this.rootUlTemplate = templates.rootUlTemplate;
518
+ }
468
519
  }
package/src/events.ts CHANGED
@@ -25,7 +25,7 @@ export interface TreeEvents {
25
25
  /**
26
26
  * Dispatched once, after the tree is rendered for the first time. With
27
27
  * `dataUrl` this is after the data has arrived, so it is the moment the
28
- * api is safe to use:
28
+ * API is safe to use:
29
29
  *
30
30
  * ```js
31
31
  * element.addEventListener("tree.init", () => {
@@ -85,7 +85,7 @@ export interface TreeEvents {
85
85
  originalEvent: MouseEvent;
86
86
  };
87
87
  /**
88
- * Dispatched on a right click on a node, unless `useContextMenu` is
88
+ * Dispatched when a node is right-clicked, unless `useContextMenu` is
89
89
  * `false`. The browser's own menu is suppressed.
90
90
  *
91
91
  * @group Mouse
@@ -96,7 +96,7 @@ export interface TreeEvents {
96
96
  };
97
97
  /**
98
98
  * Dispatched when `Enter` is pressed on the selected node, or on the
99
- * focused node when no node is selected. Use it for what a click on a
99
+ * focused node when no node is selected. Use it for what clicking a
100
100
  * link would do, like opening the edit page of the node:
101
101
  *
102
102
  * ```js
@@ -142,7 +142,9 @@ export interface TreeEvents {
142
142
  */
143
143
  "tree.select": {
144
144
  /**
145
- The node that was selected before, if any.
145
+ The node that was selected before, if any. When several nodes were
146
+ selected, this is the first, and the others get a `tree.deselect`,
147
+ dispatched before this event.
146
148
  */
147
149
  deselectedNode: Node | null;
148
150
  /**
@@ -155,13 +157,17 @@ export interface TreeEvents {
155
157
  * is clicked again or passed to `selectNode` again with
156
158
  * `mustToggle: true`. No `tree.select` is dispatched for it.
157
159
  *
160
+ * When several nodes are selected and a click or `selectNode` selects just
161
+ * one node, it is dispatched for each node that loses the selection, except
162
+ * the one in the `deselectedNode` of `tree.select`.
163
+ *
158
164
  * ```js
159
165
  * element.addEventListener("tree.deselect", (e) => {
160
166
  * console.log("deselected", e.detail.node.name);
161
167
  * });
162
168
  * ```
163
169
  *
164
- * Selecting another node does not dispatch it — that is a `tree.select`
170
+ * Selecting another node does not dispatch it—that is a `tree.select`
165
171
  * with the previous node in `deselectedNode`. It is also not dispatched
166
172
  * by `selectNode` without `mustToggle: true`, `selectNode(null)`,
167
173
  * `removeFromSelection`, `removeNode`, `loadData` and restoring a saved
@@ -198,7 +204,7 @@ export interface TreeEvents {
198
204
  };
199
205
  /**
200
206
  * Dispatched when data is set on the tree, both from the `data` option
201
- * and from a url.
207
+ * and from a URL.
202
208
  *
203
209
  * @group Loading data
204
210
  */
@@ -223,7 +229,7 @@ export interface TreeEvents {
223
229
  };
224
230
  /**
225
231
  * Dispatched when a request finishes. It is dispatched when the request
226
- * fails too; a [`tree.load_failed`](#tree-load-failed) follows it.
232
+ * fails too, and a [`tree.load_failed`](#tree-load-failed) follows it.
227
233
  *
228
234
  * @group Loading data
229
235
  */
@@ -0,0 +1,38 @@
1
+ import type { ClassNames } from "./classNames";
2
+
3
+ import { getBoolString } from "./util";
4
+
5
+ export interface FolderIcons {
6
+ closed?: globalThis.Node;
7
+ opened?: globalThis.Node;
8
+ }
9
+
10
+ /* The open state of a folder is shown on the toggler and on the <li>.
11
+ * These are used both when a folder is rendered and when it is opened or
12
+ * closed, so a rendered folder looks the same as a toggled one.
13
+ */
14
+
15
+ export const setTogglerOpenState = (
16
+ toggler: HTMLElement,
17
+ isOpen: boolean,
18
+ classNames: ClassNames,
19
+ icons: FolderIcons,
20
+ ): void => {
21
+ toggler.classList.toggle(classNames.closed, !isOpen);
22
+ toggler.replaceChildren();
23
+
24
+ const iconElement = isOpen ? icons.opened : icons.closed;
25
+
26
+ if (iconElement) {
27
+ toggler.append(iconElement.cloneNode(true));
28
+ }
29
+ };
30
+
31
+ export const setFolderLiOpenState = (
32
+ li: HTMLElement,
33
+ isOpen: boolean,
34
+ classNames: ClassNames,
35
+ ): void => {
36
+ li.classList.toggle(classNames.closed, !isOpen);
37
+ li.setAttribute("aria-expanded", getBoolString(isOpen));
38
+ };