@neovici/cosmoz-treenode-navigator 5.0.1 → 5.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.
@@ -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,38 @@
1
+ # [5.3.0](https://github.com/neovici/cosmoz-treenode-navigator/compare/v5.2.0...v5.3.0) (2022-08-01)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * close on enter ([e7cd4d1](https://github.com/neovici/cosmoz-treenode-navigator/commit/e7cd4d191abdf1b9486a5d097154f530130d4a96))
7
+ * **cosmoz-treenode-button-view:** pass opened to treenode ([4f43ade](https://github.com/neovici/cosmoz-treenode-navigator/commit/4f43ade0844fe5168cf84f6f7833dc61a608e176))
8
+ * **renderLevel:** handle leaf nodes ([760f6bf](https://github.com/neovici/cosmoz-treenode-navigator/commit/760f6bf4888ccbec0184ca72ea21ab4781e66f46))
9
+
10
+
11
+ ### Features
12
+
13
+ * **treenode-navigator:** autoscroll to highlighed node ([5c55a38](https://github.com/neovici/cosmoz-treenode-navigator/commit/5c55a38ad406b3d711405b8173c388fb26d5eb6c))
14
+
15
+ # [5.2.0](https://github.com/neovici/cosmoz-treenode-navigator/compare/v5.1.0...v5.2.0) (2022-08-01)
16
+
17
+
18
+ ### Features
19
+
20
+ * **cosmoz-treenode-navigator:** improve performance ([ac12d74](https://github.com/neovici/cosmoz-treenode-navigator/commit/ac12d74345501e34cdccde6962563a24d6efed4f))
21
+
22
+ # [5.1.0](https://github.com/neovici/cosmoz-treenode-navigator/compare/v5.0.1...v5.1.0) (2022-07-28)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **ci:** worflow typo ([17b92aa](https://github.com/neovici/cosmoz-treenode-navigator/commit/17b92aa332f0411b6548729002fd9cf62a99d223))
28
+
29
+
30
+ ### Features
31
+
32
+ * **cosmoz-treenode-navigator:** add keyboard navigation ([26b6d42](https://github.com/neovici/cosmoz-treenode-navigator/commit/26b6d42d99737d0b48a7fbc21a8677117b1d8a9e))
33
+ * refactor with lit-virtualizer instead of listbox ([7b69ab4](https://github.com/neovici/cosmoz-treenode-navigator/commit/7b69ab4e0e049c9c7f8e03c3b781f02fab83eb18))
34
+ * support `Right` key ([28a4df6](https://github.com/neovici/cosmoz-treenode-navigator/commit/28a4df6d35bc62ac49aba81135891ce8fcacfa05))
35
+
1
36
  ## [5.0.1](https://github.com/neovici/cosmoz-treenode-navigator/compare/v5.0.0...v5.0.1) (2022-07-27)
2
37
 
3
38
 
@@ -127,7 +127,8 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
127
127
  <cosmoz-dialog
128
128
  id="dialogTree"
129
129
  class="treeDialog"
130
- on-iron-overlay-opened="focusSearch"
130
+ on-iron-overlay-opened="onOpened"
131
+ on-iron-overlay-closed="onClosed"
131
132
  modal
132
133
  prerender
133
134
  >
@@ -147,6 +148,7 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
147
148
  nodes-on-node-path="{{ nodesOnNodePath }}"
148
149
  on-node-dblclicked="_selectNodeAndCloseDialog"
149
150
  on-select-node="selectNode"
151
+ opened="[[ opened ]]"
150
152
  >
151
153
  <slot></slot>
152
154
  </cosmoz-treenode-navigator>
@@ -174,13 +176,13 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
174
176
  return {
175
177
  multiSelection: {
176
178
  type: Boolean,
177
- value: false
179
+ value: false,
178
180
  },
179
181
  /*
180
182
  * The main node structure
181
183
  */
182
184
  tree: {
183
- type: Tree
185
+ type: Tree,
184
186
  },
185
187
  /*
186
188
  * Currently selected node object
@@ -190,7 +192,7 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
190
192
  value() {
191
193
  return {};
192
194
  },
193
- notify: true
195
+ notify: true,
194
196
  },
195
197
  /**
196
198
  * Selected nodes
@@ -198,32 +200,40 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
198
200
  selectedNodes: {
199
201
  type: Array,
200
202
  notify: true,
201
- value: () => []
203
+ value: () => [],
202
204
  },
203
205
  /**
204
206
  * If true, reset button gets hidden
205
207
  */
206
208
  noReset: {
207
209
  type: Boolean,
208
- value: false
210
+ value: false,
211
+ },
212
+ /**
213
+ * If true opened
214
+ */
215
+ opened: {
216
+ type: Boolean,
217
+ value: false,
209
218
  },
219
+
210
220
  /*
211
221
  * Placeholder for the search field
212
222
  */
213
223
  searchPlaceholder: {
214
- type: String
224
+ type: String,
215
225
  },
216
226
  /*
217
227
  * Placeholder for button text
218
228
  */
219
229
  buttonTextPlaceholder: {
220
230
  type: String,
221
- computed: 'getButtonTextPlaceholder(multiSelection)'
231
+ computed: 'getButtonTextPlaceholder(multiSelection)',
222
232
  },
223
233
 
224
234
  buttonText: {
225
235
  type: String,
226
- computed: '_getButtonLabel(nodesOnNodePath, buttonTextPlaceholder)'
236
+ computed: '_getButtonLabel(nodesOnNodePath, buttonTextPlaceholder)',
227
237
  },
228
238
 
229
239
  /*
@@ -231,13 +241,13 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
231
241
  */
232
242
  nodePath: {
233
243
  type: String,
234
- notify: true
244
+ notify: true,
235
245
  },
236
246
  /*
237
247
  * The nodes on the path of the selected node
238
248
  */
239
249
  nodesOnNodePath: {
240
- type: Array
250
+ type: Array,
241
251
  },
242
252
  /*
243
253
  * Text displayed when local search has finished
@@ -245,14 +255,14 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
245
255
  */
246
256
  searchGlobalPlaceholder: {
247
257
  type: String,
248
- value: 'Click to search again but globally.'
258
+ value: 'Click to search again but globally.',
249
259
  },
250
260
  /*
251
261
  * Settable text for dialog title.
252
262
  */
253
263
  dialogText: {
254
264
  type: String,
255
- value: 'Search or navigate to chosen destination'
265
+ value: 'Search or navigate to chosen destination',
256
266
  },
257
267
  /*
258
268
  * Minimum length before an search
@@ -260,14 +270,14 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
260
270
  */
261
271
  searchMinLength: {
262
272
  type: Number,
263
- value: 1
273
+ value: 1,
264
274
  },
265
275
  /*
266
276
  * Path string of highlighted (focused) node
267
277
  */
268
278
  highlightedNodePath: {
269
- type: String
270
- }
279
+ type: String,
280
+ },
271
281
  };
272
282
  }
273
283
  /**
@@ -324,8 +334,8 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
324
334
  return placeholder;
325
335
  }
326
336
  return pathParts
327
- .filter(n => n)
328
- .map(part => part[this.tree.searchProperty])
337
+ .filter((n) => n)
338
+ .map((part) => part[this.tree.searchProperty])
329
339
  .join(' / ');
330
340
  }
331
341
  /**
@@ -348,7 +358,10 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
348
358
  * @returns {void}
349
359
  */
350
360
  focusSearch() {
351
- this.$.dialogTree.paperDialog.querySelector('#treeNavigator').focus();
361
+ this.$.dialogTree.paperDialog
362
+ .querySelector('#treeNavigator')
363
+ .shadowRoot.querySelector('cosmoz-input')
364
+ .focus();
352
365
  }
353
366
  /**
354
367
  * Reset the component to make it ready for reuse
@@ -383,7 +396,7 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
383
396
  if (this.multiSelection) {
384
397
  if (
385
398
  !this.selectedNodes.some(
386
- node => node.pathLocator === this.highlightedNodePath
399
+ (node) => node.pathLocator === this.highlightedNodePath
387
400
  )
388
401
  ) {
389
402
  this.push('selectedNodes', this.selectedNode);
@@ -423,5 +436,13 @@ class CosmozTreenodeButtonView extends translatable(PolymerElement) {
423
436
  }
424
437
  );
425
438
  }
439
+
440
+ onOpened() {
441
+ this.opened = true;
442
+ this.focusSearch();
443
+ }
444
+ onClosed() {
445
+ this.opened = false;
446
+ }
426
447
  }
427
448
  customElements.define(CosmozTreenodeButtonView.is, CosmozTreenodeButtonView);
@@ -1,25 +1,30 @@
1
- import { component, useEffect, useState, useMemo, useCallback } from 'haunted';
1
+ import {
2
+ component,
3
+ useEffect,
4
+ useState,
5
+ useMemo,
6
+ useCallback,
7
+ useRef,
8
+ } from 'haunted';
2
9
  import { html } from 'lit-html';
3
10
  import { when } from 'lit-html/directives/when.js';
11
+ import { guard } from 'lit-html/directives/guard.js';
12
+ import { ref } from 'lit-html/directives/ref.js';
13
+ import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
4
14
 
5
15
  import '@neovici/cosmoz-input';
6
- import '@neovici/cosmoz-autocomplete';
16
+ import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
7
17
  import { notifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
8
18
 
9
19
  import {
20
+ getParentPath,
10
21
  computeDataPlane,
11
22
  computeRowClass,
12
- computeSearching,
13
- getNodeName,
14
- hasChildren,
15
- nodeStyles,
16
- normalizeNodes,
17
23
  onNodeDblClicked,
18
- renderLevel,
19
- showGlobalSearchBtn
20
24
  } from './helpers';
25
+ import styles from './cosmoz-treenode-navigator.styles';
21
26
 
22
- const TreenodeNavigator = host => {
27
+ const TreenodeNavigator = (host) => {
23
28
  const {
24
29
  /**
25
30
  * The main node structure
@@ -38,16 +43,13 @@ const TreenodeNavigator = host => {
38
43
  * Minimum length of searchValue to trigger a search
39
44
  */
40
45
  searchMinLength,
41
- nodePath
46
+ nodePath,
47
+ opened,
42
48
  } = host,
43
49
  /**
44
50
  * The path of the opened node
45
51
  */
46
52
  [openNodePath, setOpenNodePath] = useState(''),
47
- /**
48
- * The nodes on the path of the opened node
49
- */
50
- [nodesOnOpenNodePath, setNodesOnOpenNodePath] = useState([]),
51
53
  /**
52
54
  * The highlighted (focused) node
53
55
  * This is the node which is currently selected in the list
@@ -57,90 +59,42 @@ const TreenodeNavigator = host => {
57
59
  * The search string
58
60
  */
59
61
  [searchValue, setSearchValue] = useState(''),
62
+ search = useMemo(
63
+ () => (searchValue?.length > searchMinLength && searchValue) || undefined,
64
+ [searchValue, searchMinLength]
65
+ ),
60
66
  /**
61
67
  * The currently displayed node list
62
68
  */
63
- dataPlane = useMemo(() => {
64
- return computeDataPlane(
65
- computeSearching(searchValue, searchMinLength),
66
- searchValue,
67
- renderLevel(openNodePath, tree),
68
- tree
69
- );
70
- }, [tree, openNodePath, highlightedNode, searchValue]),
69
+ dataPlane = useMemo(
70
+ () => computeDataPlane(tree, search, openNodePath),
71
+ [tree, search, openNodePath]
72
+ ),
71
73
  /**
72
74
  * Opens a node (renderLevel) based on a given path
73
75
  * @param {object} clickedNode - The clicked node
74
76
  * @return {undefined}
75
77
  */
76
- openNode = useCallback(clickedNode => {
77
- if (hasChildren(clickedNode)) {
78
- setOpenNodePath(clickedNode.path);
78
+ openNode = useCallback(
79
+ (clickedNode) => {
80
+ setOpenNodePath(clickedNode?.pathLocator || '');
79
81
  setSearchValue('');
80
- }
81
- if (!clickedNode) {
82
- setOpenNodePath('');
83
- 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
- };
82
+ setHighlightedNode();
83
+ },
84
+ [tree]
85
+ ),
86
+ listRef = useRef();
115
87
 
116
88
  useEffect(() => {
117
89
  if (!openNodePath) {
118
90
  notifyProperty(host, 'highlightedNodePath', '');
119
91
  notifyProperty(host, 'nodesOnNodePath', []);
120
- return;
121
- }
122
- setNodesOnOpenNodePath(
123
- normalizeNodes(tree.getPathNodes(openNodePath).filter(item => item))
124
- );
125
- }, [openNodePath]);
126
-
127
- useEffect(() => {
128
- if (!openNodePath) {
129
- notifyProperty(host, 'highlightedNodePath', '');
130
- return;
131
92
  }
132
- setNodesOnOpenNodePath(
133
- normalizeNodes(tree.getPathNodes(openNodePath).filter(item => item))
134
- );
135
93
  }, [openNodePath]);
136
94
 
137
95
  useEffect(() => {
138
96
  if (nodePath !== undefined) {
139
- host.dispatchEvent(
140
- new CustomEvent('select-node', {
141
- detail: {}
142
- })
143
- );
97
+ host.dispatchEvent(new CustomEvent('select-node'));
144
98
  }
145
99
  }, [nodePath]);
146
100
 
@@ -148,77 +102,143 @@ const TreenodeNavigator = host => {
148
102
  notifyProperty(
149
103
  host,
150
104
  'highlightedNodePath',
151
- !highlightedNode ? '' : highlightedNode.path
105
+ highlightedNode?.pathLocator || ''
152
106
  );
153
107
  }, [highlightedNode]);
154
108
 
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
- }
109
+ const meta = useMeta({ dataPlane, highlightedNode, openNode });
164
110
 
165
- #header {
166
- margin: 0 16px;
167
- }
111
+ useEffect(() => {
112
+ if (!opened) {
113
+ return;
114
+ }
168
115
 
169
- #header a {
170
- text-decoration: none;
171
- color: inherit;
172
- }
116
+ const getVlist = () => {
117
+ const list = listRef.current,
118
+ vlist = list[Object.getOwnPropertySymbols(list)[0]];
119
+ return vlist;
120
+ },
121
+ // eslint-disable-next-line max-lines-per-function, max-statements
122
+ handler = (e) => {
123
+ if ((e.ctrlKey && e.altKey) || e.defaultPrevented) {
124
+ return;
125
+ }
126
+ const {
127
+ dataPlane: items,
128
+ highlightedNode: node,
129
+ openNode: open,
130
+ } = meta,
131
+ vlist = getVlist(),
132
+ getIndex = () =>
133
+ items.findIndex((i) => i.pathLocator === node?.pathLocator);
173
134
 
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
- }
135
+ switch (e.key) {
136
+ case 'Up':
137
+ case 'ArrowUp':
138
+ {
139
+ const idx = getIndex(),
140
+ nidx = Math.max(idx - 1, 0);
141
+ e.preventDefault();
142
+ setHighlightedNode(items[nidx]);
143
+ if (nidx < vlist._firstVisible) {
144
+ vlist.scrollToIndex = { index: nidx, position: 'start' };
145
+ }
146
+ }
147
+ break;
148
+ case 'Down':
149
+ case 'ArrowDown':
150
+ {
151
+ const idx = getIndex();
152
+ if (idx < items.length - 1) {
153
+ const nidx = idx + 1;
154
+ e.preventDefault();
155
+ setHighlightedNode(items[nidx]);
156
+ if (nidx > vlist._lastVisible) {
157
+ vlist.scrollToIndex = { index: nidx, position: 'end' };
158
+ }
159
+ }
160
+ }
161
+ break;
187
162
 
188
- .path {
189
- display: flex;
190
- align-items: center;
191
- flex-wrap: wrap;
192
- }
163
+ case 'Enter':
164
+ if (node) {
165
+ e.preventDefault();
166
+ host.dispatchEvent(new CustomEvent('node-dblclicked'));
167
+ }
168
+ break;
193
169
 
194
- .slash {
195
- margin: 0 2px;
196
- }
170
+ case 'Right':
171
+ case 'ArrowRight':
172
+ if (node && tree.hasChildren(node)) {
173
+ e.preventDefault();
174
+ open(node);
175
+ }
176
+ break;
177
+ default:
178
+ break;
179
+ }
180
+ };
197
181
 
198
- .pointer {
199
- cursor: pointer;
200
- }
182
+ // autoscroll on open
183
+ (() => {
184
+ getVlist().scrollToIndex = {
185
+ index: meta.dataPlane?.indexOf(meta.highlightedNode),
186
+ position: 'center',
187
+ };
188
+ })();
201
189
 
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
- }
190
+ document.addEventListener('keydown', handler, true);
191
+ return () => document.removeEventListener('keydown', handler, true);
192
+ }, [opened, meta]);
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
+ const renderItem = (node, index) => html` <div class="item">
195
+ ${((parentPath) =>
196
+ when(
197
+ search &&
198
+ (index === 0 ||
199
+ parentPath !== getParentPath(tree, dataPlane[index - 1])),
200
+ () =>
201
+ html`
202
+ <div class="section">
203
+ ${tree.getPathString(parentPath, tree.searchProperty)}
204
+ </div>
205
+ `
206
+ ))(getParentPath(tree, node))}
207
+ <div
208
+ class=${computeRowClass('node', node, highlightedNode)}
209
+ @click=${() => setHighlightedNode(node)}
210
+ @dblclick=${(e) => onNodeDblClicked(e, host)}
211
+ >
212
+ <div class="name">${node[tree.searchProperty]}</div>
213
+ ${when(
214
+ tree.hasChildren(node),
215
+ () => html`
216
+ <span class="icon" @click=${() => openNode(node)}>
217
+ <svg
218
+ viewBox="0 0 24 24"
219
+ preserveAspectRatio="xMidYMid meet"
220
+ focusable="false"
221
+ style="pointer-events: none; display: block; width: 100%; height: 100%;"
222
+ >
223
+ <g>
224
+ <path
225
+ d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
226
+ ></path>
227
+ </g>
228
+ </svg>
229
+ </span>
230
+ `
231
+ )}
232
+ </div>
233
+ </div>`;
234
+
235
+ return html`
236
+ <style>
237
+ ${styles}
218
238
  </style>
219
- <div id="header">
239
+ <div class="header">
220
240
  <h3 class="path">
221
- <span class="icon" @click=${ () => openNode() }>
241
+ <span class="icon" @click=${() => openNode()}>
222
242
  <svg
223
243
  viewBox="0 0 24 24"
224
244
  preserveAspectRatio="xMidYMid meet"
@@ -228,83 +248,43 @@ const TreenodeNavigator = host => {
228
248
  <g><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"></path></g>
229
249
  </svg>
230
250
  </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
- }) }
251
+ ${guard([tree, openNodePath], () =>
252
+ ((openNodePath && tree.getPathNodes(openNodePath)) || []).map(
253
+ (node) => html`
254
+ <span class="slash">/</span>
255
+ <span class="pointer" tabindex="0" @click=${() => openNode(node)}
256
+ >${node[tree.searchProperty]}</span
257
+ >
258
+ `
259
+ )
260
+ )}
237
261
  </h3>
238
262
  <cosmoz-input
239
263
  tabindex="0"
240
- .value=${ searchValue }
241
- .placeholder="${ searchPlaceholder }"
242
- @input="${ ({ target }) => setSearchValue(target.value) }"
264
+ .value=${searchValue}
265
+ .placeholder=${searchPlaceholder}
266
+ @input=${(e) => setSearchValue(e.target.value)}
243
267
  />
244
268
  </div>
245
- ${ when(
246
- tree,
247
- () => html`
248
- <cosmoz-listbox
249
- .query="${ searchValue }"
250
- .items="${ dataPlane }"
251
- .textual="${ item => item.name }"
252
- .itemRenderer=${ (node, index) => html`
253
- <div class="node-item-wrapper">
254
- <style>
255
- ${ nodeStyles }
256
- </style>
257
- ${ when(
258
- renderSection(index, node.parentSectionName),
259
- () => html`
260
- <div class="section">${ node.parentSectionName }</div>
261
- `
262
- ) }
263
- <div
264
- class="${ computeRowClass(
265
- 'node-item pointer',
266
- node,
267
- highlightedNode
268
- ) }"
269
- @click="${ () => setHighlightedNode(node) }"
270
- @dblclick="${ e => onNodeDblClicked(e, host) }"
271
- >
272
- <div style="flex: auto">${ node.name }</div>
273
- ${ when(
274
- hasChildren(node),
275
- () => html`
276
- <span class="icon" @click="${ () => openNode(node) }">
277
- <svg
278
- viewBox="0 0 24 24"
279
- preserveAspectRatio="xMidYMid meet"
280
- focusable="false"
281
- style="pointer-events: none; display: block; width: 100%; height: 100%;"
282
- >
283
- <g>
284
- <path
285
- d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
286
- ></path>
287
- </g>
288
- </svg>
289
- </span>
290
- `
291
- ) }
292
- </div>
293
- </div>
294
- ` }
295
- </cosmoz-listbox>`
296
- ) }
297
- ${ when(
298
- showGlobalSearchBtn(
299
- computeSearching(searchValue, searchMinLength),
300
- openNodePath
301
- ),
302
- () => html`
303
- <button class="btn-ghost" @click="${ () => setOpenNodePath('') }">
304
- ${ searchGlobalPlaceholder }
269
+ ${when(
270
+ tree,
271
+ () => html` <div class="items" ${ref((el) => (listRef.current = el))}>
272
+ <div virtualizer-sizer></div>
273
+ ${virtualize({
274
+ items: dataPlane,
275
+ renderItem,
276
+ scroller: true,
277
+ })}
278
+ </div>`
279
+ )}
280
+ ${when(
281
+ search && openNodePath,
282
+ () => html`
283
+ <button class="btn-ghost" @click=${() => setOpenNodePath('')}>
284
+ ${searchGlobalPlaceholder}
305
285
  </button>
306
286
  `
307
- ) }
287
+ )}
308
288
  `;
309
289
  };
310
290
 
@@ -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,37 @@
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
+ searchProperty = tree.searchProperty,
79
23
  sortFunc = (a, b) => {
80
24
  // First sort based on "folder" status (containing children)
81
- if (hasChildren(a)) {
82
- if (!hasChildren(b)) {
25
+ if (tree.hasChildren(a)) {
26
+ if (!tree.hasChildren(b)) {
83
27
  return -1;
84
28
  }
85
- } else if (hasChildren(b)) {
29
+ } else if (tree.hasChildren(b)) {
86
30
  return 1;
87
31
  }
88
32
  // Then sort on searchProperty
89
- const val1 = a[tree.searchProperty],
90
- val2 = b[tree.searchProperty];
33
+ const val1 = a[searchProperty],
34
+ val2 = b[searchProperty];
91
35
 
92
36
  if (val1 > val2) {
93
37
  return 1;
@@ -97,8 +41,21 @@ const tree = new DefaultTree(basicTree),
97
41
  }
98
42
  return 0;
99
43
  };
100
- return normalizeNodes(level)
101
- .sort(sortFunc);
44
+ return children.length > 0 ? children.sort(sortFunc) : node;
45
+ },
46
+ /**
47
+ * Returns the found nodes based on a search string and a given tree to be searched
48
+ * @param {Tree} tree - The main tree
49
+ * @param {String|undefined} search - The search string
50
+ * @param {String} pathLocator - The separated address parts of a node
51
+ * @return {Array} - The found nodes
52
+ */
53
+ computeDataPlane = (tree, search, pathLocator) => {
54
+ if (!tree) {
55
+ return [];
56
+ }
57
+ const nodes = renderLevel(tree, pathLocator);
58
+ return search ? tree.searchNodes(search, nodes, false) : nodes;
102
59
  },
103
60
  /**
104
61
  * Returns the classes of a row based its selection state
@@ -110,9 +67,10 @@ const tree = new DefaultTree(basicTree),
110
67
  computeRowClass = (classes, node, highlightedNode) => {
111
68
  let selected = false;
112
69
  if (highlightedNode) {
113
- selected = node === highlightedNode
114
- || node.id && node.id === highlightedNode.id
115
- || node.pathLocator === highlightedNode.pathLocator;
70
+ selected =
71
+ node === highlightedNode ||
72
+ (node.id && node.id === highlightedNode.id) ||
73
+ node.pathLocator === highlightedNode.pathLocator;
116
74
  }
117
75
  return selected ? classes + ' selected' : classes;
118
76
  },
@@ -123,28 +81,13 @@ const tree = new DefaultTree(basicTree),
123
81
  * @return {undefined}
124
82
  */
125
83
  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 !== '';
84
+ host.dispatchEvent(
85
+ new CustomEvent('node-dblclicked', {
86
+ detail: {
87
+ model: event.model,
88
+ },
89
+ })
90
+ );
148
91
  },
149
92
  /**
150
93
  * Returns the nodes on a path specified by a given path locator
@@ -156,7 +99,7 @@ const tree = new DefaultTree(basicTree),
156
99
  if (!tree || !pathLocator) {
157
100
  return [];
158
101
  }
159
- return normalizeNodes(tree.getPathNodes(pathLocator));
102
+ return tree.getPathNodes(pathLocator);
160
103
  },
161
104
  /**
162
105
  * Returns a node based on a given path locator.
@@ -175,78 +118,7 @@ const tree = new DefaultTree(basicTree),
175
118
  let nodes;
176
119
 
177
120
  if (!node) {
178
- nodes = tree.getPathNodes(pathLocator).filter(n => n != null);
121
+ nodes = tree.getPathNodes(pathLocator).filter((n) => n != null);
179
122
  }
180
123
  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
- };
124
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-treenode-navigator",
3
- "version": "5.0.1",
3
+ "version": "5.3.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