1 | import _extends from "@babel/runtime/helpers/esm/extends";
|
2 | import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
|
3 | import * as React from 'react';
|
4 | import clsx from 'clsx';
|
5 | import PropTypes from 'prop-types';
|
6 | import { withStyles } from '@material-ui/core/styles';
|
7 | import { useControlled } from '@material-ui/core/utils';
|
8 | import TreeViewContext from './TreeViewContext';
|
9 | export const styles = {
|
10 |
|
11 | root: {
|
12 | padding: 0,
|
13 | margin: 0,
|
14 | listStyle: 'none'
|
15 | }
|
16 | };
|
17 |
|
18 | function arrayDiff(arr1, arr2) {
|
19 | if (arr1.length !== arr2.length) return true;
|
20 |
|
21 | for (let i = 0; i < arr1.length; i += 1) {
|
22 | if (arr1[i] !== arr2[i]) return true;
|
23 | }
|
24 |
|
25 | return false;
|
26 | }
|
27 |
|
28 | const findNextFirstChar = (firstChars, startIndex, char) => {
|
29 | for (let i = startIndex; i < firstChars.length; i += 1) {
|
30 | if (char === firstChars[i]) {
|
31 | return i;
|
32 | }
|
33 | }
|
34 |
|
35 | return -1;
|
36 | };
|
37 |
|
38 | const defaultExpandedDefault = [];
|
39 | const defaultSelectedDefault = [];
|
40 | const TreeView = React.forwardRef(function TreeView(props, ref) {
|
41 | const {
|
42 | children,
|
43 | classes,
|
44 | className,
|
45 | defaultCollapseIcon,
|
46 | defaultEndIcon,
|
47 | defaultExpanded = defaultExpandedDefault,
|
48 | defaultExpandIcon,
|
49 | defaultParentIcon,
|
50 | defaultSelected = defaultSelectedDefault,
|
51 | disableSelection = false,
|
52 | multiSelect = false,
|
53 | expanded: expandedProp,
|
54 | onNodeSelect,
|
55 | onNodeToggle,
|
56 | selected: selectedProp
|
57 | } = props,
|
58 | other = _objectWithoutPropertiesLoose(props, ["children", "classes", "className", "defaultCollapseIcon", "defaultEndIcon", "defaultExpanded", "defaultExpandIcon", "defaultParentIcon", "defaultSelected", "disableSelection", "multiSelect", "expanded", "onNodeSelect", "onNodeToggle", "selected"]);
|
59 |
|
60 | const [tabbable, setTabbable] = React.useState(null);
|
61 | const [focusedNodeId, setFocusedNodeId] = React.useState(null);
|
62 | const nodeMap = React.useRef({});
|
63 | const firstCharMap = React.useRef({});
|
64 | const visibleNodes = React.useRef([]);
|
65 | const [expanded, setExpandedState] = useControlled({
|
66 | controlled: expandedProp,
|
67 | default: defaultExpanded,
|
68 | name: 'TreeView',
|
69 | state: 'expanded'
|
70 | });
|
71 | const [selected, setSelectedState] = useControlled({
|
72 | controlled: selectedProp,
|
73 | default: defaultSelected,
|
74 | name: 'TreeView',
|
75 | state: 'selected'
|
76 | });
|
77 | |
78 |
|
79 |
|
80 |
|
81 | const isExpanded = React.useCallback(id => Array.isArray(expanded) ? expanded.indexOf(id) !== -1 : false, [expanded]);
|
82 | const isSelected = React.useCallback(id => Array.isArray(selected) ? selected.indexOf(id) !== -1 : selected === id, [selected]);
|
83 |
|
84 | const isTabbable = id => tabbable === id;
|
85 |
|
86 | const isFocused = id => focusedNodeId === id;
|
87 | |
88 |
|
89 |
|
90 |
|
91 |
|
92 | const getNextNode = id => {
|
93 | const nodeIndex = visibleNodes.current.indexOf(id);
|
94 |
|
95 | if (nodeIndex !== -1 && nodeIndex + 1 < visibleNodes.current.length) {
|
96 | return visibleNodes.current[nodeIndex + 1];
|
97 | }
|
98 |
|
99 | return null;
|
100 | };
|
101 |
|
102 | const getPreviousNode = id => {
|
103 | const nodeIndex = visibleNodes.current.indexOf(id);
|
104 |
|
105 | if (nodeIndex !== -1 && nodeIndex - 1 >= 0) {
|
106 | return visibleNodes.current[nodeIndex - 1];
|
107 | }
|
108 |
|
109 | return null;
|
110 | };
|
111 |
|
112 | const getLastNode = () => visibleNodes.current[visibleNodes.current.length - 1];
|
113 |
|
114 | const getFirstNode = () => visibleNodes.current[0];
|
115 |
|
116 | const getParent = id => nodeMap.current[id].parent;
|
117 |
|
118 | const getNodesInRange = (a, b) => {
|
119 | const aIndex = visibleNodes.current.indexOf(a);
|
120 | const bIndex = visibleNodes.current.indexOf(b);
|
121 | const start = Math.min(aIndex, bIndex);
|
122 | const end = Math.max(aIndex, bIndex);
|
123 | return visibleNodes.current.slice(start, end + 1);
|
124 | };
|
125 | |
126 |
|
127 |
|
128 |
|
129 |
|
130 | const focus = id => {
|
131 | if (id) {
|
132 | setTabbable(id);
|
133 | setFocusedNodeId(id);
|
134 | }
|
135 | };
|
136 |
|
137 | const focusNextNode = id => focus(getNextNode(id));
|
138 |
|
139 | const focusPreviousNode = id => focus(getPreviousNode(id));
|
140 |
|
141 | const focusFirstNode = () => focus(getFirstNode());
|
142 |
|
143 | const focusLastNode = () => focus(getLastNode());
|
144 |
|
145 | const focusByFirstCharacter = (id, char) => {
|
146 | let start;
|
147 | let index;
|
148 | const lowercaseChar = char.toLowerCase();
|
149 | const firstCharIds = [];
|
150 | const firstChars = [];
|
151 |
|
152 | Object.keys(firstCharMap.current).forEach(nodeId => {
|
153 | const firstChar = firstCharMap.current[nodeId];
|
154 | const map = nodeMap.current[nodeId];
|
155 | const visible = map.parent ? isExpanded(map.parent) : true;
|
156 |
|
157 | if (visible) {
|
158 | firstCharIds.push(nodeId);
|
159 | firstChars.push(firstChar);
|
160 | }
|
161 | });
|
162 |
|
163 | start = firstCharIds.indexOf(id) + 1;
|
164 |
|
165 | if (start === nodeMap.current.length) {
|
166 | start = 0;
|
167 | }
|
168 |
|
169 |
|
170 | index = findNextFirstChar(firstChars, start, lowercaseChar);
|
171 |
|
172 | if (index === -1) {
|
173 | index = findNextFirstChar(firstChars, 0, lowercaseChar);
|
174 | }
|
175 |
|
176 |
|
177 | if (index > -1) {
|
178 | focus(firstCharIds[index]);
|
179 | }
|
180 | };
|
181 | |
182 |
|
183 |
|
184 |
|
185 |
|
186 | const toggleExpansion = (event, value = focusedNodeId) => {
|
187 | let newExpanded;
|
188 |
|
189 | if (expanded.indexOf(value) !== -1) {
|
190 | newExpanded = expanded.filter(id => id !== value);
|
191 | setTabbable(oldTabbable => {
|
192 | const map = nodeMap.current[oldTabbable];
|
193 |
|
194 | if (oldTabbable && (map && map.parent ? map.parent.id : null) === value) {
|
195 | return value;
|
196 | }
|
197 |
|
198 | return oldTabbable;
|
199 | });
|
200 | } else {
|
201 | newExpanded = [value].concat(expanded);
|
202 | }
|
203 |
|
204 | if (onNodeToggle) {
|
205 | onNodeToggle(event, newExpanded);
|
206 | }
|
207 |
|
208 | setExpandedState(newExpanded);
|
209 | };
|
210 |
|
211 | const expandAllSiblings = (event, id) => {
|
212 | const map = nodeMap.current[id];
|
213 | const parent = nodeMap.current[map.parent];
|
214 | let diff;
|
215 |
|
216 | if (parent) {
|
217 | diff = parent.children.filter(child => !isExpanded(child));
|
218 | } else {
|
219 | const topLevelNodes = nodeMap.current[-1].children;
|
220 | diff = topLevelNodes.filter(node => !isExpanded(node));
|
221 | }
|
222 |
|
223 | const newExpanded = expanded.concat(diff);
|
224 |
|
225 | if (diff.length > 0) {
|
226 | setExpandedState(newExpanded);
|
227 |
|
228 | if (onNodeToggle) {
|
229 | onNodeToggle(event, newExpanded);
|
230 | }
|
231 | }
|
232 | };
|
233 | |
234 |
|
235 |
|
236 |
|
237 |
|
238 | const lastSelectedNode = React.useRef(null);
|
239 | const lastSelectionWasRange = React.useRef(false);
|
240 | const currentRangeSelection = React.useRef([]);
|
241 |
|
242 | const handleRangeArrowSelect = (event, nodes) => {
|
243 | let base = selected;
|
244 | const {
|
245 | start,
|
246 | next,
|
247 | current
|
248 | } = nodes;
|
249 |
|
250 | if (!next || !current) {
|
251 | return;
|
252 | }
|
253 |
|
254 | if (currentRangeSelection.current.indexOf(current) === -1) {
|
255 | currentRangeSelection.current = [];
|
256 | }
|
257 |
|
258 | if (lastSelectionWasRange.current) {
|
259 | if (currentRangeSelection.current.indexOf(next) !== -1) {
|
260 | base = base.filter(id => id === start || id !== current);
|
261 | currentRangeSelection.current = currentRangeSelection.current.filter(id => id === start || id !== current);
|
262 | } else {
|
263 | base.push(next);
|
264 | currentRangeSelection.current.push(next);
|
265 | }
|
266 | } else {
|
267 | base.push(next);
|
268 | currentRangeSelection.current.push(current, next);
|
269 | }
|
270 |
|
271 | if (onNodeSelect) {
|
272 | onNodeSelect(event, base);
|
273 | }
|
274 |
|
275 | setSelectedState(base);
|
276 | };
|
277 |
|
278 | const handleRangeSelect = (event, nodes) => {
|
279 | let base = selected;
|
280 | const {
|
281 | start,
|
282 | end
|
283 | } = nodes;
|
284 |
|
285 | if (lastSelectionWasRange.current) {
|
286 | base = selected.filter(id => currentRangeSelection.current.indexOf(id) === -1);
|
287 | }
|
288 |
|
289 | const range = getNodesInRange(start, end);
|
290 | currentRangeSelection.current = range;
|
291 | let newSelected = base.concat(range);
|
292 | newSelected = newSelected.filter((id, i) => newSelected.indexOf(id) === i);
|
293 |
|
294 | if (onNodeSelect) {
|
295 | onNodeSelect(event, newSelected);
|
296 | }
|
297 |
|
298 | setSelectedState(newSelected);
|
299 | };
|
300 |
|
301 | const handleMultipleSelect = (event, value) => {
|
302 | let newSelected = [];
|
303 |
|
304 | if (selected.indexOf(value) !== -1) {
|
305 | newSelected = selected.filter(id => id !== value);
|
306 | } else {
|
307 | newSelected = [value].concat(selected);
|
308 | }
|
309 |
|
310 | if (onNodeSelect) {
|
311 | onNodeSelect(event, newSelected);
|
312 | }
|
313 |
|
314 | setSelectedState(newSelected);
|
315 | };
|
316 |
|
317 | const handleSingleSelect = (event, value) => {
|
318 | const newSelected = multiSelect ? [value] : value;
|
319 |
|
320 | if (onNodeSelect) {
|
321 | onNodeSelect(event, newSelected);
|
322 | }
|
323 |
|
324 | setSelectedState(newSelected);
|
325 | };
|
326 |
|
327 | const selectNode = (event, id, multiple = false) => {
|
328 | if (id) {
|
329 | if (multiple) {
|
330 | handleMultipleSelect(event, id);
|
331 | } else {
|
332 | handleSingleSelect(event, id);
|
333 | }
|
334 |
|
335 | lastSelectedNode.current = id;
|
336 | lastSelectionWasRange.current = false;
|
337 | currentRangeSelection.current = [];
|
338 | return true;
|
339 | }
|
340 |
|
341 | return false;
|
342 | };
|
343 |
|
344 | const selectRange = (event, nodes, stacked = false) => {
|
345 | const {
|
346 | start = lastSelectedNode.current,
|
347 | end,
|
348 | current
|
349 | } = nodes;
|
350 |
|
351 | if (stacked) {
|
352 | handleRangeArrowSelect(event, {
|
353 | start,
|
354 | next: end,
|
355 | current
|
356 | });
|
357 | } else {
|
358 | handleRangeSelect(event, {
|
359 | start,
|
360 | end
|
361 | });
|
362 | }
|
363 |
|
364 | lastSelectionWasRange.current = true;
|
365 | return true;
|
366 | };
|
367 |
|
368 | const rangeSelectToFirst = (event, id) => {
|
369 | if (!lastSelectedNode.current) {
|
370 | lastSelectedNode.current = id;
|
371 | }
|
372 |
|
373 | const start = lastSelectionWasRange.current ? lastSelectedNode.current : id;
|
374 | return selectRange(event, {
|
375 | start,
|
376 | end: getFirstNode()
|
377 | });
|
378 | };
|
379 |
|
380 | const rangeSelectToLast = (event, id) => {
|
381 | if (!lastSelectedNode.current) {
|
382 | lastSelectedNode.current = id;
|
383 | }
|
384 |
|
385 | const start = lastSelectionWasRange.current ? lastSelectedNode.current : id;
|
386 | return selectRange(event, {
|
387 | start,
|
388 | end: getLastNode()
|
389 | });
|
390 | };
|
391 |
|
392 | const selectNextNode = (event, id) => selectRange(event, {
|
393 | end: getNextNode(id),
|
394 | current: id
|
395 | }, true);
|
396 |
|
397 | const selectPreviousNode = (event, id) => selectRange(event, {
|
398 | end: getPreviousNode(id),
|
399 | current: id
|
400 | }, true);
|
401 |
|
402 | const selectAllNodes = event => selectRange(event, {
|
403 | start: getFirstNode(),
|
404 | end: getLastNode()
|
405 | });
|
406 | |
407 |
|
408 |
|
409 |
|
410 |
|
411 | const addNodeToNodeMap = (id, childrenIds) => {
|
412 | const currentMap = nodeMap.current[id];
|
413 | nodeMap.current[id] = _extends({}, currentMap, {
|
414 | children: childrenIds,
|
415 | id
|
416 | });
|
417 | childrenIds.forEach(childId => {
|
418 | const currentChildMap = nodeMap.current[childId];
|
419 | nodeMap.current[childId] = _extends({}, currentChildMap, {
|
420 | parent: id,
|
421 | id: childId
|
422 | });
|
423 | });
|
424 | };
|
425 |
|
426 | const getNodesToRemove = React.useCallback(id => {
|
427 | const map = nodeMap.current[id];
|
428 | const nodes = [];
|
429 |
|
430 | if (map) {
|
431 | nodes.push(id);
|
432 |
|
433 | if (map.children) {
|
434 | nodes.concat(map.children);
|
435 | map.children.forEach(node => {
|
436 | nodes.concat(getNodesToRemove(node));
|
437 | });
|
438 | }
|
439 | }
|
440 |
|
441 | return nodes;
|
442 | }, []);
|
443 | const cleanUpFirstCharMap = React.useCallback(nodes => {
|
444 | const newMap = _extends({}, firstCharMap.current);
|
445 |
|
446 | nodes.forEach(node => {
|
447 | if (newMap[node]) {
|
448 | delete newMap[node];
|
449 | }
|
450 | });
|
451 | firstCharMap.current = newMap;
|
452 | }, []);
|
453 | const removeNodeFromNodeMap = React.useCallback(id => {
|
454 | const nodes = getNodesToRemove(id);
|
455 | cleanUpFirstCharMap(nodes);
|
456 |
|
457 | const newMap = _extends({}, nodeMap.current);
|
458 |
|
459 | nodes.forEach(node => {
|
460 | const map = newMap[node];
|
461 |
|
462 | if (map) {
|
463 | if (map.parent) {
|
464 | const parentMap = newMap[map.parent];
|
465 |
|
466 | if (parentMap && parentMap.children) {
|
467 | const parentChildren = parentMap.children.filter(c => c !== node);
|
468 | newMap[map.parent] = _extends({}, parentMap, {
|
469 | children: parentChildren
|
470 | });
|
471 | }
|
472 | }
|
473 |
|
474 | delete newMap[node];
|
475 | }
|
476 | });
|
477 | nodeMap.current = newMap;
|
478 | setFocusedNodeId(oldFocusedNodeId => {
|
479 | if (oldFocusedNodeId === id) {
|
480 | return null;
|
481 | }
|
482 |
|
483 | return oldFocusedNodeId;
|
484 | });
|
485 | }, [getNodesToRemove, cleanUpFirstCharMap]);
|
486 |
|
487 | const mapFirstChar = (id, firstChar) => {
|
488 | firstCharMap.current[id] = firstChar;
|
489 | };
|
490 |
|
491 | const prevChildIds = React.useRef([]);
|
492 | const [childrenCalculated, setChildrenCalculated] = React.useState(false);
|
493 | React.useEffect(() => {
|
494 | const childIds = [];
|
495 | React.Children.forEach(children, child => {
|
496 | if ( React.isValidElement(child) && child.props.nodeId) {
|
497 | childIds.push(child.props.nodeId);
|
498 | }
|
499 | });
|
500 |
|
501 | if (arrayDiff(prevChildIds.current, childIds)) {
|
502 | nodeMap.current[-1] = {
|
503 | parent: null,
|
504 | children: childIds
|
505 | };
|
506 | childIds.forEach((id, index) => {
|
507 | if (index === 0) {
|
508 | setTabbable(id);
|
509 | }
|
510 | });
|
511 | visibleNodes.current = nodeMap.current[-1].children;
|
512 | prevChildIds.current = childIds;
|
513 | setChildrenCalculated(true);
|
514 | }
|
515 | }, [children]);
|
516 | React.useEffect(() => {
|
517 | const buildVisible = nodes => {
|
518 | let list = [];
|
519 |
|
520 | for (let i = 0; i < nodes.length; i += 1) {
|
521 | const item = nodes[i];
|
522 | list.push(item);
|
523 | const childs = nodeMap.current[item].children;
|
524 |
|
525 | if (isExpanded(item) && childs) {
|
526 | list = list.concat(buildVisible(childs));
|
527 | }
|
528 | }
|
529 |
|
530 | return list;
|
531 | };
|
532 |
|
533 | if (childrenCalculated) {
|
534 | visibleNodes.current = buildVisible(nodeMap.current[-1].children);
|
535 | }
|
536 | }, [expanded, childrenCalculated, isExpanded, children]);
|
537 |
|
538 | const noopSelection = () => {
|
539 | return false;
|
540 | };
|
541 |
|
542 | return React.createElement(TreeViewContext.Provider, {
|
543 | value: {
|
544 | icons: {
|
545 | defaultCollapseIcon,
|
546 | defaultExpandIcon,
|
547 | defaultParentIcon,
|
548 | defaultEndIcon
|
549 | },
|
550 | focus,
|
551 | focusFirstNode,
|
552 | focusLastNode,
|
553 | focusNextNode,
|
554 | focusPreviousNode,
|
555 | focusByFirstCharacter,
|
556 | expandAllSiblings,
|
557 | toggleExpansion,
|
558 | isExpanded,
|
559 | isFocused,
|
560 | isSelected,
|
561 | selectNode: disableSelection ? noopSelection : selectNode,
|
562 | selectRange: disableSelection ? noopSelection : selectRange,
|
563 | selectNextNode: disableSelection ? noopSelection : selectNextNode,
|
564 | selectPreviousNode: disableSelection ? noopSelection : selectPreviousNode,
|
565 | rangeSelectToFirst: disableSelection ? noopSelection : rangeSelectToFirst,
|
566 | rangeSelectToLast: disableSelection ? noopSelection : rangeSelectToLast,
|
567 | selectAllNodes: disableSelection ? noopSelection : selectAllNodes,
|
568 | isTabbable,
|
569 | multiSelect,
|
570 | getParent,
|
571 | mapFirstChar,
|
572 | addNodeToNodeMap,
|
573 | removeNodeFromNodeMap
|
574 | }
|
575 | }, React.createElement("ul", _extends({
|
576 | role: "tree",
|
577 | "aria-multiselectable": multiSelect,
|
578 | className: clsx(classes.root, className),
|
579 | ref: ref
|
580 | }, other), children));
|
581 | });
|
582 | process.env.NODE_ENV !== "production" ? TreeView.propTypes = {
|
583 |
|
584 |
|
585 |
|
586 |
|
587 |
|
588 | |
589 |
|
590 |
|
591 | children: PropTypes.node,
|
592 |
|
593 | |
594 |
|
595 |
|
596 |
|
597 | classes: PropTypes.object,
|
598 |
|
599 | |
600 |
|
601 |
|
602 | className: PropTypes.string,
|
603 |
|
604 | |
605 |
|
606 |
|
607 | defaultCollapseIcon: PropTypes.node,
|
608 |
|
609 | |
610 |
|
611 |
|
612 |
|
613 | defaultEndIcon: PropTypes.node,
|
614 |
|
615 | |
616 |
|
617 |
|
618 | defaultExpanded: PropTypes.arrayOf(PropTypes.string),
|
619 |
|
620 | |
621 |
|
622 |
|
623 | defaultExpandIcon: PropTypes.node,
|
624 |
|
625 | |
626 |
|
627 |
|
628 |
|
629 | defaultParentIcon: PropTypes.node,
|
630 |
|
631 | |
632 |
|
633 |
|
634 |
|
635 | defaultSelected: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]),
|
636 |
|
637 | |
638 |
|
639 |
|
640 | disableSelection: PropTypes.bool,
|
641 |
|
642 | |
643 |
|
644 |
|
645 | expanded: PropTypes.arrayOf(PropTypes.string),
|
646 |
|
647 | |
648 |
|
649 |
|
650 | multiSelect: PropTypes.bool,
|
651 |
|
652 | |
653 |
|
654 |
|
655 |
|
656 |
|
657 |
|
658 |
|
659 | onNodeSelect: PropTypes.func,
|
660 |
|
661 | |
662 |
|
663 |
|
664 |
|
665 |
|
666 |
|
667 | onNodeToggle: PropTypes.func,
|
668 |
|
669 | |
670 |
|
671 |
|
672 |
|
673 | selected: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string])
|
674 | } : void 0;
|
675 | export default withStyles(styles, {
|
676 | name: 'MuiTreeView'
|
677 | })(TreeView); |
\ | No newline at end of file |