@gitlab/ui
Version:
GitLab UI Components
200 lines (183 loc) • 6.15 kB
JavaScript
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 };