tree-element 0.3.2 → 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 (91) hide show
  1. package/README.md +23 -20
  2. package/lib/classNames.js +4 -4
  3. package/lib/classNames.js.map +1 -1
  4. package/lib/dataLoader.d.ts +0 -1
  5. package/lib/dataLoader.d.ts.map +1 -1
  6. package/lib/dataLoader.js +10 -7
  7. package/lib/dataLoader.js.map +1 -1
  8. package/lib/dragAndDropHandler/generateHitAreas.d.ts.map +1 -1
  9. package/lib/dragAndDropHandler/generateHitAreas.js +3 -2
  10. package/lib/dragAndDropHandler/generateHitAreas.js.map +1 -1
  11. package/lib/dragAndDropHandler/index.d.ts +2 -4
  12. package/lib/dragAndDropHandler/index.d.ts.map +1 -1
  13. package/lib/dragAndDropHandler/index.js +2 -9
  14. package/lib/dragAndDropHandler/index.js.map +1 -1
  15. package/lib/elementsRenderer.d.ts +11 -4
  16. package/lib/elementsRenderer.d.ts.map +1 -1
  17. package/lib/elementsRenderer.js +104 -43
  18. package/lib/elementsRenderer.js.map +1 -1
  19. package/lib/events.d.ts +13 -7
  20. package/lib/events.d.ts.map +1 -1
  21. package/lib/folderOpenState.d.ts +8 -0
  22. package/lib/folderOpenState.d.ts.map +1 -0
  23. package/lib/folderOpenState.js +22 -0
  24. package/lib/folderOpenState.js.map +1 -0
  25. package/lib/index.d.ts +27 -20
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +166 -65
  28. package/lib/index.js.map +1 -1
  29. package/lib/keyHandler.d.ts +16 -3
  30. package/lib/keyHandler.d.ts.map +1 -1
  31. package/lib/keyHandler.js +79 -32
  32. package/lib/keyHandler.js.map +1 -1
  33. package/lib/methodTypes.d.ts +3 -0
  34. package/lib/methodTypes.d.ts.map +1 -1
  35. package/lib/mouseHandler.d.ts.map +1 -1
  36. package/lib/mouseHandler.js +12 -12
  37. package/lib/mouseHandler.js.map +1 -1
  38. package/lib/node.d.ts +12 -10
  39. package/lib/node.d.ts.map +1 -1
  40. package/lib/node.js +12 -10
  41. package/lib/node.js.map +1 -1
  42. package/lib/nodeElement/folderElement.d.ts +7 -5
  43. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  44. package/lib/nodeElement/folderElement.js +35 -55
  45. package/lib/nodeElement/folderElement.js.map +1 -1
  46. package/lib/nodeElement/index.d.ts +3 -2
  47. package/lib/nodeElement/index.d.ts.map +1 -1
  48. package/lib/nodeElement/index.js +15 -12
  49. package/lib/nodeElement/index.js.map +1 -1
  50. package/lib/options.d.ts +29 -14
  51. package/lib/options.d.ts.map +1 -1
  52. package/lib/options.js +12 -0
  53. package/lib/options.js.map +1 -0
  54. package/lib/saveStateHandler.d.ts.map +1 -1
  55. package/lib/saveStateHandler.js +6 -6
  56. package/lib/saveStateHandler.js.map +1 -1
  57. package/lib/selectNodeHandler.d.ts +3 -0
  58. package/lib/selectNodeHandler.d.ts.map +1 -1
  59. package/lib/selectNodeHandler.js +66 -22
  60. package/lib/selectNodeHandler.js.map +1 -1
  61. package/lib/setDefaultOptions.d.ts.map +1 -1
  62. package/lib/setDefaultOptions.js +1 -0
  63. package/lib/setDefaultOptions.js.map +1 -1
  64. package/lib/version.d.ts +1 -1
  65. package/lib/version.js +1 -1
  66. package/lib/version.js.map +1 -1
  67. package/package.json +38 -32
  68. package/src/classNames.ts +4 -4
  69. package/src/dataLoader.ts +9 -10
  70. package/src/dragAndDropHandler/generateHitAreas.ts +3 -2
  71. package/src/dragAndDropHandler/index.ts +3 -17
  72. package/src/elementsRenderer.ts +124 -55
  73. package/src/events.ts +13 -7
  74. package/src/folderOpenState.ts +38 -0
  75. package/src/index.ts +191 -82
  76. package/src/keyHandler.ts +104 -34
  77. package/src/methodTypes.ts +6 -0
  78. package/src/mouseHandler.ts +15 -13
  79. package/src/node.ts +12 -10
  80. package/src/nodeElement/folderElement.ts +47 -72
  81. package/src/nodeElement/index.ts +20 -17
  82. package/src/options.ts +46 -14
  83. package/src/saveStateHandler.ts +5 -8
  84. package/src/selectNodeHandler.ts +69 -25
  85. package/src/setDefaultOptions.ts +1 -0
  86. package/src/version.ts +1 -1
  87. package/tree_element.css +48 -5
  88. package/tree_element.debug.js +540 -278
  89. package/tree_element.debug.js.map +1 -1
  90. package/tree_element.js +2 -2
  91. package/tree_element.js.map +1 -1
