| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91 |
3x
3x
81x
81x
486x
486x
486x
486x
486x
81x
3x
1515x
1515x
998x
998x
3x
70x
70x
56x
70x
3x
24x
13x
9x
27x
50x
3x
50x
50x
28x
50x
13x
13x
13x
50x
341x
341x
478x
471x
291x
291x
13x
50x
3x
| import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"
import styles from "../styles.css"
const ROOT_ID = "__root"
export const generateTree = nodes => {
let tree = {
[ROOT_ID]: { children: [], id: ROOT_ID }
}
nodes.forEach(node => {
const { parent = ROOT_ID } = node
tree[parent] = tree[parent] || { children: [] }
tree[node.id] = tree[node.id] || { children: [] }
tree[parent].children.push(node.id)
tree[node.id] = { ...node, ...tree[node.id] }
})
return tree
}
export const doUntil = (tree, id, condition) => {
const node = tree[id]
if (!node) return
const result = condition(node)
return result ? result : doUntil(tree, node.parent, condition)
}
export const processValues = (tree, id, values, acc = []) => {
const node = tree[id]
if (values.includes(node.id)) acc.push(node.id)
else node.children.forEach(id => processValues(tree, id, values, acc))
return acc
}
export const toggleNode = (tree, id, values) => {
const isAnySelected = doUntil(tree, id, node => values.includes(node.id))
if (!isAnySelected) return [...values, id]
return doUntil(tree, id, node => {
// if active, just toggle
if (values.includes(node.id)) return values.filter(i => i !== node.id)
// if not, add siblings but current and continue
values = [...values, ...tree[node.parent].children].filter(i => i !== node.id)
})
}
const TreeNodes = ({ nodes, values = [], onChange, unremovableValues = [] }) => {
const [tree, setTree] = useState(generateTree(nodes))
useEffect(
() => {
setTree(generateTree(nodes))
},
[nodes]
)
const handleChange = id => {
const newValues = toggleNode(tree, id, values)
const result = processValues(tree, ROOT_ID, [...newValues, ...unremovableValues])
onChange(result)
}
const renderNode = id => {
const node = tree[id]
if (id === ROOT_ID) return <ul>{node.children.map(renderNode)}</ul>
const isSelected = doUntil(tree, id, node => values.includes(node.id))
const isUnremovable = doUntil(tree, id, node => unremovableValues.includes(node.id))
const className =
[isSelected && styles.selected, isUnremovable && styles.unremovable]
.filter(Boolean)
.join(" ") || undefined
return (
<React.Fragment key={node.id}>
<li {...node.props} className={className} onClick={() => handleChange(node.id)}>
{node.label}
</li>
{!!node.children.length && node.children.map(renderNode)}
</React.Fragment>
)
}
return renderNode(ROOT_ID)
}
TreeNodes.propTypes = {
nodes: PropTypes.array.isRequired
}
export default TreeNodes
|