All files / TreeSelect/TreeNodes index.js

100% Statements 55/55
100% Branches 28/28
100% Functions 17/17
100% Lines 44/44
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