tree-element 0.1.1 → 0.3.0

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 (155) hide show
  1. package/README.md +43 -31
  2. package/lib/animation.d.ts +2 -2
  3. package/lib/animation.d.ts.map +1 -1
  4. package/lib/animation.js +16 -12
  5. package/lib/animation.js.map +1 -1
  6. package/lib/classNames.d.ts.map +1 -1
  7. package/lib/classNames.js +2 -2
  8. package/lib/classNames.js.map +1 -1
  9. package/lib/dataLoader.d.ts +1 -0
  10. package/lib/dataLoader.d.ts.map +1 -1
  11. package/lib/dataLoader.js +22 -20
  12. package/lib/dataLoader.js.map +1 -1
  13. package/lib/dragAndDropHandler/binarySearch.js +3 -3
  14. package/lib/dragAndDropHandler/binarySearch.js.map +1 -1
  15. package/lib/dragAndDropHandler/dragElement.d.ts.map +1 -1
  16. package/lib/dragAndDropHandler/dragElement.js +11 -13
  17. package/lib/dragAndDropHandler/dragElement.js.map +1 -1
  18. package/lib/dragAndDropHandler/generateHitAreas.d.ts.map +1 -1
  19. package/lib/dragAndDropHandler/generateHitAreas.js +25 -25
  20. package/lib/dragAndDropHandler/generateHitAreas.js.map +1 -1
  21. package/lib/dragAndDropHandler/index.d.ts.map +1 -1
  22. package/lib/dragAndDropHandler/index.js +125 -158
  23. package/lib/dragAndDropHandler/index.js.map +1 -1
  24. package/lib/dragAndDropHandler/iterateVisibleNodes.d.ts.map +1 -1
  25. package/lib/dragAndDropHandler/iterateVisibleNodes.js +23 -22
  26. package/lib/dragAndDropHandler/iterateVisibleNodes.js.map +1 -1
  27. package/lib/dragAndDropHandler/types.d.ts.map +1 -1
  28. package/lib/elementsRenderer.d.ts +20 -7
  29. package/lib/elementsRenderer.d.ts.map +1 -1
  30. package/lib/elementsRenderer.js +195 -136
  31. package/lib/elementsRenderer.js.map +1 -1
  32. package/lib/events.d.ts +44 -11
  33. package/lib/events.d.ts.map +1 -1
  34. package/lib/index.d.ts +36 -13
  35. package/lib/index.d.ts.map +1 -1
  36. package/lib/index.js +386 -349
  37. package/lib/index.js.map +1 -1
  38. package/lib/keyHandler.d.ts +13 -8
  39. package/lib/keyHandler.d.ts.map +1 -1
  40. package/lib/keyHandler.js +106 -51
  41. package/lib/keyHandler.js.map +1 -1
  42. package/lib/methodTypes.d.ts +3 -3
  43. package/lib/methodTypes.d.ts.map +1 -1
  44. package/lib/mouseHandler.d.ts +1 -1
  45. package/lib/mouseHandler.d.ts.map +1 -1
  46. package/lib/mouseHandler.js +36 -49
  47. package/lib/mouseHandler.js.map +1 -1
  48. package/lib/mouseUtils.d.ts.map +1 -1
  49. package/lib/mouseUtils.js +2 -2
  50. package/lib/mouseUtils.js.map +1 -1
  51. package/lib/node.d.ts +47 -16
  52. package/lib/node.d.ts.map +1 -1
  53. package/lib/node.js +169 -171
  54. package/lib/node.js.map +1 -1
  55. package/lib/nodeElement/borderDropHint.d.ts.map +1 -1
  56. package/lib/nodeElement/borderDropHint.js +4 -5
  57. package/lib/nodeElement/borderDropHint.js.map +1 -1
  58. package/lib/nodeElement/folderElement.d.ts +8 -5
  59. package/lib/nodeElement/folderElement.d.ts.map +1 -1
  60. package/lib/nodeElement/folderElement.js +75 -67
  61. package/lib/nodeElement/folderElement.js.map +1 -1
  62. package/lib/nodeElement/ghostDropHint.d.ts.map +1 -1
  63. package/lib/nodeElement/ghostDropHint.js +13 -13
  64. package/lib/nodeElement/ghostDropHint.js.map +1 -1
  65. package/lib/nodeElement/index.d.ts +2 -1
  66. package/lib/nodeElement/index.d.ts.map +1 -1
  67. package/lib/nodeElement/index.js +38 -30
  68. package/lib/nodeElement/index.js.map +1 -1
  69. package/lib/nodeUtils.d.ts.map +1 -1
  70. package/lib/nodeUtils.js +1 -1
  71. package/lib/nodeUtils.js.map +1 -1
  72. package/lib/options.d.ts +19 -3
  73. package/lib/options.d.ts.map +1 -1
  74. package/lib/positionUtils.js +3 -3
  75. package/lib/positionUtils.js.map +1 -1
  76. package/lib/requestUrl.d.ts +1 -1
  77. package/lib/requestUrl.d.ts.map +1 -1
  78. package/lib/requestUrl.js +6 -17
  79. package/lib/requestUrl.js.map +1 -1
  80. package/lib/saveStateHandler.d.ts.map +1 -1
  81. package/lib/saveStateHandler.js +59 -89
  82. package/lib/saveStateHandler.js.map +1 -1
  83. package/lib/scrollHandler/containerScrollParent.d.ts.map +1 -1
  84. package/lib/scrollHandler/containerScrollParent.js +11 -18
  85. package/lib/scrollHandler/containerScrollParent.js.map +1 -1
  86. package/lib/scrollHandler/createScrollParent.d.ts.map +1 -1
  87. package/lib/scrollHandler/createScrollParent.js +12 -13
  88. package/lib/scrollHandler/createScrollParent.js.map +1 -1
  89. package/lib/scrollHandler/documentScrollParent.d.ts.map +1 -1
  90. package/lib/scrollHandler/documentScrollParent.js +18 -27
  91. package/lib/scrollHandler/documentScrollParent.js.map +1 -1
  92. package/lib/scrollHandler/scrollParent.d.ts.map +1 -1
  93. package/lib/scrollHandler/scrollParent.js +30 -34
  94. package/lib/scrollHandler/scrollParent.js.map +1 -1
  95. package/lib/scrollHandler.d.ts.map +1 -1
  96. package/lib/scrollHandler.js +11 -14
  97. package/lib/scrollHandler.js.map +1 -1
  98. package/lib/selectNodeHandler.d.ts +4 -2
  99. package/lib/selectNodeHandler.d.ts.map +1 -1
  100. package/lib/selectNodeHandler.js +64 -83
  101. package/lib/selectNodeHandler.js.map +1 -1
  102. package/lib/setDefaultOptions.d.ts.map +1 -1
  103. package/lib/setDefaultOptions.js +34 -38
  104. package/lib/setDefaultOptions.js.map +1 -1
  105. package/lib/triggerCustomEvent.d.ts.map +1 -1
  106. package/lib/triggerCustomEvent.js +2 -2
  107. package/lib/triggerCustomEvent.js.map +1 -1
  108. package/lib/util.d.ts.map +1 -1
  109. package/lib/util.js +2 -2
  110. package/lib/util.js.map +1 -1
  111. package/lib/version.d.ts +1 -1
  112. package/lib/version.js +1 -1
  113. package/lib/version.js.map +1 -1
  114. package/package.json +43 -32
  115. package/src/animation.ts +14 -12
  116. package/src/classNames.ts +52 -52
  117. package/src/dataLoader.ts +79 -71
  118. package/src/dragAndDropHandler/binarySearch.ts +17 -17
  119. package/src/dragAndDropHandler/dragElement.ts +51 -50
  120. package/src/dragAndDropHandler/generateHitAreas.ts +156 -161
  121. package/src/dragAndDropHandler/index.ts +334 -348
  122. package/src/dragAndDropHandler/iterateVisibleNodes.ts +61 -63
  123. package/src/dragAndDropHandler/types.ts +5 -5
  124. package/src/elementsRenderer.ts +377 -294
  125. package/src/events.ts +241 -208
  126. package/src/header.txt +1 -1
  127. package/src/index.ts +259 -229
  128. package/src/keyHandler.ts +179 -115
  129. package/src/methodTypes.ts +7 -10
  130. package/src/mouseHandler.ts +316 -316
  131. package/src/mouseUtils.ts +14 -14
  132. package/src/node.ts +773 -771
  133. package/src/nodeElement/borderDropHint.ts +25 -25
  134. package/src/nodeElement/folderElement.ts +116 -102
  135. package/src/nodeElement/ghostDropHint.ts +64 -62
  136. package/src/nodeElement/index.ts +81 -73
  137. package/src/nodeUtils.ts +5 -5
  138. package/src/options.ts +25 -5
  139. package/src/requestUrl.ts +7 -14
  140. package/src/saveStateHandler.ts +191 -207
  141. package/src/scrollHandler/containerScrollParent.ts +40 -49
  142. package/src/scrollHandler/createScrollParent.ts +26 -27
  143. package/src/scrollHandler/documentScrollParent.ts +66 -72
  144. package/src/scrollHandler/scrollParent.ts +90 -86
  145. package/src/scrollHandler.ts +45 -45
  146. package/src/selectNodeHandler.ts +171 -165
  147. package/src/setDefaultOptions.ts +44 -32
  148. package/src/triggerCustomEvent.ts +7 -3
  149. package/src/util.ts +2 -3
  150. package/src/version.ts +1 -1
  151. package/tree_element.css +87 -69
  152. package/tree_element.debug.js +3866 -3831
  153. package/tree_element.debug.js.map +1 -1
  154. package/tree_element.js +2 -2
  155. package/tree_element.js.map +1 -1
