@neovici/cosmoz-treenode-navigator 5.1.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  # [5.1.0](https://github.com/neovici/cosmoz-treenode-navigator/compare/v5.0.1...v5.1.0) (2022-07-28)
2
9
 
3
10
 
@@ -1,19 +1,17 @@
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
+ import { guard } from 'lit-html/directives/guard.js';
4
5
  import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
5
6
  import '@neovici/cosmoz-input';
6
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
- normalizeNodes,
14
14
  onNodeDblClicked,
15
- renderLevel,
16
- showGlobalSearchBtn,
17
15
  } from './helpers';
18
16
  import styles from './cosmoz-treenode-navigator.styles';
19
17
 
@@ -42,19 +40,6 @@ const TreenodeNavigator = (host) => {
42
40
  * The path of the opened node
43
41
  */
44
42
  [openNodePath, setOpenNodePath] = useState(''),
45
- /**
46
- * The nodes on the path of the opened node
47
- */
48
- nodesOnOpenNodePath = useMemo(
49
- () =>
50
- openNodePath
51
- ? normalizeNodes(
52
- tree,
53
- tree.getPathNodes(openNodePath).filter((item) => item)
54
- )
55
- : [],
56
- [tree, openNodePath]
57
- ),
58
43
  /**
59
44
  * The highlighted (focused) node
60
45
  * This is the node which is currently selected in the list
@@ -64,20 +49,16 @@ const TreenodeNavigator = (host) => {
64
49
  * The search string
65
50
  */
66
51
  [searchValue, setSearchValue] = useState(''),
67
- isSearching = useMemo(
68
- () => computeSearching(searchValue, searchMinLength),
52
+ search = useMemo(
53
+ () => (searchValue?.length > searchMinLength && searchValue) || undefined,
69
54
  [searchValue, searchMinLength]
70
55
  ),
71
- renderedLevel = useMemo(
72
- () => renderLevel(openNodePath, tree),
73
- [openNodePath, tree]
74
- ),
75
56
  /**
76
57
  * The currently displayed node list
77
58
  */
78
59
  dataPlane = useMemo(
79
- () => computeDataPlane(isSearching, searchValue, renderedLevel, tree),
80
- [isSearching, searchValue, renderedLevel, tree]
60
+ () => computeDataPlane(tree, search, openNodePath),
61
+ [tree, search, openNodePath]
81
62
  ),
82
63
  /**
83
64
  * Opens a node (renderLevel) based on a given path
@@ -86,42 +67,11 @@ const TreenodeNavigator = (host) => {
86
67
  */
87
68
  openNode = useCallback(
88
69
  (clickedNode) => {
89
- if (tree.hasChildren(clickedNode)) {
90
- setOpenNodePath(clickedNode.path);
91
- setSearchValue('');
92
- }
93
- if (!clickedNode) {
94
- setOpenNodePath('');
95
- setSearchValue('');
96
- setHighlightedNode(null);
97
- }
70
+ setOpenNodePath(clickedNode?.pathLocator || '');
71
+ setSearchValue('');
72
+ setHighlightedNode();
98
73
  },
99
74
  [tree]
100
- ),
101
- /**
102
- * Returns true, if the path of a node should be visible in the view
103
- * @param {Number} index - The node's current index in the list
104
- * @param {Object} parentSectionName - The nodes current parent section name
105
- * @return {Boolean} - If the path should be visible
106
- */
107
- renderSection = useCallback(
108
- (index, parentSectionName) => {
109
- if (
110
- !isSearching ||
111
- index == null ||
112
- dataPlane == null ||
113
- index >= dataPlane.length ||
114
- parentSectionName == null
115
- ) {
116
- return false;
117
- }
118
- if (index === 0) {
119
- return true;
120
- }
121
- const prevItem = dataPlane[index - 1];
122
- return prevItem.parentSectionName !== parentSectionName;
123
- },
124
- [isSearching, dataPlane]
125
75
  );
126
76
 
127
77
  useEffect(() => {
@@ -133,31 +83,35 @@ const TreenodeNavigator = (host) => {
133
83
 
134
84
  useEffect(() => {
135
85
  if (nodePath !== undefined) {
136
- host.dispatchEvent(
137
- new CustomEvent('select-node', {
138
- detail: {},
139
- })
140
- );
86
+ host.dispatchEvent(new CustomEvent('select-node'));
141
87
  }
142
88
  }, [nodePath]);
143
89
 
144
90
  useEffect(() => {
145
- notifyProperty(host, 'highlightedNodePath', highlightedNode?.path || '');
91
+ notifyProperty(
92
+ host,
93
+ 'highlightedNodePath',
94
+ highlightedNode?.pathLocator || ''
95
+ );
146
96
  }, [highlightedNode]);
147
97
 
148
98
  const meta = useMeta({ dataPlane, highlightedNode, openNode });
149
99
 
150
100
  useEffect(() => {
101
+ // eslint-disable-next-line max-lines-per-function
151
102
  const handler = (e) => {
152
103
  if ((e.ctrlKey && e.altKey) || e.defaultPrevented) {
153
104
  return;
154
105
  }
155
- const { dataPlane: items, highlightedNode: node, openNode: open } = meta;
106
+ const { dataPlane: items, highlightedNode: node, openNode: open } = meta,
107
+ getIndex = () =>
108
+ items.findIndex((i) => i.pathLocator === node?.pathLocator);
109
+
156
110
  switch (e.key) {
157
111
  case 'Up':
158
112
  case 'ArrowUp':
159
113
  {
160
- const idx = items.findIndex((i) => i.path === node?.path);
114
+ const idx = getIndex();
161
115
  e.preventDefault();
162
116
  setHighlightedNode(items[Math.max(idx - 1, 0)]);
163
117
  }
@@ -165,7 +119,7 @@ const TreenodeNavigator = (host) => {
165
119
  case 'Down':
166
120
  case 'ArrowDown':
167
121
  {
168
- const idx = items.findIndex((i) => i.path === node?.path);
122
+ const idx = getIndex();
169
123
  if (idx < items.length - 1) {
170
124
  e.preventDefault();
171
125
  setHighlightedNode(items[idx + 1]);
@@ -173,9 +127,15 @@ const TreenodeNavigator = (host) => {
173
127
  }
174
128
  break;
175
129
 
130
+ case 'Enter':
131
+ if (node) {
132
+ e.preventDefault();
133
+ host.dispatchEvent(new CustomEvent('select-node'));
134
+ }
135
+ break;
136
+
176
137
  case 'Right':
177
138
  case 'ArrowRight':
178
- case 'Enter':
179
139
  if (node) {
180
140
  e.preventDefault();
181
141
  open(node);
@@ -191,16 +151,24 @@ const TreenodeNavigator = (host) => {
191
151
  }, [meta]);
192
152
 
193
153
  const renderItem = (node, index) => html` <div class="item">
194
- ${when(
195
- renderSection(index, node.parentSectionName),
196
- () => html` <div class="section">${node.parentSectionName}</div> `
197
- )}
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))}
198
166
  <div
199
167
  class=${computeRowClass('node', node, highlightedNode)}
200
168
  @click=${() => setHighlightedNode(node)}
201
169
  @dblclick=${(e) => onNodeDblClicked(e, host)}
202
170
  >
203
- <div class="name">${node.name}</div>
171
+ <div class="name">${node[tree.searchProperty]}</div>
204
172
  ${when(
205
173
  tree.hasChildren(node),
206
174
  () => html`
@@ -239,12 +207,16 @@ const TreenodeNavigator = (host) => {
239
207
  <g><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"></path></g>
240
208
  </svg>
241
209
  </span>
242
- ${nodesOnOpenNodePath.map((node) => {
243
- return html` <span class="slash">/</span>
244
- <span class="pointer" tabindex="0" @click=${() => openNode(node)}
245
- >${node[tree.searchProperty]}</span
246
- >`;
247
- })}
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
+ )}
248
220
  </h3>
249
221
  <cosmoz-input
250
222
  tabindex="0"
@@ -261,7 +233,7 @@ const TreenodeNavigator = (host) => {
261
233
  </div>`
262
234
  )}
263
235
  ${when(
264
- showGlobalSearchBtn(isSearching, openNodePath),
236
+ search && openNodePath,
265
237
  () => html`
266
238
  <button class="btn-ghost" @click=${() => setOpenNodePath('')}>
267
239
  ${searchGlobalPlaceholder}
package/helpers.js CHANGED
@@ -1,72 +1,26 @@
1
1
  export const //
2
- /**
3
- * Returns true, if a search string is eligable to trigger a search
4
- * @param {String} value - The search string
5
- * @param {Number} searchMinLength - The minimum length of value to be valid
6
- * @return {Boolean} - If a search should be triggered
7
- */
8
- computeSearching = (value, searchMinLength) => {
9
- return value && value.length >= searchMinLength && value !== '';
10
- },
11
- /**
12
- * Normalizes and returns an Array of nodes
13
- * with the properties name, path, parentSectionName, children
14
- * @param {Object} tree - The cosmoz tree
15
- * @param {Array} nodes - The input nodes
16
- * @return {Array} - The normalized nodes
17
- */
18
- normalizeNodes = (tree, nodes) => {
19
- if (!Array.isArray(nodes)) {
20
- return [];
21
- }
22
- return nodes.map((node) => {
23
- if (!node) {
24
- return node;
25
- }
26
- const path = node.pathLocator || node.path,
27
- pathLocatorSeparator = tree.pathLocatorSeparator,
28
- parentPath = path.includes(pathLocatorSeparator)
29
- ? path.substring(0, path.lastIndexOf(pathLocatorSeparator))
30
- : path;
31
- return {
32
- name: node[tree.searchProperty],
33
- path,
34
- children: node[tree.childProperty],
35
- parentSectionName: tree.getPathString(parentPath, tree.searchProperty),
36
- id: node.id,
37
- pathLocator: node.pathLocator,
38
- };
39
- });
40
- },
41
- /**
42
- * Returns the found nodes based on a search string and a given tree to be searched
43
- * @param {Boolean} searching - If true, a search should be executed
44
- * @param {String} searchString - The search string
45
- * @param {Array} renderedLevel - The node list on which the search should be executed
46
- * @param {Tree} tree - The main tree
47
- * @return {Array} - The found nodes
48
- */
49
- computeDataPlane = (searching, searchString, renderedLevel, tree) => {
50
- if (searching && tree) {
51
- const results = tree.searchNodes(searchString, renderedLevel, false);
52
- return normalizeNodes(tree, results);
53
- }
54
- return renderedLevel;
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;
55
8
  },
56
9
  /**
57
10
  * Returns a node array with the children of a node on the given path
58
11
  * If the node doesn't have children, the node gets returned
59
- * @param {String} pathLocator - The separated address parts of a node
60
12
  * @param {Tree} tree - The main tree
13
+ * @param {String} pathLocator - The separated address parts of a node
61
14
  * @return {Array} - Nodes
62
15
  */
63
- renderLevel = (pathLocator, tree) => {
16
+ renderLevel = (tree, pathLocator) => {
64
17
  if (!tree) {
65
18
  return;
66
19
  }
67
20
  const node = tree.getNodeByPathLocator(pathLocator),
68
21
  children = tree.getChildren(node),
69
- level = tree.hasChildren(node) ? children : node,
22
+ level = children.length > 0 ? children : node,
23
+ searchProperty = tree.searchProperty,
70
24
  sortFunc = (a, b) => {
71
25
  // First sort based on "folder" status (containing children)
72
26
  if (tree.hasChildren(a)) {
@@ -77,8 +31,8 @@ export const //
77
31
  return 1;
78
32
  }
79
33
  // Then sort on searchProperty
80
- const val1 = a[tree.searchProperty],
81
- val2 = b[tree.searchProperty];
34
+ const val1 = a[searchProperty],
35
+ val2 = b[searchProperty];
82
36
 
83
37
  if (val1 > val2) {
84
38
  return 1;
@@ -88,7 +42,21 @@ export const //
88
42
  }
89
43
  return 0;
90
44
  };
91
- return normalizeNodes(tree, level).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;
92
60
  },
93
61
  /**
94
62
  * Returns the classes of a row based its selection state
@@ -122,15 +90,6 @@ export const //
122
90
  })
123
91
  );
124
92
  },
125
- /**
126
- * Returns true, if the button should be visible
127
- * @param {Boolean} searching - If a search is currently executed
128
- * @param {String} openNodeLevelPath - The open node level
129
- * @return {Boolean} - The visibility of the button
130
- */
131
- showGlobalSearchBtn = (searching, openNodeLevelPath) => {
132
- return searching && openNodeLevelPath !== '';
133
- },
134
93
  /**
135
94
  * Returns the nodes on a path specified by a given path locator
136
95
  * @param {String} pathLocator - The separated address parts of a node
@@ -141,7 +100,7 @@ export const //
141
100
  if (!tree || !pathLocator) {
142
101
  return [];
143
102
  }
144
- return normalizeNodes(tree, tree.getPathNodes(pathLocator));
103
+ return tree.getPathNodes(pathLocator);
145
104
  },
146
105
  /**
147
106
  * Returns a node based on a given path locator.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-treenode-navigator",
3
- "version": "5.1.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",
@@ -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, 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,27 +39,34 @@ 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
63
  assert.equal(navigator.tree.hasChildren(node), true);
64
- const node2 = { name: 'Git', path: '1.5.7', parentSectionName: 'C:/Program Files', pathLocator: '1.5.7' };
64
+ const node2 = {
65
+ name: 'Git',
66
+ path: '1.5.7',
67
+ parentSectionName: 'C:/Program Files',
68
+ pathLocator: '1.5.7',
69
+ };
65
70
  assert.equal(navigator.tree.hasChildren(node2), false);
66
71
  });
67
72
 
@@ -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