@neovici/cosmoz-treenode-navigator 6.0.2 → 7.0.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 (33) hide show
  1. package/README.md +4 -12
  2. package/dist/cosmoz-treenode-button-view.d.ts +2 -0
  3. package/dist/cosmoz-treenode-button-view.d.ts.map +1 -0
  4. package/dist/cosmoz-treenode-button-view.js +198 -0
  5. package/dist/cosmoz-treenode-button-view.js.map +1 -0
  6. package/dist/cosmoz-treenode-button-view.styles.d.ts +3 -0
  7. package/dist/cosmoz-treenode-button-view.styles.d.ts.map +1 -0
  8. package/dist/cosmoz-treenode-button-view.styles.js +216 -0
  9. package/dist/cosmoz-treenode-button-view.styles.js.map +1 -0
  10. package/dist/cosmoz-treenode-navigator.d.ts +2 -0
  11. package/dist/cosmoz-treenode-navigator.d.ts.map +1 -0
  12. package/dist/cosmoz-treenode-navigator.js +246 -0
  13. package/dist/cosmoz-treenode-navigator.js.map +1 -0
  14. package/dist/cosmoz-treenode-navigator.styles.d.ts +3 -0
  15. package/dist/cosmoz-treenode-navigator.styles.d.ts.map +1 -0
  16. package/{cosmoz-treenode-navigator.styles.js → dist/cosmoz-treenode-navigator.styles.js} +11 -7
  17. package/dist/cosmoz-treenode-navigator.styles.js.map +1 -0
  18. package/dist/hooks/useKeyDown.d.ts +2 -0
  19. package/dist/hooks/useKeyDown.d.ts.map +1 -0
  20. package/dist/hooks/useKeyDown.js +16 -0
  21. package/dist/hooks/useKeyDown.js.map +1 -0
  22. package/dist/util/helpers.d.ts +51 -0
  23. package/dist/util/helpers.d.ts.map +1 -0
  24. package/dist/util/helpers.js +125 -0
  25. package/dist/util/helpers.js.map +1 -0
  26. package/package.json +61 -21
  27. package/test/data/basicTree.js +34 -34
  28. package/test/data/rtlTest.js +6 -7
  29. package/cosmoz-treenode-button-view.js +0 -445
  30. package/cosmoz-treenode-navigator.js +0 -297
  31. package/helpers.js +0 -124
  32. package/test/cosmoz-treenode-button-view.test.js +0 -52
  33. package/test/cosmoz-treenode-navigator.test.js +0 -102