@@ -6,362 +6,445 @@ import type { IconElement, OnCreateLi } from "./options";
6
6
  import { getBoolString } from "./util";
7
7
 
8
8
  interface ElementsRendererParams {
9
- autoEscape: boolean;
10
- buttonLeft: boolean;
11
- classNames: ClassNames;
12
- closedIcon?: IconElement;
13
- dragAndDrop: boolean;
14
- getTree: GetTree;
15
- htmlElement: HTMLElement;
16
- isNodeSelected: IsNodeSelected;
17
- onCreateLi?: OnCreateLi;
18
- openedIcon?: IconElement;
19
- rtl?: boolean;
20
- setNodeElement: (element: HTMLElement, node: Node) => void;
21
- showEmptyFolder: boolean;
22
- tabIndex?: number;
9
+ ariaLabel?: string;
10
+ ariaLabelledBy?: string;
11
+ autoEscape: boolean;
12
+ buttonLeft: boolean;
13
+ classNames: ClassNames;
14
+ closedIcon?: IconElement;
15
+ dragAndDrop: boolean;
16
+ getTree: GetTree;
17
+ htmlElement: HTMLElement;
18
+ isNodeSelected: IsNodeSelected;
19
+ onCreateLi?: OnCreateLi;
20
+ openedIcon?: IconElement;
21
+ rtl?: boolean;
22
+ setNodeElement: (element: HTMLElement, node: Node) => void;
23
+ showEmptyFolder: boolean;
24
+ tabIndex?: number;
23
25
  }
