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
package/src/options.ts CHANGED
@@ -31,10 +31,32 @@ export type OnIsMoveHandle = (el: HTMLElement) => boolean;
31
31
 
32
32
  export type OnSetStateFromStorage = ((data: string) => void) | undefined;
33
33
 
34
+ /* The options that setOption can change. The others are read once, in the
35
+ * constructor, so changing them later would have no effect or leave the tree
36
+ * in an inconsistent state.
37
+ */
38
+ export const changeableOptions = [
39
+ "animationSpeed",
40
+ "dataUrl",
41
+ "dragAndDrop",
42
+ "keyboardSupport",
43
+ "multiSelect",
44
+ "onCanSelectNode",
45
+ "selectable",
46
+ "slide",
47
+ "startDndDelay",
48
+ "tabIndex",
49
+ ] as const;
50
+
51
+ /**
52
+ * The options that `setOption` can change.
53
+ */
54
+ export type ChangeableOption = (typeof changeableOptions)[number];
55
+
34
56
  export interface TreeElementOptions {
35
57
  /**
36
- * The duration of the slide animation: `"fast"` is 200ms, `"slow"` is
37
- * 600ms, a number is milliseconds.
58
+ * The duration of the slide animation: `"fast"` is 200 milliseconds,
59
+ * `"slow"` is 600, and a number is milliseconds.
38
60
  *
39
61
  * @defaultValue `"fast"`, or `0` when the user prefers reduced motion
40
62
  * (`prefers-reduced-motion: reduce`).
@@ -56,7 +78,7 @@ export interface TreeElementOptions {
56
78
  */
57
79
  ariaLabelledBy?: string;
58
80
  /**
59
- * Escape node names. With `false`, names are inserted as html — only do
81
+ * Escape node names. With `false`, names are inserted as HTML—only do
60
82
  * that for data you control.
61
83
  *
62
84
  * @defaultValue `true`
@@ -81,10 +103,10 @@ export interface TreeElementOptions {
81
103
  */
82
104
  buttonLeft: boolean;
83
105
  /**
84
- * The prefix of the css classes the widget puts on the elements it
106
+ * The prefix of the CSS classes the widget puts on the elements it
85
107
  * creates: with `"my-tree"` a title gets `my-tree-title` instead of
86
- * `tree-element-title`. The bundled stylesheet uses the default prefix, so
87
- * change it only if you bring your own css. See
108
+ * `tree-element-title`. The bundled style sheet uses the default prefix,
109
+ * so change it only if you bring your own CSS. See
88
110
  * [Styling](/guide/styling#changing-the-class-names).
89
111
  *
90
112
  * @defaultValue `"tree-element"`
@@ -92,12 +114,12 @@ export interface TreeElementOptions {
92
114
  */
93
115
  classPrefix: string;
94
116
  /**
95
- * The icon of a closed folder. A string is inserted as html; an element is
96
- * cloned for every node.
117
+ * The icon of a closed folder. A string is inserted as HTML, and an element
118
+ * is cloned for every node.
97
119
  *
98
120
  * Type: `HTMLElement | string`
99
121
  *
100
- * @defaultValue An svg triangle pointing right, or left when `rtl` is
122
+ * @defaultValue An SVG triangle pointing right, or left when `rtl` is
101
123
  * true. It is sized in em and filled with `currentColor`.
102
124
  * @group Appearance
103
125
  */
@@ -125,7 +147,7 @@ export interface TreeElementOptions {
125
147
  */
126
148
  dataFilter?: DataFilter;
127
149
  /**
128
- * A url to fetch the data from, or a function that returns one. The
150
+ * A URL to fetch the data from, or a function that returns one. The
129
151
  * function is called with the node that is being loaded, and without an
130
152
  * argument for the initial load. See
131
153
  * [Loading on demand](/guide/loading-on-demand).
@@ -151,6 +173,16 @@ export interface TreeElementOptions {
151
173
  * @group Selection
152
174
  */
153
175
  keyboardSupport: boolean;
176
+ /**
177
+ * Let the user select multiple nodes: a click adds a node to the
178
+ * selection, or removes it when it is selected. The arrow keys move the
179
+ * focus instead of the selection, and `Space` adds the focused node or
180
+ * removes it. See [Selection](/guide/selection#multiple-nodes).
181
+ *
182
+ * @defaultValue `false`
183
+ * @group Selection
184
+ */
185
+ multiSelect: boolean;
154
186
  /**
155
187
  * The class used to create nodes. Subclass [`Node`](/reference/node) to
156
188
  * add your own methods. The `Node` class is not exposed on the global
@@ -227,7 +259,7 @@ export interface TreeElementOptions {
227
259
  */
228
260
  onDragStop?: DragMethod;
229
261
  /**
230
- * Read the saved state, as a json string, from somewhere other than
262
+ * Read the saved state, as a JSON string, from somewhere other than
231
263
  * `localStorage`.
232
264
  *
233
265
  * Type: `() => string`
@@ -245,7 +277,7 @@ export interface TreeElementOptions {
245
277
  */
246
278
  onIsMoveHandle?: OnIsMoveHandle;
247
279
  /**
248
- * Write the state, as a json string, to somewhere other than
280
+ * Write the state, as a JSON string, to somewhere other than
249
281
  * `localStorage`.
250
282
  *
251
283
  * Type: `(state: string) => void`
@@ -258,7 +290,7 @@ export interface TreeElementOptions {
258
290
  *
259
291
  * Type: `HTMLElement | string`
260
292
  *
261
- * @defaultValue An svg triangle pointing down, sized in em and filled with
293
+ * @defaultValue An SVG triangle pointing down, sized in em and filled with
262
294
  * `currentColor`.
263
295
  * @group Appearance
264
296
  */
@@ -280,7 +312,7 @@ export interface TreeElementOptions {
280
312
  rtl?: boolean;
281
313
  /**
282
314
  * Remember the open and selected nodes in `localStorage`. A string is used
283
- * as the storage key; `true` uses `tree`. See
315
+ * as the storage key, and `true` uses `tree`. See
284
316
  * [Saving state](/guide/saving-state).
285
317
  *
286
318
  * @defaultValue `false`
@@ -67,10 +67,7 @@ export default class SaveStateHandler {
67
67
  }
68
68
 
69
69
  public getNodeIdToBeSelected(): NodeId | null {
70
- if (!this.saveStateOption) {
71
- return null;
72
- }
73
-
70
+ // Without the saveState option there is no state
74
71
  const state = this.getStateFromStorage();
75
72
 
76
73
  return state?.selected_node ? (state.selected_node[0] ?? null) : null;
@@ -243,8 +240,9 @@ export default class SaveStateHandler {
243
240
  }
244
241
  }
245
242
 
243
+ // result: is a node selected?
246
244
  private selectInitialNodes(nodeIds: NodeId[]): boolean {
247
- let selectCount = 0;
245
+ let isNodeSelected = false;
248
246
 
249
247
  for (const nodeId of nodeIds) {
250
248
  const node = this.getNodeById(nodeId);
@@ -253,11 +251,10 @@ export default class SaveStateHandler {
253
251
  continue;
254
252
  }
255
253
 
256
- selectCount += 1;
257
-
258
254
  this.addToSelection(node);
255
+ isNodeSelected = true;
259
256
  }
260
257
 
261
- return selectCount !== 0;
258
+ return isNodeSelected;
262
259
  }
263
260
  }
@@ -128,9 +128,9 @@ export default class SelectNodeHandler {
128
128
  this.selectedNodes.delete(node.id);
129
129
 
130
130
  if (includeChildren) {
131
- node.iterate(() => {
132
- if (node.id != null) {
133
- this.selectedNodes.delete(node.id);
131
+ node.iterate((child: Node) => {
132
+ if (child.id != null) {
133
+ this.selectedNodes.delete(child.id);
134
134
  }
135
135
  return true;
136
136
  });
@@ -149,43 +149,49 @@ export default class SelectNodeHandler {
149
149
  * mustToggle: support deselecting the selected node
150
150
  */
151
151
  public selectSingleNode(node: Node, optionsParam?: SelectNodeOptions) {
152
- const defaultOptions = { mustSetFocus: true, mustToggle: true };
153
- const selectOptions = { ...defaultOptions, ...optionsParam };
154
-
155
- const canSelect = (): boolean => {
156
- if (!this.getSelectable()) {
157
- return false;
158
- }
159
-
160
- const onCanSelectNode = this.getOnCanSelectNode();
161
- return onCanSelectNode ? onCanSelectNode(node) : true;
162
- };
163
-
164
- if (!canSelect()) {
152
+ if (!this.canSelect(node)) {
165
153
  return;
166
154
  }
167
155
 
168
- const deselectCurrentNode = (deselectedNode: Node) => {
169
- this.removeFromSelection(deselectedNode);
170
- this.getNodeElementForNode(deselectedNode).deselect();
171
- };
156
+ const defaultOptions = { mustSetFocus: true, mustToggle: true };
157
+ const selectOptions = { ...defaultOptions, ...optionsParam };
158
+
159
+ const otherSelectedNodes = this.getSelectedNodes().filter(
160
+ (selectedNode) => selectedNode !== node,
161
+ );
172
162
 
173
163
  if (this.isNodeSelected(node)) {
174
- if (selectOptions.mustToggle) {
175
- deselectCurrentNode(node);
164
+ if (otherSelectedNodes.length) {
165
+ // Part of a multiple selection: keep only this node selected
166
+ for (const otherNode of otherSelectedNodes) {
167
+ this.deselectNode(otherNode);
168
+ }
169
+
170
+ for (const otherNode of otherSelectedNodes) {
171
+ this.triggerEvent("tree.deselect", { node: otherNode });
172
+ }
173
+ } else if (selectOptions.mustToggle) {
174
+ this.deselectNode(node);
176
175
  this.triggerEvent("tree.deselect", { node });
177
176
  }
178
177
  } else {
179
- const deselectedNode = this.getSelectedNode();
178
+ // tree.select reports the first deselected node; the others, from a
179
+ // multiple selection, get a tree.deselect.
180
+ const [deselectedNode = null, ...alsoDeselectedNodes] =
181
+ otherSelectedNodes;
180
182
 
181
- if (deselectedNode) {
182
- deselectCurrentNode(deselectedNode);
183
+ for (const otherNode of otherSelectedNodes) {
184
+ this.deselectNode(otherNode);
183
185
  }
184
186
 
185
187
  this.addToSelection(node);
186
188
  this.openParents(node);
187
189
  this.getNodeElementForNode(node).select(selectOptions.mustSetFocus);
188
190
 
191
+ for (const otherNode of alsoDeselectedNodes) {
192
+ this.triggerEvent("tree.deselect", { node: otherNode });
193
+ }
194
+
189
195
  this.triggerEvent("tree.select", {
190
196
  deselectedNode,
191
197
  node,
@@ -195,4 +201,42 @@ export default class SelectNodeHandler {
195
201
  this.updateTabStop();
196
202
  this.saveState();
197
203
  }
204
+
205
+ /* Add a node to the selection, or remove it when it is selected, without
206
+ * changing the other selected nodes.
207
+ * Checks if the node is selectable and saves the state.
208
+ */
209
+ public toggleNodeInSelection(node: Node, mustSetFocus = true): void {
210
+ if (!this.canSelect(node)) {
211
+ return;
212
+ }
213
+
214
+ if (this.isNodeSelected(node)) {
215
+ this.deselectNode(node);
216
+ this.triggerEvent("tree.deselect", { node });
217
+ } else {
218
+ this.addToSelection(node);
219
+ this.openParents(node);
220
+ this.getNodeElementForNode(node).select(mustSetFocus);
221
+
222
+ this.triggerEvent("tree.select", { deselectedNode: null, node });
223
+ }
224
+
225
+ this.updateTabStop();
226
+ this.saveState();
227
+ }
228
+
229
+ private canSelect(node: Node): boolean {
230
+ if (!this.getSelectable()) {
231
+ return false;
232
+ }
233
+
234
+ const onCanSelectNode = this.getOnCanSelectNode();
235
+ return onCanSelectNode ? onCanSelectNode(node) : true;
236
+ }
237
+
238
+ private deselectNode(node: Node): void {
239
+ this.removeFromSelection(node);
240
+ this.getNodeElementForNode(node).deselect();
241
+ }
198
242
  }
@@ -29,6 +29,7 @@ const defaults: TreeElementOptions = {
29
29
  // dataUrl: undefined,
30
30
  dragAndDrop: false,
31
31
  keyboardSupport: true,
32
+ multiSelect: false,
32
33
  nodeClass: Node,
33
34
  // onCanMove: undefined, // Can this node be moved?
34
35
  // onCanMoveTo: undefined, // Can this node be moved to this position? function(moved_node, target_node, position)
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  // Generated by config/generateVersion.mjs. Do not edit.
2
- const version = "0.3.2";
2
+ const version = "0.4.1";
3
3
 
4
4
  export default version;
package/tree_element.css CHANGED
@@ -2,6 +2,7 @@
2
2
  Colors. Override them on :root or on an element around the tree, for example
3
3
  for a dark theme. :where keeps the specificity at 0, so any rule wins.
4
4
  --tree-element-selected-color defaults to --tree-element-title-color.
5
+ Set --tree-element-line-color to transparent to hide the tree lines.
5
6
  */
6
7
  :where(:root) {
7
8
  --tree-element-title-color: #1c4257;
@@ -12,6 +13,19 @@
12
13
  --tree-element-drop-hint-color: #00f;
13
14
  --tree-element-dragging-color: #fff;
14
15
  --tree-element-dragging-background: #000;
16
+ --tree-element-line-color: #d5dbe1;
17
+ }
18
+
19
+ /*
20
+ The width of the toggler. The indentation and the tree lines follow it, so
21
+ the line stays under the center of the toggler. 1.5em is 24px at a 16px
22
+ font: above that the toggler grows with the font, so the space around the
23
+ 0.8em icon, which separates it from the title, grows along. Set it to the
24
+ width of a custom icon that is wider. It is resolved where it is used, so em
25
+ is the font size of the tree.
26
+ */
27
+ :where(:root) {
28
+ --tree-element-toggler-size: max(24px, 1.5em);
15
29
  }
16
30
 
17
31
  ul.tree-element {
@@ -27,11 +41,14 @@ ul.tree-element.tree-element-dnd {
27
41
 
28
42
  ul.tree-element ul.tree-element-common {
29
43
  list-style: none outside;
30
- margin-left: 12px;
44
+ margin-left: calc(var(--tree-element-toggler-size) / 2);
31
45
  margin-right: 0;
32
46
  margin-bottom: 0;
33
47
  padding: 0;
34
48
  display: block;
49
+ /* The line down from the toggler of the folder: the indent is half the
50
+ width of the toggler, so the line is under its center. */
51
+ border-left: 1px solid var(--tree-element-line-color);
35
52
  }
36
53
 
37
54
  ul.tree-element li.tree-element-closed > ul.tree-element-common {
@@ -52,12 +69,16 @@ ul.tree-element .tree-element-toggler {
52
69
  align-items: center;
53
70
  justify-content: center;
54
71
  box-sizing: border-box;
55
- min-width: 24px;
72
+ min-width: var(--tree-element-toggler-size);
56
73
  min-height: 24px;
57
74
  border-bottom: none;
58
75
  color: var(--tree-element-toggler-color);
59
76
  text-decoration: none;
60
77
  vertical-align: middle;
78
+ /* The line under the toggler is 1px wide, so its center is half a pixel
79
+ after the center of the toggler. Move the toggler along.
80
+ Margin or padding would be rounded to a whole pixel; translate isn't. */
81
+ translate: 0.5px;
61
82
  }
62
83
 
63
84
  ul.tree-element .tree-element-toggler:hover {
@@ -86,10 +107,26 @@ ul.tree-element .tree-element-title {
86
107
  vertical-align: middle;
87
108
  }
88
109
 
110
+ /* The li is the treeitem, so it gets the focus. Show the focus ring around
111
+ the title, not around the li with all its children. */
112
+
113
+ ul.tree-element li.tree-element-common:focus {
114
+ outline: none;
115
+ }
116
+
117
+ ul.tree-element li.tree-element-common:focus-visible
118
+ > .tree-element-element
119
+ .tree-element-title {
120
+ /* Same as the default focus ring of the browser. Browsers without
121
+ -webkit-focus-ring-color ignore the second line. */
122
+ outline: auto;
123
+ outline-color: -webkit-focus-ring-color;
124
+ }
125
+
89
126
  /* Line up with the titles of folders, after the toggler */
90
127
 
91
128
  ul.tree-element .tree-element-title-button-left {
92
- margin-left: 24px;
129
+ margin-left: var(--tree-element-toggler-size);
93
130
  }
94
131
 
95
132
  ul.tree-element .tree-element-title-button-left.tree-element-title-folder {
@@ -170,12 +207,18 @@ ul.tree-element.tree-element-rtl {
170
207
 
171
208
  ul.tree-element.tree-element-rtl ul.tree-element-common {
172
209
  margin-left: 0;
173
- margin-right: 12px;
210
+ margin-right: calc(var(--tree-element-toggler-size) / 2);
211
+ border-left: none;
212
+ border-right: 1px solid var(--tree-element-line-color);
213
+ }
214
+
215
+ ul.tree-element.tree-element-rtl .tree-element-toggler {
216
+ translate: -0.5px;
174
217
  }
175
218
 
176
219
  ul.tree-element.tree-element-rtl .tree-element-title {
177
220
  margin-left: 0;
178
- margin-right: 24px;
221
+ margin-right: var(--tree-element-toggler-size);
179
222
  }
180
223
 
181
224
  ul.tree-element.tree-element-rtl .tree-element-title.tree-element-title-folder {