@@ -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;
@@ -16,6 +17,7 @@ interface ElementsRendererParams {
16
17
  getTree: GetTree;
17
18
  htmlElement: HTMLElement;
18
19
  isNodeSelected: IsNodeSelected;
20
+ multiSelect: boolean;
19
21
  onCreateLi?: OnCreateLi;
20
22
  openedIcon?: IconElement;
21
23
  rtl?: boolean;
@@ -36,8 +38,7 @@ interface ElementsRendererParams {
36
38
  * selected and loading classes.
37
39
  */
38
40
  export default class ElementsRenderer {
39
- public closedIconElement?: globalThis.Node;
40
- public openedIconElement?: globalThis.Node;
41
+ public icons: FolderIcons;
41
42
  private ariaLabel?: string;
42
43
  private ariaLabelledBy?: string;
43
44
  private autoEscape: boolean;
@@ -49,6 +50,7 @@ export default class ElementsRenderer {
49
50
  private groupUlTemplate: HTMLUListElement;
50
51
  private htmlElement: HTMLElement;
51
52
  private isNodeSelected: IsNodeSelected;
53
+ private multiSelect: boolean;
52
54
  private nodeTemplate: HTMLLIElement;
53
55
  private onCreateLi?: OnCreateLi;
54
56
  private openedFolderTemplate: HTMLLIElement;
@@ -69,6 +71,7 @@ export default class ElementsRenderer {
69
71
  getTree,
70
72
  htmlElement,
71
73
  isNodeSelected,
74
+ multiSelect,
72
75
  onCreateLi,
73
76
  openedIcon = "+",
74
77
  rtl,
@@ -85,19 +88,23 @@ export default class ElementsRenderer {
85
88
  this.getTree = getTree;
86
89
  this.htmlElement = htmlElement;
87
90
  this.isNodeSelected = isNodeSelected;
91
+ this.multiSelect = multiSelect;
88
92
  this.onCreateLi = onCreateLi;
89
93
  this.rtl = rtl;
90
94
  this.setNodeElement = setNodeElement;
91
95
  this.showEmptyFolder = showEmptyFolder;
92
96
  this.tabIndex = tabIndex;
93
- this.openedIconElement = this.createButtonElement(openedIcon);
94
- this.closedIconElement = this.createButtonElement(closedIcon);
95
-
96
- this.rootUlTemplate = this.createUlTemplate(true);
97
- this.groupUlTemplate = this.createUlTemplate(false);
98
- this.nodeTemplate = this.createNodeTemplate();
99
- this.openedFolderTemplate = this.createFolderTemplate(true);
100
- 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;
101
108
  }
102
109
 
103
110
  public render(fromNode: Node | null): void {
@@ -112,14 +119,14 @@ export default class ElementsRenderer {
112
119
  * null when the folder is not rendered or has no children.
113
120
  */
114
121
  public renderChildren(node: Node): HTMLUListElement | null {
115
- return !node.element || !node.hasChildren()
116
- ? null
117
- : this.createDomElements(
122
+ return node.element && node.hasChildren()
123
+ ? this.createDomElements(
118
124
  node.element,
119
125
  node.children,
120
126
  false,
121
127
  node.getLevel() + 1,
122
- );
128
+ )
129
+ : null;
123
130
  }
124
131
 
125
132
  public renderFromNode(node: Node): void {
@@ -146,6 +153,50 @@ export default class ElementsRenderer {
146
153
  }
147
154
  }
148
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
+
149
200
  private attachNodeData(node: Node, li: HTMLElement): void {
150
201
  node.element = li;
151
202
  this.setNodeElement(li, node);
@@ -208,11 +259,11 @@ export default class ElementsRenderer {
208
259
  this.buttonLeft ? 1 : 0
209
260
  ] as HTMLSpanElement;
210
261
 
211
- this.fillTitleSpan(titleSpan, node.name, isSelected, level);
262
+ this.fillNode(li, titleSpan, node.name, isSelected, level);
212
263
 
213
264
  if (node.is_loading) {
214
265
  li.classList.add(this.classNames.loading);
215
- titleSpan.setAttribute("aria-busy", "true");
266
+ li.setAttribute("aria-busy", "true");
216
267
  }
217
268
 
218
269
  return li;
@@ -224,37 +275,26 @@ export default class ElementsRenderer {
224
275
  * state, so there is a template for each.
225
276
  */
226
277
  private createFolderTemplate(isOpen: boolean): HTMLLIElement {
227
- const liClasses = [this.classNames.common, this.classNames.folder];
278
+ const li = this.createLiTemplate(
279
+ `${this.classNames.common} ${this.classNames.folder}`,
280
+ );
281
+ setFolderLiOpenState(li, isOpen, this.classNames);
228
282
 
229
- if (!isOpen) {
230
- liClasses.push(this.classNames.closed);
231
- }
232
-
233
- const li = this.createLiTemplate(liClasses.join(" "));
234
283
  const div = li.firstChild as HTMLDivElement;
235
284
 
236
285
  // The toggler is hidden from screen readers: the icon is decoration, and
237
286
  // aria-expanded on the treeitem has the open state.
238
287
  const toggler = document.createElement("span");
239
- toggler.className = this.getButtonClasses(isOpen);
288
+ toggler.className = this.getButtonClasses();
240
289
  toggler.setAttribute("aria-hidden", "true");
241
-
242
- const iconElement = isOpen
243
- ? this.openedIconElement
244
- : this.closedIconElement;
245
-
246
- if (iconElement) {
247
- toggler.append(iconElement.cloneNode(true));
248
- }
290
+ setTogglerOpenState(toggler, isOpen, this.classNames, this.icons);
249
291
 
250
292
  if (this.buttonLeft) {
251
293
  div.append(toggler);
252
294
  }
253
295
 
254
296
  // title span
255
- const titleSpan = this.createTitleSpanTemplate(true);
256
- titleSpan.setAttribute("aria-expanded", getBoolString(isOpen));
257
- div.append(titleSpan);
297
+ div.append(this.createTitleSpanTemplate(true));
258
298
 
259
299
  if (!this.buttonLeft) {
260
300
  div.append(toggler);
@@ -288,11 +328,22 @@ export default class ElementsRenderer {
288
328
 
289
329
  /*
290
330
  Create the outer part of a <li> template: li > div
331
+ The li is the treeitem, so the ul of its children is inside the treeitem.
291
332
  */
292
333
  private createLiTemplate(liClasses: string): HTMLLIElement {
293
334
  const li = document.createElement("li");
294
335
  li.className = liClasses;
295
- li.setAttribute("role", "none");
336
+ li.setAttribute("role", "treeitem");
337
+
338
+ // Only a multiselect tree marks the nodes that are not selected
339
+ if (this.multiSelect) {
340
+ li.setAttribute("aria-selected", "false");
341
+ }
342
+
343
+ // Every node is focusable, but only the tab stop is in the tab order
344
+ if (this.tabIndex !== undefined) {
345
+ li.setAttribute("tabindex", "-1");
346
+ }
296
347
 
297
348
  const div = document.createElement("div");
298
349
  div.className = `${this.classNames.element} ${this.classNames.common}`;
@@ -318,7 +369,7 @@ export default class ElementsRenderer {
318
369
  const div = li.firstChild as HTMLDivElement;
319
370
  const titleSpan = div.firstChild as HTMLSpanElement;
320
371
 
321
- this.fillTitleSpan(titleSpan, node.name, isSelected, level);
372
+ this.fillNode(li, titleSpan, node.name, isSelected, level);
322
373
 
323
374
  return li;
324
375
  }
@@ -335,6 +386,16 @@ export default class ElementsRenderer {
335
386
  return li;
336
387
  }
337
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
+
338
399
  private createTitleSpanTemplate(isFolder: boolean): HTMLSpanElement {
339
400
  const titleSpan = document.createElement("span");
340
401
 
@@ -347,13 +408,6 @@ export default class ElementsRenderer {
347
408
  classes += ` ${this.buttonLeft ? this.classNames.titleButtonLeft : this.classNames.titleButtonRight}`;
348
409
 
349
410
  titleSpan.className = classes;
350
- titleSpan.setAttribute("role", "treeitem");
351
- titleSpan.setAttribute("aria-selected", "false");
352
-
353
- // Every node is focusable, but only the tab stop is in the tab order
354
- if (this.tabIndex !== undefined) {
355
- titleSpan.setAttribute("tabindex", "-1");
356
- }
357
411
 
358
412
  return titleSpan;
359
413
  }
@@ -391,29 +445,35 @@ export default class ElementsRenderer {
391
445
  if (this.ariaLabelledBy !== undefined) {
392
446
  ul.setAttribute("aria-labelledby", this.ariaLabelledBy);
393
447
  }
448
+
449
+ if (this.multiSelect) {
450
+ ul.setAttribute("aria-multiselectable", "true");
451
+ }
394
452
  }
395
453
 
396
454
  return ul;
397
455
  }
398
456
 
399
457
  /*
400
- Set the parts of a cloned title span that differ per node
458
+ Set the parts of a cloned li and title span that differ per node
401
459
  */
402
- private fillTitleSpan(
460
+ private fillNode(
461
+ li: HTMLLIElement,
403
462
  titleSpan: HTMLSpanElement,
404
463
  nodeName: string,
405
464
  isSelected: boolean,
406
465
  level: number,
407
466
  ): void {
408
- titleSpan.setAttribute("aria-level", String(level));
467
+ li.setAttribute("aria-level", String(level));
409
468
 
410
469
  if (isSelected) {
411
- titleSpan.setAttribute("aria-selected", "true");
470
+ li.setAttribute("aria-selected", "true");
412
471
 
413
472
  const tabIndex = this.tabIndex;
414
473
 
415
- if (tabIndex !== undefined) {
416
- titleSpan.setAttribute("tabindex", String(tabIndex));
474
+ // With multiSelect the tab stop is the active node, see updateTabStop
475
+ if (tabIndex !== undefined && !this.multiSelect) {
476
+ li.setAttribute("tabindex", String(tabIndex));
417
477
  }
418
478
  }
419
479
 
@@ -423,15 +483,15 @@ export default class ElementsRenderer {
423
483
  // eslint-disable-next-line no-unsanitized/property -- the user opted out of escaping with autoEscape: false
424
484
  titleSpan.innerHTML = nodeName;
425
485
  }
486
+
487
+ // The treeitem is named by its title, without the html when autoEscape
488
+ // is false. Otherwise the name would contain everything in the row.
489
+ li.setAttribute("aria-label", titleSpan.textContent);
426
490
  }
427
491
 
428
- private getButtonClasses(isOpen: boolean): string {
492
+ private getButtonClasses(): string {
429
493
  const classes = [this.classNames.toggler, this.classNames.common];
430
494
 
431
- if (!isOpen) {
432
- classes.push(this.classNames.closed);
433
- }
434
-
435
495
  if (this.buttonLeft) {
436
496
  classes.push(this.classNames.togglerLeft);
437
497
  } else {
@@ -447,4 +507,13 @@ export default class ElementsRenderer {
447
507
  private mustRenderChildren(node: Node): boolean {
448
508
  return node.hasChildren() && node.is_open === true;
449
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
+ }
450
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
+ };