24
26
 
27
+ /* Renders the tree to html.
28
+ *
29
+ * The children of a closed folder are not rendered. They are rendered when
30
+ * the folder is opened, see renderChildren.
31
+ *
32
+ * The elements are created by cloning templates. A template is a fully
33
+ * built <li> or <ul> with everything that is the same for every node
34
+ * (structure, roles, static classes, the toggler icon). Per node only the
35
+ * parts that differ are set: the title, the aria attributes and the
36
+ * selected and loading classes.
37
+ */
25
38
  export default class ElementsRenderer {
26
- public closedIconElement?: HTMLElement | Text;
27
- public openedIconElement?: HTMLElement | Text;
28
- private autoEscape: boolean;
29
- private buttonLeft: boolean;
30
- private classNames: ClassNames;
31
- private dragAndDrop: boolean;
32
- private getTree: GetTree;
33
- private htmlElement: HTMLElement;
34
- private isNodeSelected: IsNodeSelected;
35
- private onCreateLi?: OnCreateLi;
36
- private rtl?: boolean;
37
- private setNodeElement: (element: HTMLElement, node: Node) => void;
38
- private showEmptyFolder: boolean;
39
- private tabIndex?: number;
40
-
41
- constructor({
42
- autoEscape,
43
- buttonLeft,
44
- classNames,
45
- closedIcon,
46
- dragAndDrop,
47
- getTree,
48
- htmlElement,
49
- isNodeSelected,
50
- onCreateLi,
51
- openedIcon,
52
- rtl,
53
- setNodeElement,
54
- showEmptyFolder,
55
- tabIndex,
56
- }: ElementsRendererParams) {
57
- this.autoEscape = autoEscape;
58
- this.buttonLeft = buttonLeft;
59
- this.classNames = classNames;
60
- this.dragAndDrop = dragAndDrop;
61
- this.getTree = getTree;
62
- this.htmlElement = htmlElement;
63
- this.isNodeSelected = isNodeSelected;
64
- this.onCreateLi = onCreateLi;
65
- this.rtl = rtl;
66
- this.setNodeElement = setNodeElement;
67
- this.showEmptyFolder = showEmptyFolder;
68
- this.tabIndex = tabIndex;
69
- this.openedIconElement = this.createButtonElement(openedIcon ?? "+");
70
- this.closedIconElement = this.createButtonElement(closedIcon ?? "-");
39
+ public closedIconElement?: globalThis.Node;
40
+ public openedIconElement?: globalThis.Node;
41
+ private ariaLabel?: string;
42
+ private ariaLabelledBy?: string;
43
+ private autoEscape: boolean;
44
+ private buttonLeft: boolean;
45
+ private classNames: ClassNames;
46
+ private closedFolderTemplate: HTMLLIElement;
47
+ private dragAndDrop: boolean;
48
+ private getTree: GetTree;
49
+ private groupUlTemplate: HTMLUListElement;
50
+ private htmlElement: HTMLElement;
51
+ private isNodeSelected: IsNodeSelected;
52
+ private nodeTemplate: HTMLLIElement;
53
+ private onCreateLi?: OnCreateLi;
54
+ private openedFolderTemplate: HTMLLIElement;
55
+ private rootUlTemplate: HTMLUListElement;
56
+ private rtl?: boolean;
57
+ private setNodeElement: (element: HTMLElement, node: Node) => void;
58
+ private showEmptyFolder: boolean;
59
+ private tabIndex?: number;
60
+
61
+ constructor({
62
+ ariaLabel,
63
+ ariaLabelledBy,
64
+ autoEscape,
65
+ buttonLeft,
66
+ classNames,
67
+ closedIcon = "-",
68
+ dragAndDrop,
69
+ getTree,
70
+ htmlElement,
71
+ isNodeSelected,
72
+ onCreateLi,
73
+ openedIcon = "+",
74
+ rtl,
75
+ setNodeElement,
76
+ showEmptyFolder,
77
+ tabIndex,
78
+ }: ElementsRendererParams) {
79
+ this.ariaLabel = ariaLabel;
80
+ this.ariaLabelledBy = ariaLabelledBy;
81
+ this.autoEscape = autoEscape;
82
+ this.buttonLeft = buttonLeft;
83
+ this.classNames = classNames;
84
+ this.dragAndDrop = dragAndDrop;
85
+ this.getTree = getTree;
86
+ this.htmlElement = htmlElement;
87
+ this.isNodeSelected = isNodeSelected;
88
+ this.onCreateLi = onCreateLi;
89
+ this.rtl = rtl;
90
+ this.setNodeElement = setNodeElement;
91
+ this.showEmptyFolder = showEmptyFolder;
92
+ 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);
101
+ }
102
+
103
+ public render(fromNode: Node | null): void {
104
+ if (fromNode?.parent) {
105
+ this.renderFromNode(fromNode);
106
+ } else {
107
+ this.renderFromRoot();
71
108
  }
109
+ }
110
+
111
+ /* Render the children of a rendered folder. Returns the new <ul>, or
112
+ * null when the folder is not rendered or has no children.
113
+ */
114
+ public renderChildren(node: Node): HTMLUListElement | null {
115
+ return !node.element || !node.hasChildren()
116
+ ? null
117
+ : this.createDomElements(
118
+ node.element,
119
+ node.children,
120
+ false,
121
+ node.getLevel() + 1,
122
+ );
123
+ }
72
124
 
73
- public render(fromNode: Node | null): void {
74
- if (fromNode?.parent) {
75
- this.renderFromNode(fromNode);
76
- } else {
77
- this.renderFromRoot();
78
- }
125
+ public renderFromNode(node: Node): void {
126
+ if (!node.element) {
127
+ return;
79
128
  }
80
129
 
81
- public renderFromNode(node: Node): void {
82
- if (!node.element) {
83
- return;
84
- }
85
-
86
- const currentLi = node.element;
87
- const newLi = this.createLi(node, node.getLevel());
88
- currentLi.replaceWith(newLi);
130
+ const currentLi = node.element;
131
+ const newLi = this.createLi(node, node.getLevel());
132
+ currentLi.replaceWith(newLi);
89
133
 
90
- // create children
91
- this.createDomElements(newLi, node.children, false, node.getLevel() + 1);
134
+ if (this.mustRenderChildren(node)) {
135
+ this.createDomElements(newLi, node.children, false, node.getLevel() + 1);
92
136
  }
137
+ }
93
138
 
94
- public renderFromRoot(): void {
95
- this.htmlElement.textContent = '';
139
+ public renderFromRoot(): void {
140
+ this.htmlElement.textContent = "";
96
141
 
97
- const tree = this.getTree();
142
+ const tree = this.getTree();
98
143
 
99
- if (tree) {
100
- this.createDomElements(this.htmlElement, tree.children, true, 1);
101
- }
144
+ if (tree) {
145
+ this.createDomElements(this.htmlElement, tree.children, true, 1);
102
146
  }
103
-
104
- private attachNodeData(node: Node, li: HTMLElement): void {
105
- node.element = li;
106
- this.setNodeElement(li, node);
147
+ }
148
+
149
+ private attachNodeData(node: Node, li: HTMLElement): void {
150
+ node.element = li;
151
+ this.setNodeElement(li, node);
152
+ }
153
+
154
+ /* Returns the template of a toggler icon; it is cloned for every folder.
155
+ * A string is parsed as html and becomes a fragment, so it can hold
156
+ * entities, text and elements.
157
+ */
158
+ private createButtonElement(value: IconElement): globalThis.Node | undefined {
159
+ if (typeof value === "string") {
160
+ const template = document.createElement("template");
161
+ // eslint-disable-next-line no-unsanitized/property -- icon options are documented to accept html
162
+ template.innerHTML = value;
163
+
164
+ return template.content;
165
+ }
166
+ return value.nodeType ? value : undefined;
167
+ }
168
+
169
+ private createDomElements(
170
+ element: Element,
171
+ children: Node[],
172
+ isRootNode: boolean,
173
+ level: number,
174
+ ): HTMLUListElement {
175
+ const template = isRootNode ? this.rootUlTemplate : this.groupUlTemplate;
176
+ const ul = template.cloneNode() as HTMLUListElement;
177
+ element.append(ul);
178
+
179
+ for (const child of children) {
180
+ const li = this.createLi(child, level);
181
+ ul.append(li);
182
+
183
+ if (this.mustRenderChildren(child)) {
184
+ this.createDomElements(li, child.children, false, level + 1);
185
+ }
107
186
  }
108
187
 
109
- private createButtonElement(
110
- value: IconElement,
111
- ): HTMLElement | Text | undefined {
112
- if (typeof value === "string") {
113
- // convert value to html
114
- const div = document.createElement("div");
115
- div.innerHTML = value;
116
-
117
- return document.createTextNode(div.innerHTML);
118
- } else if (value.nodeType) {
119
- return value;
120
- } else {
121
- return undefined;
122
- }
188
+ return ul;
189
+ }
190
+
191
+ private createFolderLi(
192
+ node: Node,
193
+ level: number,
194
+ isSelected: boolean,
195
+ ): HTMLLIElement {
196
+ const template = node.is_open
197
+ ? this.openedFolderTemplate
198
+ : this.closedFolderTemplate;
199
+ const li = template.cloneNode(true) as HTMLLIElement;
200
+
201
+ if (isSelected) {
202
+ li.classList.add(this.classNames.selected);
123
203
  }
124
204
 
125
- private createDomElements(
126
- element: Element,
127
- children: Node[],
128
- isRootNode: boolean,
129
- level: number,
130
- ): void {
131
- const ul = this.createUl(isRootNode);
132
- element.appendChild(ul);
133
-
134
- for (const child of children) {
135
- const li = this.createLi(child, level);
136
- ul.appendChild(li);
137
-
138
- if (child.hasChildren()) {
139
- this.createDomElements(li, child.children, false, level + 1);
140
- }
141
- }
205
+ // li > div > [toggler], title span, [toggler]
206
+ const div = li.firstChild as HTMLDivElement;
207
+ const titleSpan = div.childNodes[
208
+ this.buttonLeft ? 1 : 0
209
+ ] as HTMLSpanElement;
210
+
211
+ this.fillTitleSpan(titleSpan, node.name, isSelected, level);
212
+
213
+ if (node.is_loading) {
214
+ li.classList.add(this.classNames.loading);
215
+ titleSpan.setAttribute("aria-busy", "true");
142
216
  }
143
217
 
144
- private createFolderLi(
145
- node: Node,
146
- level: number,
147
- isSelected: boolean,
148
- ): HTMLLIElement {
149
- const buttonClasses = this.getButtonClasses(node);
150
- const folderClasses = this.getFolderClasses(node, isSelected);
151
-
152
- const iconElement = node.is_open
153
- ? this.openedIconElement
154
- : this.closedIconElement;
155
-
156
- // li
157
- const li = document.createElement("li");
158
- li.className = `${this.classNames.common} ${folderClasses}`;
159
- li.setAttribute("role", "none");
160
-
161
- // div
162
- const div = document.createElement("div");
163
- div.className = `${this.classNames.element} ${this.classNames.common}`;
164
- div.setAttribute("role", "none");
165
-
166
- li.appendChild(div);
167
-
168
- // button link
169
- const buttonLink = document.createElement("a");
170
- buttonLink.className = buttonClasses;
171
-
172
- if (iconElement) {
173
- buttonLink.appendChild(iconElement.cloneNode(true));
174
- }
175
-
176
- if (this.buttonLeft) {
177
- div.appendChild(buttonLink);
178
- }
179
-
180
- // title span
181
- const titleSpan = this.createTitleSpan(
182
- node.name,
183
- isSelected,
184
- true,
185
- level,
186
- );
187
- titleSpan.setAttribute("aria-expanded", getBoolString(node.is_open));
188
- div.appendChild(titleSpan);
218
+ return li;
219
+ }
189
220
 
190
- if (!this.buttonLeft) {
191
- div.appendChild(buttonLink);
192
- }
221
+ /* Template for a folder <li>:
222
+ * li > div > [toggler], title span, [toggler]
223
+ * The toggler icon and the aria-expanded attribute depend on the open
224
+ * state, so there is a template for each.
225
+ */
226
+ private createFolderTemplate(isOpen: boolean): HTMLLIElement {
227
+ const liClasses = [this.classNames.common, this.classNames.folder];
193
228
 
194
- return li;
229
+ if (!isOpen) {
230
+ liClasses.push(this.classNames.closed);
195
231
  }
196
232
 
197
- /* Create the <li> element
198
- * Attach it to node.element.
199
- * Call onCreateLi
200
- */
201
- private createLi(node: Node, level: number): HTMLLIElement {
202
- const isSelected = this.isNodeSelected(node);
233
+ const li = this.createLiTemplate(liClasses.join(" "));
234
+ const div = li.firstChild as HTMLDivElement;
235
+
236
+ // The toggler is hidden from screen readers: the icon is decoration, and
237
+ // aria-expanded on the treeitem has the open state.
238
+ const toggler = document.createElement("span");
239
+ toggler.className = this.getButtonClasses(isOpen);
240
+ toggler.setAttribute("aria-hidden", "true");
203
241
 
204
- const mustShowFolder =
205
- node.isFolder() || (node.isEmptyFolder && this.showEmptyFolder);
242
+ const iconElement = isOpen
243
+ ? this.openedIconElement
244
+ : this.closedIconElement;
206
245
 
207
- const li = mustShowFolder
208
- ? this.createFolderLi(node, level, isSelected)
209
- : this.createNodeLi(node, level, isSelected);
246
+ if (iconElement) {
247
+ toggler.append(iconElement.cloneNode(true));
248
+ }
210
249
 
211
- this.attachNodeData(node, li);
250
+ if (this.buttonLeft) {
251
+ div.append(toggler);
252
+ }
212
253
 
213
- if (this.onCreateLi) {
214
- this.onCreateLi(node, li, isSelected);
215
- }
254
+ // title span
255
+ const titleSpan = this.createTitleSpanTemplate(true);
256
+ titleSpan.setAttribute("aria-expanded", getBoolString(isOpen));
257
+ div.append(titleSpan);
216
258
 
217
- return li;
259
+ if (!this.buttonLeft) {
260
+ div.append(toggler);
218
261
  }
219
262
 
220
- private createNodeLi(
221
- node: Node,
222
- level: number,
223
- isSelected: boolean,
224
- ): HTMLLIElement {
225
- const liClasses = [this.classNames.common];
226
-
227
- if (isSelected) {
228
- liClasses.push(this.classNames.selected);
229
- }
230
-
231
- const classString = liClasses.join(" ");
232
-
233
- // li
234
- const li = document.createElement("li");
235
- li.className = classString;
236
- li.setAttribute("role", "none");
237
-
238
- // div
239
- const div = document.createElement("div");
240
- div.className = `${this.classNames.element} ${this.classNames.common}`;
241
- div.setAttribute("role", "none");
242
-
243
- li.appendChild(div);
244
-
245
- // title span
246
- const titleSpan = this.createTitleSpan(
247
- node.name,
248
- isSelected,
249
- false,
250
- level,
251
- );
252
- div.appendChild(titleSpan);
263
+ return li;
264
+ }
265
+
266
+ /* Create the <li> element
267
+ * Attach it to node.element.
268
+ * Call onCreateLi
269
+ */
270
+ private createLi(node: Node, level: number): HTMLLIElement {
271
+ const isSelected = this.isNodeSelected(node);
253
272
 
254
- return li;
273
+ const mustShowFolder =
274
+ node.isFolder() || (node.isEmptyFolder && this.showEmptyFolder);
275
+
276
+ const li = mustShowFolder
277
+ ? this.createFolderLi(node, level, isSelected)
278
+ : this.createNodeLi(node, level, isSelected);
279
+
280
+ this.attachNodeData(node, li);
281
+
282
+ if (this.onCreateLi) {
283
+ this.onCreateLi(node, li, isSelected);
255
284
  }
256
285
 
257
- private createTitleSpan(
258
- nodeName: string,
259
- isSelected: boolean,
260
- isFolder: boolean,
261
- level: number,
262
- ): HTMLSpanElement {
263
- const titleSpan = document.createElement("span");
286
+ return li;
287
+ }
288
+
289
+ /*
290
+ Create the outer part of a <li> template: li > div
291
+ */
292
+ private createLiTemplate(liClasses: string): HTMLLIElement {
293
+ const li = document.createElement("li");
294
+ li.className = liClasses;
295
+ li.setAttribute("role", "none");
264
296
 
265
- let classes = `${this.classNames.title} ${this.classNames.common}`;
297
+ const div = document.createElement("div");
298
+ div.className = `${this.classNames.element} ${this.classNames.common}`;
299
+ div.setAttribute("role", "none");
266
300
 
267
- if (isFolder) {
268
- classes += ` ${this.classNames.titleFolder}`;
269
- }
301
+ li.append(div);
270
302
 
271
- classes += ` ${this.buttonLeft ? this.classNames.titleButtonLeft : this.classNames.titleButtonRight}`;
303
+ return li;
304
+ }
272
305
 
273
- titleSpan.className = classes;
306
+ private createNodeLi(
307
+ node: Node,
308
+ level: number,
309
+ isSelected: boolean,
310
+ ): HTMLLIElement {
311
+ const li = this.nodeTemplate.cloneNode(true) as HTMLLIElement;
274
312
 
275
- if (isSelected) {
276
- const tabIndex = this.tabIndex;
313
+ if (isSelected) {
314
+ li.classList.add(this.classNames.selected);
315
+ }
316
+
317
+ // li > div > title span
318
+ const div = li.firstChild as HTMLDivElement;
319
+ const titleSpan = div.firstChild as HTMLSpanElement;
320
+
321
+ this.fillTitleSpan(titleSpan, node.name, isSelected, level);
322
+
323
+ return li;
324
+ }
277
325
 
278
- if (tabIndex !== undefined) {
279
- titleSpan.setAttribute("tabindex", `${tabIndex}`);
280
- }
281
- }
326
+ /*
327
+ Template for a <li> without children: li > div > title span
328
+ */
329
+ private createNodeTemplate(): HTMLLIElement {
330
+ const li = this.createLiTemplate(this.classNames.common);
331
+ const div = li.firstChild as HTMLDivElement;
282
332
 
283
- this.setTreeItemAriaAttributes(titleSpan, nodeName, level, isSelected);
333
+ div.append(this.createTitleSpanTemplate(false));
284
334
 
285
- if (this.autoEscape) {
286
- titleSpan.textContent = nodeName;
287
- } else {
288
- titleSpan.innerHTML = nodeName;
289
- }
335
+ return li;
336
+ }
290
337
 
291
- return titleSpan;
338
+ private createTitleSpanTemplate(isFolder: boolean): HTMLSpanElement {
339
+ const titleSpan = document.createElement("span");
340
+
341
+ let classes = `${this.classNames.title} ${this.classNames.common}`;
342
+
343
+ if (isFolder) {
344
+ classes += ` ${this.classNames.titleFolder}`;
292
345
  }
293
346
 
294
- private createUl(isRootNode: boolean): HTMLUListElement {
295
- let classString;
296
- let role;
347
+ classes += ` ${this.buttonLeft ? this.classNames.titleButtonLeft : this.classNames.titleButtonRight}`;
297
348
 
298
- if (!isRootNode) {
299
- classString = "";
300
- role = "group";
301
- } else {
302
- classString = this.classNames.tree;
303
- role = "tree";
349
+ 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
+ }
304
357
 
305
- if (this.rtl) {
306
- classString += ` ${this.classNames.rtl}`;
307
- }
308
- }
358
+ return titleSpan;
359
+ }
309
360
 
310
- if (this.dragAndDrop) {
311
- classString += ` ${this.classNames.dnd}`;
312
- }
361
+ private createUlTemplate(isRootNode: boolean): HTMLUListElement {
362
+ let classString;
363
+ let role;
313
364
 
314
- const ul = document.createElement("ul");
315
- ul.className = `${this.classNames.common} ${classString}`;
365
+ if (isRootNode) {
366
+ classString = this.classNames.tree;
367
+ role = "tree";
316
368
 
317
- ul.setAttribute("role", role);
369
+ if (this.rtl) {
370
+ classString += ` ${this.classNames.rtl}`;
371
+ }
372
+ } else {
373
+ classString = "";
374
+ role = "group";
375
+ }
318
376
 
319
- return ul;
377
+ if (this.dragAndDrop) {
378
+ classString += ` ${this.classNames.dnd}`;
320
379
  }
321
380
 
322
- private getButtonClasses(node: Node): string {
323
- const classes = [this.classNames.toggler, this.classNames.common];
381
+ const ul = document.createElement("ul");
382
+ ul.className = `${this.classNames.common} ${classString}`;
324
383
 
325
- if (!node.is_open) {
326
- classes.push(this.classNames.closed);
327
- }
384
+ ul.setAttribute("role", role);
328
385
 
329
- if (this.buttonLeft) {
330
- classes.push(this.classNames.togglerLeft);
331
- } else {
332
- classes.push(this.classNames.togglerRight);
333
- }
386
+ if (isRootNode) {
387
+ if (this.ariaLabel !== undefined) {
388
+ ul.setAttribute("aria-label", this.ariaLabel);
389
+ }
334
390
 
335
- return classes.join(" ");
391
+ if (this.ariaLabelledBy !== undefined) {
392
+ ul.setAttribute("aria-labelledby", this.ariaLabelledBy);
393
+ }
336
394
  }
337
395
 
338
- private getFolderClasses(node: Node, isSelected: boolean): string {
339
- const classes = [this.classNames.folder];
396
+ return ul;
397
+ }
398
+
399
+ /*
400
+ Set the parts of a cloned title span that differ per node
401
+ */
402
+ private fillTitleSpan(
403
+ titleSpan: HTMLSpanElement,
404
+ nodeName: string,
405
+ isSelected: boolean,
406
+ level: number,
407
+ ): void {
408
+ titleSpan.setAttribute("aria-level", String(level));
409
+
410
+ if (isSelected) {
411
+ titleSpan.setAttribute("aria-selected", "true");
412
+
413
+ const tabIndex = this.tabIndex;
340
414
 
341
- if (!node.is_open) {
342
- classes.push(this.classNames.closed);
343
- }
415
+ if (tabIndex !== undefined) {
416
+ titleSpan.setAttribute("tabindex", String(tabIndex));
417
+ }
418
+ }
344
419
 
345
- if (isSelected) {
346
- classes.push(this.classNames.selected);
347
- }
420
+ if (this.autoEscape) {
421
+ titleSpan.textContent = nodeName;
422
+ } else {
423
+ // eslint-disable-next-line no-unsanitized/property -- the user opted out of escaping with autoEscape: false
424
+ titleSpan.innerHTML = nodeName;
425
+ }
426
+ }
348
427
 
349
- if (node.is_loading) {
350
- classes.push(this.classNames.loading);
351
- }
428
+ private getButtonClasses(isOpen: boolean): string {
429
+ const classes = [this.classNames.toggler, this.classNames.common];
352
430
 
353
- return classes.join(" ");
431
+ if (!isOpen) {
432
+ classes.push(this.classNames.closed);
354
433
  }
355
434
 
356
- private setTreeItemAriaAttributes(
357
- element: HTMLElement,
358
- name: string,
359
- level: number,
360
- isSelected: boolean,
361
- ) {
362
- element.setAttribute("aria-label", name);
363
- element.setAttribute("aria-level", `${level}`);
364
- element.setAttribute("aria-selected", getBoolString(isSelected));
365
- element.setAttribute("role", "treeitem");
435
+ if (this.buttonLeft) {
436
+ classes.push(this.classNames.togglerLeft);
437
+ } else {
438
+ classes.push(this.classNames.togglerRight);
366
439
  }
440
+
441
+ return classes.join(" ");
442
+ }
443
+
444
+ /*
445
+ The children of a closed folder are rendered when it is opened
446
+ */
447
+ private mustRenderChildren(node: Node): boolean {
448
+ return node.hasChildren() && node.is_open === true;
449
+ }
367
450
  }