UNPKG

@gitlab/ui

Version:
200 lines (183 loc) 6.15 kB
import { CHECKED_STATE } from './constants'; import { Node } from './node'; class Tree { constructor(options, selected) { this.treeDepth = 0; this.nodes = {}; this.initNodes(options, selected); this.initIndeterminateStates(); } /** * @returns {[Node]} The tree as an array of Node instances */ get nodesList() { return Object.values(this.nodes); } /** * @returns {array} The values currently selected */ get selected() { return this.nodesList.filter(node => node.isChecked).map(node => node.value); } /** * @returns {boolean} Whether all options are checked */ get allOptionsChecked() { return this.selected.length === this.nodesList.length; } /** * @returns {boolean} Whether some, but not all options are checked */ get someOptionsChecked() { return this.selected.length > 0 && !this.allOptionsChecked; } /** * Creates a flat tree of Node instances. * @param {array} options The options list * @param {array} selected Pre-selected option values * @param {object} parent The options' parent * @param {number} depth The current depth-level in the tree */ initNodes() { let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; let selected = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; let parent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; let depth = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; if (!options.length) { return; } this.treeDepth = depth > this.treeDepth ? depth : this.treeDepth; options.forEach(option => { const isChecked = selected.includes(option.value); this.nodes[option.value] = new Node({ ...option, parent, isChecked, depth }); this.initNodes(option.children, selected, option, depth + 1); }); } /** * Looks for UNCHECKED nodes and sets their checked state to INDETERMINATE if needed. We start * with the deepest leaves and we go up level by level to propagate the correct INDETERMINATE * states to each parent node. */ initIndeterminateStates() { const nodes = [...this.nodesList]; for (let i = this.treeDepth; i >= 0; i -= 1) { const removeIndices = []; nodes.forEach((node, nodeIndex) => { if (node.depth === i && node.isUnchecked) { node.setCheckedState(this.optionHasSomeChildrenChecked(node) ? CHECKED_STATE.INDETERMINATE : node.checkedState); removeIndices.push(nodeIndex); } }); removeIndices.reverse().forEach(index => { nodes.splice(index, 1); }); } } /** * Returns true if all of the option's children are checked, false otherwise. * @param {object} option * @returns {boolean} */ optionHasAllChildrenChecked(option) { return this.getOptionChildren(option).every(child => child.isChecked); } /** * Returns true if at least one of the option's children is in a checked or indeterminate state, * returns false otherwise. * We consider the INDETERMINATE state as a checked state so we can propagate INDETERMINATE states * to the option's parents. * @param {object} option * @returns {boolean} */ optionHasSomeChildrenChecked(option) { return this.getOptionChildren(option).some(child => child.isCheckedOrIndeterminate); } /** * Returns the Node instance for a given option's value. * @param {number|string} value The option's value * @returns {Node} */ getNode(value) { return this.nodes[value]; } /** * Returns the option's children as Node instances. * @param {object} option * @returns {[Node]} */ getOptionChildren(option) { return option.children.map(_ref => { let { value } = _ref; return this.getNode(value); }); } /** * Sets a node's state based on whether it got checked or unchecked * @param {Node} node The node to be toggled * @param {boolean} checked Whether the node should be checked */ static toggleNodeState(node, checked) { node.setCheckedState(checked ? CHECKED_STATE.CHECKED : CHECKED_STATE.UNCHECKED); } /** * Toggles all options. * @param {boolean} checked Whether the options should be checked or unchecked */ toggleAllOptions(checked) { this.nodesList.forEach(node => { Tree.toggleNodeState(node, checked); }); } /** * Toggles an option's checked state and propagates the state change to the * option's parents and children. * @param {object} param0 The option to be toggled * @param {boolean} checked Whether the option is checked * @param {boolean} propagateToParent Whether the state should be propagated to the parents */ toggleOption(_ref2, checked) { let { value } = _ref2; let propagateToParent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; const node = this.getNode(value); Tree.toggleNodeState(node, checked); if (node.isChild && propagateToParent) { this.toggleParentOption(node.parent); } if (node.isParent) { node.children.forEach(child => this.toggleOption(child, checked, false)); } } /** * Toggles a parent option's checked state. This is called as a result of a child option being * toggled by the user and the change being propagated to that option's parents. This method * recursively propagates the state changes to all the ancestors chain until we have reached the * tree's trunk. * @param {object} param0 The option to be toggled */ toggleParentOption(_ref3) { let { value } = _ref3; const node = this.getNode(value); if (this.optionHasAllChildrenChecked(node)) { node.checkedState = CHECKED_STATE.CHECKED; } else if (this.optionHasSomeChildrenChecked(node)) { node.checkedState = CHECKED_STATE.INDETERMINATE; } else { node.checkedState = CHECKED_STATE.UNCHECKED; } if (node.isChild) { this.toggleParentOption(node.parent); } } } export { Tree };