@@ -1,297 +0,0 @@
1
- import {
2
- component,
3
- useEffect,
4
- useState,
5
- useMemo,
6
- useCallback,
7
- useRef,
8
- } from '@pionjs/pion';
9
- import { html, nothing } from 'lit-html';
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';
14
-
15
- import '@neovici/cosmoz-input';
16
- import { useMeta } from '@neovici/cosmoz-utils/hooks/use-meta';
17
- import { notifyProperty } from '@neovici/cosmoz-utils/hooks/use-notify-property';
18
-
19
- import {
20
- getParentPath,
21
- computeDataPlane,
22
- computeRowClass,
23
- onNodeDblClicked,
24
- } from './helpers';
25
- import styles from './cosmoz-treenode-navigator.styles';
26
-
27
- const TreenodeNavigator = (host) => {
28
- const {
29
- /**
30
- * The main node structure
31
- */
32
- tree,
33
- /**
34
- * Placeholder for search field.
35
- */
36
- searchPlaceholder,
37
- /**
38
- * Text displayed when local search has finished
39
- * to suggest a search on the entire tree
40
- */
41
- searchGlobalPlaceholder,
42
- /**
43
- * Minimum length of searchValue to trigger a search
44
- */
45
- searchMinLength,
46
- nodePath,
47
- opened,
48
- } = host,
49
- /**
50
- * The path of the opened node
51
- */
52
- [openNodePath, setOpenNodePath] = useState(''),
53
- /**
54
- * The highlighted (focused) node
55
- * This is the node which is currently selected in the list
56
- */
57
- [highlightedNode, setHighlightedNode] = useState(),
58
- /**
59
- * The search string
60
- */
61
- [searchValue, setSearchValue] = useState(''),
62
- search = useMemo(
63
- () => (searchValue?.length > searchMinLength && searchValue) || undefined,
64
- [searchValue, searchMinLength],
65
- ),
66
- /**
67
- * The currently displayed node list
68
- */
69
- dataPlane = useMemo(
70
- () => computeDataPlane(tree, search, openNodePath),
71
- [tree, search, openNodePath],
72
- ),
73
- /**
74
- * Opens a node (renderLevel) based on a given path
75
- * @param {object} clickedNode - The clicked node
76
- * @return {undefined}
77
- */
78
- openNode = useCallback(
79
- (clickedNode) => {
80
- setOpenNodePath(clickedNode?.pathLocator || '');
81
- setSearchValue('');
82
- setHighlightedNode();
83
- },
84
- [tree],
85
- ),
86
- listRef = useRef();
87
-
88
- useEffect(() => {
89
- if (!openNodePath) {
90
- notifyProperty(host, 'highlightedNodePath', '');
91
- notifyProperty(host, 'nodesOnNodePath', []);
92
- }
93
- }, [openNodePath]);
94
-
95
- useEffect(() => {
96
- if (nodePath !== undefined) {
97
- host.dispatchEvent(new CustomEvent('select-node'));
98
- }
99
- }, [nodePath]);
100
-
101
- useEffect(() => {
102
- notifyProperty(
103
- host,
104
- 'highlightedNodePath',
105
- highlightedNode?.pathLocator || '',
106
- );
107
- }, [highlightedNode]);
108
-
109
- const meta = useMeta({ dataPlane, highlightedNode, openNode });
110
-
111
- useEffect(() => {
112
- if (!opened) {
113
- return;
114
- }
115
-
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);
134
-
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;
162
-
163
- case 'Enter':
164
- if (node) {
165
- e.preventDefault();
166
- host.dispatchEvent(new CustomEvent('node-dblclicked'));
167
- }
168
- break;
169
-
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
- };
181
-
182
- // autoscroll on open
183
- (() => {
184
- getVlist().scrollToIndex = {
185
- index: meta.dataPlane?.indexOf(meta.highlightedNode),
186
- position: 'center',
187
- };
188
- })();
189
-
190
- document.addEventListener('keydown', handler, true);
191
- return () => document.removeEventListener('keydown', handler, true);
192
- }, [opened, meta]);
193
-
194
- const renderItem = (node, index) =>
195
- node
196
- ? html` <div class="item">
197
- ${((parentPath) =>
198
- when(
199
- search &&
200
- (index === 0 ||
201
- parentPath !== getParentPath(tree, dataPlane[index - 1])),
202
- () => html`
203
- <div class="section">
204
- ${tree.getPathString(parentPath, tree.searchProperty)}
205
- </div>
206
- `,
207
- ))(getParentPath(tree, node))}
208
- <div
209
- class=${computeRowClass('node', node, highlightedNode)}
210
- @click=${() => setHighlightedNode(node)}
211
- @dblclick=${(e) => onNodeDblClicked(e, host)}
212
- >
213
- <div class="name">${node[tree.searchProperty]}</div>
214
- ${when(
215
- tree.hasChildren(node),
216
- () => html`
217
- <span class="icon" @click=${() => openNode(node)}>
218
- <svg
219
- viewBox="0 0 24 24"
220
- preserveAspectRatio="xMidYMid meet"
221
- focusable="false"
222
- style="pointer-events: none; display: block; width: 100%; height: 100%;"
223
- >
224
- <g>
225
- <path
226
- d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
227
- ></path>
228
- </g>
229
- </svg>
230
- </span>
231
- `,
232
- )}
233
- </div>
234
- </div>`
235
- : nothing;
236
-
237
- return html`
238
- <style>
239
- ${styles}
240
- </style>
241
- <div class="header">
242
- <h3 class="path">
243
- <span class="icon" @click=${() => openNode()}>
244
- <svg
245
- viewBox="0 0 24 24"
246
- preserveAspectRatio="xMidYMid meet"
247
- focusable="false"
248
- style="pointer-events: none; display: block; width: 100%; height: 100%;"
249
- >
250
- <g><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"></path></g>
251
- </svg>
252
- </span>
253
- ${guard([tree, openNodePath], () =>
254
- ((openNodePath && tree?.getPathNodes(openNodePath)) || []).map(
255
- (node) => html`
256
- <span class="slash">/</span>
257
- <span class="pointer" tabindex="0" @click=${() => openNode(node)}
258
- >${node[tree.searchProperty]}</span
259
- >
260
- `,
261
- ),
262
- )}
263
- </h3>
264
- <cosmoz-input
265
- tabindex="0"
266
- .value=${searchValue}
267
- .placeholder=${searchPlaceholder}
268
- @input=${(e) => setSearchValue(e.target.value)}
269
- />
270
- </div>
271
- ${when(
272
- tree,
273
- () =>
274
- html` <div class="items" ${ref((el) => (listRef.current = el))}>
275
- <div virtualizer-sizer></div>
276
- ${virtualize({
277
- items: dataPlane,
278
- renderItem,
279
- scroller: true,
280
- })}
281
- </div>`,
282
- )}
283
- ${when(
284
- search && openNodePath,
285
- () => html`
286
- <button class="btn-ghost" @click=${() => setOpenNodePath('')}>
287
- ${searchGlobalPlaceholder}
288
- </button>
289
- `,
290
- )}
291
- `;
292
- };
293
-
294
- customElements.define(
295
- 'cosmoz-treenode-navigator',
296
- component(TreenodeNavigator),
297
- );
package/helpers.js DELETED
@@ -1,124 +0,0 @@
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;
8
- },
9
- /**
10
- * Returns a node array with the children of a node on the given path
11
- * If the node doesn't have children, the node gets returned
12
- * @param {Tree} tree - The main tree
13
- * @param {String} pathLocator - The separated address parts of a node
14
- * @return {Array} - Nodes
15
- */
16
- renderLevel = (tree, pathLocator) => {
17
- if (!tree) {
18
- return;
19
- }
20
- const node = tree.getNodeByPathLocator(pathLocator),
21
- children = tree.getChildren(node),
22
- searchProperty = tree.searchProperty,
23
- sortFunc = (a, b) => {
24
- // First sort based on "folder" status (containing children)
25
- if (tree.hasChildren(a)) {
26
- if (!tree.hasChildren(b)) {
27
- return -1;
28
- }
29
- } else if (tree.hasChildren(b)) {
30
- return 1;
31
- }
32
- // Then sort on searchProperty
33
- const val1 = a[searchProperty],
34
- val2 = b[searchProperty];
35
-
36
- if (val1 > val2) {
37
- return 1;
38
- }
39
- if (val1 < val2) {
40
- return -1;
41
- }
42
- return 0;
43
- };
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;
59
- },
60
- /**
61
- * Returns the classes of a row based its selection state
62
- * @param {String} classes - The default classes
63
- * @param {object} node - Node to check
64
- * @param {object} highlightedNode - Currently highlighted node
65
- * @return {String} - The CSS classes
66
- */
67
- computeRowClass = (classes, node, highlightedNode) => {
68
- let selected = false;
69
- if (highlightedNode) {
70
- selected =
71
- node === highlightedNode ||
72
- (node.id && node.id === highlightedNode.id) ||
73
- node.pathLocator === highlightedNode.pathLocator;
74
- }
75
- return selected ? classes + ' selected' : classes;
76
- },
77
- /**
78
- * Selects the doubled clicked node and dispatches an node-dblclicked event.
79
- * @param {Event} event The triggering event
80
- * @param {object} host Host
81
- * @return {undefined}
82
- */
83
- onNodeDblClicked = (event, host) => {
84
- host.dispatchEvent(
85
- new CustomEvent('node-dblclicked', {
86
- detail: {
87
- model: event.model,
88
- },
89
- })
90
- );
91
- },
92
- /**
93
- * Returns the nodes on a path specified by a given path locator
94
- * @param {String} pathLocator - The separated address parts of a node
95
- * @param {Tree} tree - The main tree
96
- * @return {Array} - The found nodes or empty array
97
- */
98
- getTreePathParts = (pathLocator, tree) => {
99
- if (!tree || !pathLocator) {
100
- return [];
101
- }
102
- return tree.getPathNodes(pathLocator);
103
- },
104
- /**
105
- * Returns a node based on a given path locator.
106
- * If pathLocator is empty or not defined, null gets returned.
107
- * If pathLocator is only partly valid, the last valid node gets returned.
108
- * @param {String} pathLocator - The separated address parts of a node
109
- * @param {Tree} tree - The main tree
110
- * @return {Object} - The found node
111
- */
112
- getNode = (pathLocator, tree) => {
113
- if (!tree || !pathLocator) {
114
- return null;
115
- }
116
-
117
- const node = tree.getNodeByPathLocator(pathLocator);
118
- let nodes;
119
-
120
- if (!node) {
121
- nodes = tree.getPathNodes(pathLocator).filter((n) => n != null);
122
- }
123
- return nodes && nodes.length > 0 ? nodes.pop() : node;
124
- };
@@ -1,52 +0,0 @@
1
- import { assert, fixture, html } from '@open-wc/testing';
2
- import { DefaultTree } from '@neovici/cosmoz-tree/cosmoz-default-tree';
3
- import rtlData from './data/rtlTest.js';
4
- import '../cosmoz-treenode-button-view';
5
-
6
- suite('rtl', () => {
7
- let treeButton;
8
-
9
- suiteSetup(async () => {
10
- treeButton = await fixture(html`<cosmoz-treenode-button-view .tree=${ new DefaultTree(rtlData) }>`);
11
- });
12
-
13
- test('button renders selected path', done => {
14
- treeButton.nodePath = '1.2.3';
15
-
16
- // Button view label depends on a binding in a cosmoz-treenode-navigator
17
- // stamped asynchronously, so we need to wait a bit.
18
- setTimeout(() => {
19
- const buttonLabel = treeButton.shadowRoot.querySelector('.pathToNode').textContent;
20
- assert.isTrue(buttonLabel.endsWith('1 / 2 / 3'));
21
- done();
22
- }, 500);
23
- });
24
-
25
- /*
26
- // commented out because test is unstable
27
- function isIdActiveElement(id, element, idPreviouslyFound) {
28
- let idFound = idPreviouslyFound || element && element.id === id;
29
-
30
- if (element != null && element.shadowRoot != null) {
31
- return isIdActiveElement(id, element.shadowRoot.activeElement, idFound);
32
- }
33
-
34
- // active element not null means that it has focus
35
- return element != null && idFound;
36
- }
37
-
38
- test('search input has focus when dialog has opened', done => {
39
- treeButton.$$('.pathToNode').click();
40
- Polymer.Base.async(() => {
41
- // real focus can only be tested here, not valid outside test()
42
- if (window.document.hasFocus()) {
43
- assert.equal(isIdActiveElement('searchInput', window.document.activeElement), true);
44
- }
45
- if (treeButton.$.dialogTree != null) {
46
- treeButton.$.dialogTree.close();
47
- }
48
- done();
49
- }, 50);
50
- });
51
- */
52
- });
@@ -1,102 +0,0 @@
1
- import { assert, fixture, html } from '@open-wc/testing';
2
- import { DefaultTree } from '@neovici/cosmoz-tree/cosmoz-default-tree';
3
- import basicTree from './data/basicTree.js';
4
- import '../cosmoz-treenode-navigator';
5
- import { computeDataPlane } from '../helpers';
6
-
7
- suite('cosmoz-treenode-navigator', () => {
8
- let navigator;
9
-
10
- setup(async () => {
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>`);
16
- });
17
-
18
- test('instantiating the element', () => {
19
- assert.equal(navigator.tagName, 'COSMOZ-TREENODE-NAVIGATOR');
20
- });
21
-
22
- test('tree length and sort', () => {
23
- // given a node navigator with some data
24
-
25
- // when I enter the first node
26
- navigator.openNodePath = '1000';
27
-
28
- const dataPlane = computeDataPlane(
29
- navigator.tree,
30
- navigator.searchValue,
31
- navigator.openNodePath
32
- );
33
- // then I can see the contents of the node
34
- // and it is ordered folder-first, then alphabetically
35
- assert.equal(dataPlane[0].name, 'Data');
36
- assert.equal(dataPlane[1].name, 'Backup');
37
- assert.equal(dataPlane.length, 2);
38
- });
39
-
40
- test('set proper search placeholder', () => {
41
- assert.equal(
42
- navigator.shadowRoot
43
- .querySelector('cosmoz-input')
44
- .shadowRoot.querySelector('input').placeholder,
45
- 'Custom search placeholder'
46
- );
47
- });
48
-
49
- test('should check if node has children', () => {
50
- const node = {
51
- name: 'D:',
52
- path: '1000',
53
- children: {
54
- 1001: {
55
- name: 'Data',
56
- pathLocator: '1000.1001',
57
- children: { 1002: { name: 'John', pathLocator: '1000.1001.1002' } },
58
- },
59
- },
60
- parentSectionName: 'D:',
61
- pathLocator: '1000',
62
- };
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);
71
- });
72
-
73
- test('match search string', () => {
74
- // given a node navigator with some data
75
- // when I search for 'John'
76
- navigator.searchValue = 'John';
77
-
78
- const dataPlane = computeDataPlane(
79
- navigator.tree,
80
- navigator.searchValue,
81
- '1000'
82
- );
83
-
84
- //then I can see only 1 item matching 'John'
85
- assert.lengthOf(dataPlane, 1);
86
- assert.equal(dataPlane[0].name, 'John');
87
- });
88
-
89
- test('not match search string', () => {
90
- // given a node navigator with some data
91
- // when I search for 'NONEXISTING'
92
- navigator.searchValue = 'NONEXISTING';
93
- const dataPlane = computeDataPlane(
94
- navigator.tree,
95
- navigator.searchValue,
96
- '1000'
97
- );
98
-
99
- //then I can see that is no contents
100
- assert.lengthOf(dataPlane, 0);
101
- });
102
- });