@neovici/cosmoz-treenode-navigator 5.0.0 → 5.2.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.
@@ -12,22 +12,17 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  steps:
14
14
  - name: Checkout
15
- uses: actions/checkout@v1
16
-
17
- - name: Use Node.js 14.x
18
- uses: actions/setup-node@v1
15
+ uses: actions/checkout@v2
19
16
  with:
20
- node-version: 14.x
17
+ fetch-depth: 0
21
18
 
22
- - name: Restore npm cache
23
- id: node-cache
24
- uses: actions/cache@v2
19
+ - name: Use Node.js
20
+ uses: actions/setup-node@v2
25
21
  with:
26
- path: node_modules
27
- key: ${{ runner.os }}-node-0-${{ hashFiles('**/package-lock.json') }}
22
+ node-version: 16.x
23
+ cache: 'npm'
28
24
 
29
25
  - name: Install
30
- if: steps.node-cache.outputs.cache-hit != 'true'
31
26
  run: npm ci
32
27
 
33
28
  - name: Lint commit messages
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # [5.2.0](https://github.com/neovici/cosmoz-treenode-navigator/compare/v5.1.0...v5.2.0) (2022-08-01)
2
+
3
+
4
+ ### Features
5
+
6
+ * **cosmoz-treenode-navigator:** improve performance ([ac12d74](https://github.com/neovici/cosmoz-treenode-navigator/commit/ac12d74345501e34cdccde6962563a24d6efed4f))
7
+
8
+ # [5.1.0](https://github.com/neovici/cosmoz-treenode-navigator/compare/v5.0.1...v5.1.0) (2022-07-28)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **ci:** worflow typo ([17b92aa](https://github.com/neovici/cosmoz-treenode-navigator/commit/17b92aa332f0411b6548729002fd9cf62a99d223))
14
+
15
+
16
+ ### Features
17
+
18
+ * **cosmoz-treenode-navigator:** add keyboard navigation ([26b6d42](https://github.com/neovici/cosmoz-treenode-navigator/commit/26b6d42d99737d0b48a7fbc21a8677117b1d8a9e))
19
+ * refactor with lit-virtualizer instead of listbox ([7b69ab4](https://github.com/neovici/cosmoz-treenode-navigator/commit/7b69ab4e0e049c9c7f8e03c3b781f02fab83eb18))
20
+ * support `Right` key ([28a4df6](https://github.com/neovici/cosmoz-treenode-navigator/commit/28a4df6d35bc62ac49aba81135891ce8fcacfa05))
21
+
22
+ ## [5.0.1](https://github.com/neovici/cosmoz-treenode-navigator/compare/v5.0.0...v5.0.1) (2022-07-27)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **search:** remove label ([950ed7c](https://github.com/neovici/cosmoz-treenode-navigator/commit/950ed7cef3ab282f5fea8925dae121c771723eab))
28
+
1
29
  # [5.0.0](https://github.com/neovici/cosmoz-treenode-navigator/compare/v4.0.0...v5.0.0) (2022-07-19)
2
30
 
3
31
 
@@ -1,25 +1,21 @@
1
1
  import { component, useEffect, useState, useMemo, useCallback } from 'haunted';
2
2
  import { html } from 'lit-html';
3
3
  import { when } from 'lit-html/directives/when.js';
4
-
4
+ import { guard } from 'lit-html/directives/guard.js';
5
+ import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
5
6
  import '@neovici/cosmoz-input';
6
- import '@neovici/cosmoz-autocomplete';
7
+ import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
7
8
  import { notifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
8
9
 
9
10
  import {
11
+ getParentPath,
10
12
  computeDataPlane,
11
13
  computeRowClass,
12
- computeSearching,
13
- getNodeName,
14
- hasChildren,
15
- nodeStyles,
16
- normalizeNodes,
17
14
  onNodeDblClicked,
18
- renderLevel,
19
- showGlobalSearchBtn
20
15
  } from './helpers';
16
+ import styles from './cosmoz-treenode-navigator.styles';
21
17
 
22
- const TreenodeNavigator = host => {
18
+ const TreenodeNavigator = (host) => {
23
19
  const {
24
20
  /**
25
21
  * The main node structure
@@ -38,16 +34,12 @@ const TreenodeNavigator = host => {
38
34
  * Minimum length of searchValue to trigger a search
39
35
  */
40
36
  searchMinLength,
41
- nodePath
37
+ nodePath,
42
38
  } = host,
43
39
  /**
44
40
  * The path of the opened node
45
41
  */
46
42
  [openNodePath, setOpenNodePath] = useState(''),
47
- /**
48
- * The nodes on the path of the opened node
49
- */
50
- [nodesOnOpenNodePath, setNodesOnOpenNodePath] = useState([]),
51
43
  /**
52
44
  * The highlighted (focused) node
53
45
  * This is the node which is currently selected in the list
@@ -57,90 +49,41 @@ const TreenodeNavigator = host => {
57
49
  * The search string
58
50
  */
59
51
  [searchValue, setSearchValue] = useState(''),
52
+ search = useMemo(
53
+ () => (searchValue?.length > searchMinLength && searchValue) || undefined,
54
+ [searchValue, searchMinLength]
55
+ ),
60
56
  /**
61
57
  * The currently displayed node list
62
58
  */
63
- dataPlane = useMemo(() => {
64
- return computeDataPlane(
65
- computeSearching(searchValue, searchMinLength),
66
- searchValue,
67
- renderLevel(openNodePath, tree),
68
- tree
69
- );
70
- }, [tree, openNodePath, highlightedNode, searchValue]),
59
+ dataPlane = useMemo(
60
+ () => computeDataPlane(tree, search, openNodePath),
61
+ [tree, search, openNodePath]
62
+ ),
71
63
  /**
72
64
  * Opens a node (renderLevel) based on a given path
73
65
  * @param {object} clickedNode - The clicked node
74
66
  * @return {undefined}
75
67
  */
76
- openNode = useCallback(clickedNode => {
77
- if (hasChildren(clickedNode)) {
78
- setOpenNodePath(clickedNode.path);
79
- setSearchValue('');
80
- }
81
- if (!clickedNode) {
82
- setOpenNodePath('');
68
+ openNode = useCallback(
69
+ (clickedNode) => {
70
+ setOpenNodePath(clickedNode?.pathLocator || '');
83
71
  setSearchValue('');
84
- setHighlightedNode(null);
85
- }
86
- }, []),
87
- /**
88
- * Returns true, if the path of a node should be visible in the view
89
- * @param {Number} index - The node's current index in the list
90
- * @param {Object} parentSectionName - The nodes current parent section name
91
- * @return {Boolean} - If the path should be visible
92
- */
93
- renderSection = (index, parentSectionName) => {
94
- const _dataPlane = computeDataPlane(
95
- computeSearching(searchValue, searchMinLength),
96
- searchValue,
97
- renderLevel(openNodePath, tree),
98
- tree
99
- );
100
- if (
101
- !computeSearching(searchValue, searchMinLength) ||
102
- index == null ||
103
- _dataPlane == null ||
104
- index >= _dataPlane.length ||
105
- parentSectionName == null
106
- ) {
107
- return false;
108
- }
109
- if (index === 0) {
110
- return true;
111
- }
112
- const prevItem = _dataPlane[index - 1];
113
- return prevItem.parentSectionName !== parentSectionName;
114
- };
115
-
116
- useEffect(() => {
117
- if (!openNodePath) {
118
- notifyProperty(host, 'highlightedNodePath', '');
119
- notifyProperty(host, 'nodesOnNodePath', []);
120
- return;
121
- }
122
- setNodesOnOpenNodePath(
123
- normalizeNodes(tree.getPathNodes(openNodePath).filter(item => item))
72
+ setHighlightedNode();
73
+ },
74
+ [tree]
124
75
  );
125
- }, [openNodePath]);
126
76
 
127
77
  useEffect(() => {
128
78
  if (!openNodePath) {
129
79
  notifyProperty(host, 'highlightedNodePath', '');
130
- return;
80
+ notifyProperty(host, 'nodesOnNodePath', []);
131
81
  }
132
- setNodesOnOpenNodePath(
133
- normalizeNodes(tree.getPathNodes(openNodePath).filter(item => item))
134
- );
135
82
  }, [openNodePath]);
136
83
 
137
84
  useEffect(() => {
138
85
  if (nodePath !== undefined) {
139
- host.dispatchEvent(
140
- new CustomEvent('select-node', {
141
- detail: {}
142
- })
143
- );
86
+ host.dispatchEvent(new CustomEvent('select-node'));
144
87
  }
145
88
  }, [nodePath]);
146
89
 
@@ -148,77 +91,113 @@ const TreenodeNavigator = host => {
148
91
  notifyProperty(
149
92
  host,
150
93
  'highlightedNodePath',
151
- !highlightedNode ? '' : highlightedNode.path
94
+ highlightedNode?.pathLocator || ''
152
95
  );
153
96
  }, [highlightedNode]);
154
97
 
155
- return html`
156
- <style>
157
- :host {
158
- --cosmoz-treenode-navigator-select-node-icon-color: var(
159
- --primary-color,
160
- white
161
- );
162
- --cosmoz-treenode-navigator-list-item-focused-color: #f0f8ff;
163
- }
164
-
165
- #header {
166
- margin: 0 16px;
167
- }
168
-
169
- #header a {
170
- text-decoration: none;
171
- color: inherit;
172
- }
173
-
174
- .icon {
175
- display: inline-block;
176
- position: relative;
177
- padding: 8px;
178
- outline: none;
179
- user-select: none;
180
- cursor: pointer;
181
- z-index: 0;
182
- line-height: 1;
183
- width: 40px;
184
- height: 40px;
185
- box-sizing: border-box;
186
- }
187
-
188
- .path {
189
- display: flex;
190
- align-items: center;
191
- flex-wrap: wrap;
192
- }
98
+ const meta = useMeta({ dataPlane, highlightedNode, openNode });
193
99
 
194
- .slash {
195
- margin: 0 2px;
100
+ useEffect(() => {
101
+ // eslint-disable-next-line max-lines-per-function
102
+ const handler = (e) => {
103
+ if ((e.ctrlKey && e.altKey) || e.defaultPrevented) {
104
+ return;
196
105
  }
197
-
198
- .pointer {
199
- cursor: pointer;
106
+ const { dataPlane: items, highlightedNode: node, openNode: open } = meta,
107
+ getIndex = () =>
108
+ items.findIndex((i) => i.pathLocator === node?.pathLocator);
109
+
110
+ switch (e.key) {
111
+ case 'Up':
112
+ case 'ArrowUp':
113
+ {
114
+ const idx = getIndex();
115
+ e.preventDefault();
116
+ setHighlightedNode(items[Math.max(idx - 1, 0)]);
117
+ }
118
+ break;
119
+ case 'Down':
120
+ case 'ArrowDown':
121
+ {
122
+ const idx = getIndex();
123
+ if (idx < items.length - 1) {
124
+ e.preventDefault();
125
+ setHighlightedNode(items[idx + 1]);
126
+ }
127
+ }
128
+ break;
129
+
130
+ case 'Enter':
131
+ if (node) {
132
+ e.preventDefault();
133
+ host.dispatchEvent(new CustomEvent('select-node'));
134
+ }
135
+ break;
136
+
137
+ case 'Right':
138
+ case 'ArrowRight':
139
+ if (node) {
140
+ e.preventDefault();
141
+ open(node);
142
+ }
143
+ break;
144
+ default:
145
+ break;
200
146
  }
147
+ };
201
148
 
202
- cosmoz-listbox {
203
- box-shadow: none;
204
- display: block;
205
- height: var(--cosmoz-treenode-navigator-list-height, 50vh);
206
- position: static;
207
- width: 100%;
208
- }
149
+ document.addEventListener('keydown', handler, true);
150
+ return () => document.removeEventListener('keydown', handler, true);
151
+ }, [meta]);
152
+
153
+ const renderItem = (node, index) => html` <div class="item">
154
+ ${((parentPath) =>
155
+ when(
156
+ search &&
157
+ (index === 0 ||
158
+ parentPath !== getParentPath(tree, dataPlane[index - 1])),
159
+ () =>
160
+ html`
161
+ <div class="section">
162
+ ${tree.getPathString(parentPath, tree.searchProperty)}
163
+ </div>
164
+ `
165
+ ))(getParentPath(tree, node))}
166
+ <div
167
+ class=${computeRowClass('node', node, highlightedNode)}
168
+ @click=${() => setHighlightedNode(node)}
169
+ @dblclick=${(e) => onNodeDblClicked(e, host)}
170
+ >
171
+ <div class="name">${node[tree.searchProperty]}</div>
172
+ ${when(
173
+ tree.hasChildren(node),
174
+ () => html`
175
+ <span class="icon" @click=${() => openNode(node)}>
176
+ <svg
177
+ viewBox="0 0 24 24"
178
+ preserveAspectRatio="xMidYMid meet"
179
+ focusable="false"
180
+ style="pointer-events: none; display: block; width: 100%; height: 100%;"
181
+ >
182
+ <g>
183
+ <path
184
+ d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
185
+ ></path>
186
+ </g>
187
+ </svg>
188
+ </span>
189
+ `
190
+ )}
191
+ </div>
192
+ </div>`;
209
193
 
210
- .btn-ghost {
211
- background: transparent;
212
- border: none;
213
- cursor: pointer;
214
- margin: 0 0.29em;
215
- padding: 0.7em 0.57em;
216
- text-transform: uppercase;
217
- }
194
+ return html`
195
+ <style>
196
+ ${styles}
218
197
  </style>
219
- <div id="header">
198
+ <div class="header">
220
199
  <h3 class="path">
221
- <span class="icon" @click=${ () => openNode() }>
200
+ <span class="icon" @click=${() => openNode()}>
222
201
  <svg
223
202
  viewBox="0 0 24 24"
224
203
  preserveAspectRatio="xMidYMid meet"
@@ -228,84 +207,39 @@ const TreenodeNavigator = host => {
228
207
  <g><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"></path></g>
229
208
  </svg>
230
209
  </span>
231
- ${ nodesOnOpenNodePath.map(node => {
232
- return html` <span class="slash">/</span>
233
- <span class="pointer" tabindex="0" @click="${ () => openNode(node) }"
234
- >${ getNodeName(node) }</span
235
- >`;
236
- }) }
210
+ ${guard([tree, openNodePath], () =>
211
+ ((openNodePath && tree.getPathNodes(openNodePath)) || []).map(
212
+ (node) => html`
213
+ <span class="slash">/</span>
214
+ <span class="pointer" tabindex="0" @click=${() => openNode(node)}
215
+ >${node[tree.searchProperty]}</span
216
+ >
217
+ `
218
+ )
219
+ )}
237
220
  </h3>
238
221
  <cosmoz-input
239
222
  tabindex="0"
240
- .value=${ searchValue }
241
- .placeholder="${ searchPlaceholder }"
242
- .label="${ searchPlaceholder }"
243
- @input="${ ({ target }) => setSearchValue(target.value) }"
223
+ .value=${searchValue}
224
+ .placeholder=${searchPlaceholder}
225
+ @input=${(e) => setSearchValue(e.target.value)}
244
226
  />
245
227
  </div>
246
- ${ when(
247
- tree,
248
- () => html`
249
- <cosmoz-listbox
250
- .query="${ searchValue }"
251
- .items="${ dataPlane }"
252
- .textual="${ item => item.name }"
253
- .itemRenderer=${ (node, index) => html`
254
- <div class="node-item-wrapper">
255
- <style>
256
- ${ nodeStyles }
257
- </style>
258
- ${ when(
259
- renderSection(index, node.parentSectionName),
260
- () => html`
261
- <div class="section">${ node.parentSectionName }</div>
262
- `
263
- ) }
264
- <div
265
- class="${ computeRowClass(
266
- 'node-item pointer',
267
- node,
268
- highlightedNode
269
- ) }"
270
- @click="${ () => setHighlightedNode(node) }"
271
- @dblclick="${ e => onNodeDblClicked(e, host) }"
272
- >
273
- <div style="flex: auto">${ node.name }</div>
274
- ${ when(
275
- hasChildren(node),
276
- () => html`
277
- <span class="icon" @click="${ () => openNode(node) }">
278
- <svg
279
- viewBox="0 0 24 24"
280
- preserveAspectRatio="xMidYMid meet"
281
- focusable="false"
282
- style="pointer-events: none; display: block; width: 100%; height: 100%;"
283
- >
284
- <g>
285
- <path
286
- d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
287
- ></path>
288
- </g>
289
- </svg>
290
- </span>
291
- `
292
- ) }
293
- </div>
294
- </div>
295
- ` }
296
- </cosmoz-listbox>`
297
- ) }
298
- ${ when(
299
- showGlobalSearchBtn(
300
- computeSearching(searchValue, searchMinLength),
301
- openNodePath
302
- ),
303
- () => html`
304
- <button class="btn-ghost" @click="${ () => setOpenNodePath('') }">
305
- ${ searchGlobalPlaceholder }
228
+ ${when(
229
+ tree,
230
+ () => html` <div class="items">
231
+ <div virtualizer-sizer></div>
232
+ ${virtualize({ items: dataPlane, renderItem, scroller: true })}
233
+ </div>`
234
+ )}
235
+ ${when(
236
+ search && openNodePath,
237
+ () => html`
238
+ <button class="btn-ghost" @click=${() => setOpenNodePath('')}>
239
+ ${searchGlobalPlaceholder}
306
240
  </button>
307
241
  `
308
- ) }
242
+ )}
309
243
  `;
310
244
  };
311
245
 
@@ -0,0 +1,119 @@
1
+ import { tagged as css } from '@neovici/cosmoz-utils';
2
+
3
+ export default css`
4
+ :host {
5
+ --cosmoz-treenode-navigator-select-node-icon-color: var(
6
+ --primary-color,
7
+ white
8
+ );
9
+ --cosmoz-treenode-navigator-list-item-focused-color: #f0f8ff;
10
+ }
11
+
12
+ .header {
13
+ margin: 0 16px;
14
+ }
15
+
16
+ .header a {
17
+ text-decoration: none;
18
+ color: inherit;
19
+ }
20
+
21
+ .icon {
22
+ display: inline-block;
23
+ position: relative;
24
+ padding: 8px;
25
+ outline: none;
26
+ user-select: none;
27
+ cursor: pointer;
28
+ z-index: 0;
29
+ line-height: 1;
30
+ width: 40px;
31
+ height: 40px;
32
+ box-sizing: border-box;
33
+ }
34
+
35
+ .path {
36
+ display: flex;
37
+ align-items: center;
38
+ flex-wrap: wrap;
39
+ }
40
+
41
+ .slash {
42
+ margin: 0 2px;
43
+ }
44
+
45
+ .pointer {
46
+ }
47
+
48
+ .items {
49
+ height: var(--cosmoz-treenode-navigator-list-height, 50vh);
50
+ width: 100%;
51
+ }
52
+
53
+ .section {
54
+ background-color: #f5f5f5;
55
+ padding: 5px;
56
+ }
57
+
58
+ .pointer {
59
+ cursor: pointer;
60
+ }
61
+
62
+ .item {
63
+ width: 100%;
64
+ }
65
+
66
+ .node {
67
+ align-items: center;
68
+ display: flex;
69
+ font-family: 'Roboto', 'Noto', sans-serif;
70
+ font-size: 16px;
71
+ font-weight: 400;
72
+ height: 40px;
73
+ line-height: 24px;
74
+ padding: 6px 12px;
75
+ cursor: pointer;
76
+ }
77
+ .name {
78
+ flex: auto;
79
+ }
80
+ .icon {
81
+ box-sizing: border-box;
82
+ cursor: pointer;
83
+ display: inline-block;
84
+ height: 40px;
85
+ line-height: 1;
86
+ outline: none;
87
+ padding: 8px;
88
+ position: relative;
89
+ user-select: none;
90
+ width: 40px;
91
+ z-index: 0;
92
+ }
93
+
94
+ .node.selected {
95
+ background-color: var(
96
+ --cosmoz-listbox-active-color,
97
+ var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1))
98
+ );
99
+ transition: background-color 0.2s ease-out;
100
+ }
101
+
102
+ .node.selected .icon svg {
103
+ fill: var(--cosmoz-treenode-navigator-select-node-icon-color);
104
+ transition: color 0.8s ease-out;
105
+ }
106
+
107
+ .btn-ghost {
108
+ background: transparent;
109
+ border: none;
110
+ cursor: pointer;
111
+ margin: 0 20px;
112
+ padding: 0.7em 0.57em;
113
+ width: calc(100% - 50px);
114
+ border: solid 1px #a4abae;
115
+ border-radius: 16px;
116
+ font-size: 14px;
117
+ font-family: inherit;
118
+ }
119
+ `;
package/helpers.js CHANGED
@@ -1,93 +1,38 @@
1
- import { DefaultTree } from '@neovici/cosmoz-tree/cosmoz-default-tree';
2
- import basicTree from './test/data/basicTree';
3
- import { tagged as css } from '@neovici/cosmoz-utils';
4
-
5
- const tree = new DefaultTree(basicTree),
6
- /**
7
- * Returns true if a given node has children
8
- * @param {Object} node - The node
9
- * @return {Boolean} - True if node has children
10
- */
11
- hasChildren = node => {
12
- return tree.hasChildren(node);
13
- },
14
- /**
15
- * Returns true, if a search string is eligable to trigger a search
16
- * @param {String} value - The search string
17
- * @param {Number} searchMinLength - The minimum length of value to be valid
18
- * @return {Boolean} - If a search should be triggered
19
- */
20
- computeSearching = (value, searchMinLength) => {
21
- return value && value.length >= searchMinLength && value !== '';
22
- },
23
- /**
24
- * Normalizes and returns an Array of nodes
25
- * with the properties name, path, parentSectionName, children
26
- * @param {Array} nodes - The input nodes
27
- * @return {Array} - The normalized nodes
28
- */
29
- normalizeNodes = nodes => {
30
- if (!Array.isArray(nodes)) {
31
- return [];
32
- }
33
- return nodes.map(node => {
34
- if (!node) {
35
- return node;
36
- }
37
- const path = node.pathLocator || node.path,
38
- pathLocatorSeparator = tree.pathLocatorSeparator,
39
- parentPath = path.includes(pathLocatorSeparator) ? path.substring(0, path.lastIndexOf(pathLocatorSeparator)) : path;
40
- return {
41
- name: node[tree.searchProperty],
42
- path,
43
- children: node[tree.childProperty],
44
- parentSectionName: tree.getPathString(parentPath, tree.searchProperty),
45
- id: node.id,
46
- pathLocator: node.pathLocator
47
- };
48
- });
49
- },
50
- /**
51
- * Returns the found nodes based on a search string and a given tree to be searched
52
- * @param {Boolean} searching - If true, a search should be executed
53
- * @param {String} searchString - The search string
54
- * @param {Array} renderedLevel - The node list on which the search should be executed
55
- * @param {Tree} tree - The main tree
56
- * @return {Array} - The found nodes
57
- */
58
- computeDataPlane = (searching, searchString, renderedLevel, tree) => {
59
- if (searching && tree) {
60
- const results = tree.searchNodes(searchString, renderedLevel, false);
61
- return normalizeNodes(results);
62
- }
63
- return renderedLevel;
1
+ export const //
2
+ getParentPath = (tree, node) => {
3
+ const path = node.pathLocator || node.path,
4
+ pathLocatorSeparator = tree.pathLocatorSeparator;
5
+ return path.includes(pathLocatorSeparator)
6
+ ? path.substring(0, path.lastIndexOf(pathLocatorSeparator))
7
+ : path;
64
8
  },
65
9
  /**
66
10
  * Returns a node array with the children of a node on the given path
67
11
  * If the node doesn't have children, the node gets returned
68
- * @param {String} pathLocator - The separated address parts of a node
69
12
  * @param {Tree} tree - The main tree
13
+ * @param {String} pathLocator - The separated address parts of a node
70
14
  * @return {Array} - Nodes
71
15
  */
72
- renderLevel = (pathLocator, tree) => {
16
+ renderLevel = (tree, pathLocator) => {
73
17
  if (!tree) {
74
18
  return;
75
19
  }
76
20
  const node = tree.getNodeByPathLocator(pathLocator),
77
21
  children = tree.getChildren(node),
78
- level = tree.hasChildren(node) ? children : node,
22
+ level = children.length > 0 ? children : node,
23
+ searchProperty = tree.searchProperty,
79
24
  sortFunc = (a, b) => {
80
25
  // First sort based on "folder" status (containing children)
81
- if (hasChildren(a)) {
82
- if (!hasChildren(b)) {
26
+ if (tree.hasChildren(a)) {
27
+ if (!tree.hasChildren(b)) {
83
28
  return -1;
84
29
  }
85
- } else if (hasChildren(b)) {
30
+ } else if (tree.hasChildren(b)) {
86
31
  return 1;
87
32
  }
88
33
  // Then sort on searchProperty
89
- const val1 = a[tree.searchProperty],
90
- val2 = b[tree.searchProperty];
34
+ const val1 = a[searchProperty],
35
+ val2 = b[searchProperty];
91
36
 
92
37
  if (val1 > val2) {
93
38
  return 1;
@@ -97,8 +42,21 @@ const tree = new DefaultTree(basicTree),
97
42
  }
98
43
  return 0;
99
44
  };
100
- return normalizeNodes(level)
101
- .sort(sortFunc);
45
+ return level.sort(sortFunc);
46
+ },
47
+ /**
48
+ * Returns the found nodes based on a search string and a given tree to be searched
49
+ * @param {Tree} tree - The main tree
50
+ * @param {String|undefined} search - The search string
51
+ * @param {String} pathLocator - The separated address parts of a node
52
+ * @return {Array} - The found nodes
53
+ */
54
+ computeDataPlane = (tree, search, pathLocator) => {
55
+ if (!tree) {
56
+ return [];
57
+ }
58
+ const nodes = renderLevel(tree, pathLocator);
59
+ return search ? tree.searchNodes(search, nodes, false) : nodes;
102
60
  },
103
61
  /**
104
62
  * Returns the classes of a row based its selection state
@@ -110,9 +68,10 @@ const tree = new DefaultTree(basicTree),
110
68
  computeRowClass = (classes, node, highlightedNode) => {
111
69
  let selected = false;
112
70
  if (highlightedNode) {
113
- selected = node === highlightedNode
114
- || node.id && node.id === highlightedNode.id
115
- || node.pathLocator === highlightedNode.pathLocator;
71
+ selected =
72
+ node === highlightedNode ||
73
+ (node.id && node.id === highlightedNode.id) ||
74
+ node.pathLocator === highlightedNode.pathLocator;
116
75
  }
117
76
  return selected ? classes + ' selected' : classes;
118
77
  },
@@ -123,28 +82,13 @@ const tree = new DefaultTree(basicTree),
123
82
  * @return {undefined}
124
83
  */
125
84
  onNodeDblClicked = (event, host) => {
126
- host.dispatchEvent(new CustomEvent('node-dblclicked', {
127
- detail: {
128
- model: event.model
129
- }
130
- }));
131
- },
132
- /**
133
- * Returns the name of a given node
134
- * @param {Object} node - The node
135
- * @return {String} - The name
136
- */
137
- getNodeName = node => {
138
- return node[tree.searchProperty];
139
- },
140
- /**
141
- * Returns true, if the button should be visible
142
- * @param {Boolean} searching - If a search is currently executed
143
- * @param {String} openNodeLevelPath - The open node level
144
- * @return {Boolean} - The visibility of the button
145
- */
146
- showGlobalSearchBtn = (searching, openNodeLevelPath) => {
147
- return searching && openNodeLevelPath !== '';
85
+ host.dispatchEvent(
86
+ new CustomEvent('node-dblclicked', {
87
+ detail: {
88
+ model: event.model,
89
+ },
90
+ })
91
+ );
148
92
  },
149
93
  /**
150
94
  * Returns the nodes on a path specified by a given path locator
@@ -156,7 +100,7 @@ const tree = new DefaultTree(basicTree),
156
100
  if (!tree || !pathLocator) {
157
101
  return [];
158
102
  }
159
- return normalizeNodes(tree.getPathNodes(pathLocator));
103
+ return tree.getPathNodes(pathLocator);
160
104
  },
161
105
  /**
162
106
  * Returns a node based on a given path locator.
@@ -175,78 +119,7 @@ const tree = new DefaultTree(basicTree),
175
119
  let nodes;
176
120
 
177
121
  if (!node) {
178
- nodes = tree.getPathNodes(pathLocator).filter(n => n != null);
122
+ nodes = tree.getPathNodes(pathLocator).filter((n) => n != null);
179
123
  }
180
124
  return nodes && nodes.length > 0 ? nodes.pop() : node;
181
- },
182
- nodeStyles = css`
183
- .section {
184
- background-color: #f5f5f5;
185
- padding: 5px;
186
- }
187
-
188
- .pointer {
189
- cursor: pointer;
190
- }
191
-
192
- .item {
193
- padding: 0;
194
- }
195
-
196
- .item[data-index] {
197
- background: transparent;
198
- }
199
-
200
- .node-item {
201
- align-items: center;
202
- display: flex;
203
- font-family: 'Roboto', 'Noto', sans-serif;
204
- font-size: 16px;
205
- font-weight: 400;
206
- height: 40px;
207
- line-height: 24px;
208
- padding: 6px 12px;
209
- }
210
-
211
- .node-item-wrapper {
212
- width: 100%;
213
- }
214
-
215
- .icon {
216
- box-sizing: border-box;
217
- cursor: pointer;
218
- display: inline-block;
219
- height: 40px;
220
- line-height: 1;
221
- outline: none;
222
- padding: 8px;
223
- position: relative;
224
- user-select: none;
225
- width: 40px;
226
- z-index: 0;
227
- }
228
-
229
- .node-item.selected {
230
- background-color: var(--cosmoz-listbox-active-color, var(--cosmoz-selection-color, rgba(58, 145, 226, 0.1)));
231
- transition: background-color 0.2s ease-out;
232
- }
233
-
234
- .node-item.selected .icon svg {
235
- fill: var(--cosmoz-treenode-navigator-select-node-icon-color);
236
- transition: color 0.8s ease-out;
237
- }`;
238
-
239
- export {
240
- hasChildren,
241
- computeSearching,
242
- normalizeNodes,
243
- renderLevel,
244
- computeDataPlane,
245
- computeRowClass,
246
- onNodeDblClicked,
247
- getNodeName,
248
- showGlobalSearchBtn,
249
- getTreePathParts,
250
- getNode,
251
- nodeStyles
252
- };
125
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-treenode-navigator",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "A Polymer component that lets you navigate and search through hierarchically structured data-nodes and select one of them.",
5
5
  "keywords": [
6
6
  "polymer",
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@neovici/cosmoz-autocomplete": "^5.0.0",
57
+ "@lit-labs/virtualizer": "^0.7.1",
58
58
  "@neovici/cosmoz-dialog": "^3.0.0",
59
59
  "@neovici/cosmoz-i18next": "^3.2.0",
60
60
  "@neovici/cosmoz-input": "^3.0.0",
@@ -80,9 +80,9 @@
80
80
  "@polymer/test-fixture": "^4.0.2",
81
81
  "@semantic-release/changelog": "^6.0.0",
82
82
  "@semantic-release/git": "^10.0.0",
83
- "@webcomponents/webcomponentsjs": "^2.0.0",
84
83
  "@web/dev-server": "^0.1.25",
85
84
  "@web/test-runner": "^0.13.19",
85
+ "@webcomponents/webcomponentsjs": "^2.0.0",
86
86
  "husky": "^8.0.0",
87
87
  "semantic-release": "^19.0.3"
88
88
  }
@@ -2,18 +2,17 @@ import { assert, fixture, html } from '@open-wc/testing';
2
2
  import { DefaultTree } from '@neovici/cosmoz-tree/cosmoz-default-tree';
3
3
  import basicTree from './data/basicTree.js';
4
4
  import '../cosmoz-treenode-navigator';
5
- import { computeDataPlane, computeSearching, hasChildren, renderLevel } from '../helpers';
5
+ import { computeDataPlane } from '../helpers';
6
6
 
7
7
  suite('cosmoz-treenode-navigator', () => {
8
8
  let navigator;
9
9
 
10
10
  setup(async () => {
11
- navigator = await fixture(html`
12
- <cosmoz-treenode-navigator
13
- .tree=${ new DefaultTree(basicTree) }
14
- .searchPlaceholder="${ 'Custom search placeholder' }"
15
- .searchMinLength="${ 2 }"
16
- >`);
11
+ navigator = await fixture(html` <cosmoz-treenode-navigator
12
+ .tree=${new DefaultTree(basicTree)}
13
+ .searchPlaceholder="${'Custom search placeholder'}"
14
+ .searchMinLength="${2}"
15
+ ></cosmoz-treenode-navigator>`);
17
16
  });
18
17
 
19
18
  test('instantiating the element', () => {
@@ -27,10 +26,9 @@ suite('cosmoz-treenode-navigator', () => {
27
26
  navigator.openNodePath = '1000';
28
27
 
29
28
  const dataPlane = computeDataPlane(
30
- computeSearching(navigator.searchValue, navigator.searchMinLength),
29
+ navigator.tree,
31
30
  navigator.searchValue,
32
- renderLevel(navigator.openNodePath, navigator.tree),
33
- navigator.tree
31
+ navigator.openNodePath
34
32
  );
35
33
  // then I can see the contents of the node
36
34
  // and it is ordered folder-first, then alphabetically
@@ -41,28 +39,35 @@ suite('cosmoz-treenode-navigator', () => {
41
39
 
42
40
  test('set proper search placeholder', () => {
43
41
  assert.equal(
44
- navigator.shadowRoot.querySelector('cosmoz-input')
45
- .shadowRoot
46
- .querySelector('input').placeholder,
47
- 'Custom search placeholder');
48
- });
49
-
50
- test('compute search if value is bigger than searchMinLength', () => {
51
- assert.equal(computeSearching('long string', navigator.searchMinLength), true);
52
- assert.equal(computeSearching('a', navigator.searchMinLength), false);
42
+ navigator.shadowRoot
43
+ .querySelector('cosmoz-input')
44
+ .shadowRoot.querySelector('input').placeholder,
45
+ 'Custom search placeholder'
46
+ );
53
47
  });
54
48
 
55
49
  test('should check if node has children', () => {
56
50
  const node = {
57
51
  name: 'D:',
58
52
  path: '1000',
59
- children: { 1001: { name: 'Data', pathLocator: '1000.1001', children: { 1002: { name: 'John', pathLocator: '1000.1001.1002' }}}},
53
+ children: {
54
+ 1001: {
55
+ name: 'Data',
56
+ pathLocator: '1000.1001',
57
+ children: { 1002: { name: 'John', pathLocator: '1000.1001.1002' } },
58
+ },
59
+ },
60
60
  parentSectionName: 'D:',
61
- pathLocator: '1000'
61
+ pathLocator: '1000',
62
62
  };
63
- assert.equal(hasChildren(node), true);
64
- const node2 = { name: 'Git', path: '1.5.7', parentSectionName: 'C:/Program Files', pathLocator: '1.5.7' };
65
- assert.equal(hasChildren(node2), false);
63
+ assert.equal(navigator.tree.hasChildren(node), true);
64
+ const node2 = {
65
+ name: 'Git',
66
+ path: '1.5.7',
67
+ parentSectionName: 'C:/Program Files',
68
+ pathLocator: '1.5.7',
69
+ };
70
+ assert.equal(navigator.tree.hasChildren(node2), false);
66
71
  });
67
72
 
68
73
  test('match search string', () => {
@@ -71,16 +76,14 @@ suite('cosmoz-treenode-navigator', () => {
71
76
  navigator.searchValue = 'John';
72
77
 
73
78
  const dataPlane = computeDataPlane(
74
- computeSearching(navigator.searchValue, navigator.searchMinLength),
79
+ navigator.tree,
75
80
  navigator.searchValue,
76
- renderLevel('1000', navigator.tree),
77
- navigator.tree
81
+ '1000'
78
82
  );
79
83
 
80
84
  //then I can see only 1 item matching 'John'
81
85
  assert.lengthOf(dataPlane, 1);
82
86
  assert.equal(dataPlane[0].name, 'John');
83
-
84
87
  });
85
88
 
86
89
  test('not match search string', () => {
@@ -88,10 +91,9 @@ suite('cosmoz-treenode-navigator', () => {
88
91
  // when I search for 'NONEXISTING'
89
92
  navigator.searchValue = 'NONEXISTING';
90
93
  const dataPlane = computeDataPlane(
91
- computeSearching(navigator.searchValue, navigator.searchMinLength),
94
+ navigator.tree,
92
95
  navigator.searchValue,
93
- renderLevel('1000', navigator.tree),
94
- navigator.tree
96
+ '1000'
95
97
  );
96
98
 
97
99
  //then I can see that is no contents