UNPKG

@gitlab/ui

Version:
362 lines (310 loc) • 10.9 kB
import Node from './node'; import { CHECKED_STATE } from './constants'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var Tree = /*#__PURE__*/function () { function Tree(options, selected) { _classCallCheck(this, Tree); this.treeDepth = 0; this.nodes = {}; this.toggleAllOptions = this.toggleAllOptions.bind(this); this.initNodes(options, selected); this.initIndeterminateStates(); } /** * @returns {[Node]} The tree as an array of Node instances */ _createClass(Tree, [{ key: "initNodes", /** * 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 */ value: function initNodes() { var _this = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var selected = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var parent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var depth = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; if (!options.length) { return; } this.treeDepth = depth > this.treeDepth ? depth : this.treeDepth; options.forEach(function (option) { var isChecked = selected.includes(option.value); _this.nodes[option.value] = new Node(_objectSpread2(_objectSpread2({}, option), {}, { parent: parent, isChecked: isChecked, depth: 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. */ }, { key: "initIndeterminateStates", value: function initIndeterminateStates() { var _this2 = this; var nodes = _toConsumableArray(this.nodesList); var _loop = function _loop(i) { var removeIndices = []; nodes.forEach(function (node, nodeIndex) { if (node.depth === i && node.isUnchecked) { node.setCheckedState(_this2.optionHasSomeChildrenChecked(node) ? CHECKED_STATE.INDETERMINATE : node.checkedState); removeIndices.push(nodeIndex); } }); removeIndices.reverse().forEach(function (index) { nodes.splice(index, 1); }); }; for (var i = this.treeDepth; i >= 0; i -= 1) { _loop(i); } } /** * Returns true if all of the option's children are checked, false otherwise. * @param {object} option * @returns {boolean} */ }, { key: "optionHasAllChildrenChecked", value: function optionHasAllChildrenChecked(option) { return this.getOptionChildren(option).every(function (child) { return 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} */ }, { key: "optionHasSomeChildrenChecked", value: function optionHasSomeChildrenChecked(option) { return this.getOptionChildren(option).some(function (child) { return child.isCheckedOrIndeterminate; }); } /** * Returns the Node instance for a given option's value. * @param {number|string} value The option's value * @returns {Node} */ }, { key: "getNode", value: function getNode(value) { return this.nodes[value]; } /** * Returns the option's children as Node instances. * @param {object} option * @returns {[Node]} */ }, { key: "getOptionChildren", value: function getOptionChildren(option) { var _this3 = this; return option.children.map(function (_ref) { var value = _ref.value; return _this3.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 */ }, { key: "toggleAllOptions", /** * Toggles all options. * @param {boolean} checked Whether the options should be checked or unchecked */ value: function toggleAllOptions(checked) { this.nodesList.forEach(function (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 */ }, { key: "toggleOption", value: function toggleOption(_ref2, checked) { var _this4 = this; var value = _ref2.value; var propagateToParent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; var node = this.getNode(value); Tree.toggleNodeState(node, checked); if (node.isChild && propagateToParent) { this.toggleParentOption(node.parent); } if (node.isParent) { node.children.forEach(function (child) { return _this4.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 */ }, { key: "toggleParentOption", value: function toggleParentOption(_ref3) { var value = _ref3.value; var 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); } } }, { key: "nodesList", get: function get() { return Object.values(this.nodes); } /** * @returns {array} The values currently selected */ }, { key: "selected", get: function get() { return this.nodesList.filter(function (node) { return node.isChecked; }).map(function (node) { return node.value; }); } /** * @returns {boolean} Whether all options are checked */ }, { key: "allOptionsChecked", get: function get() { return this.selected.length === this.nodesList.length; } /** * @returns {boolean} Whether some, but not all options are checked */ }, { key: "someOptionsChecked", get: function get() { return this.selected.length > 0 && !this.allOptionsChecked; } }], [{ key: "toggleNodeState", value: function toggleNodeState(node, checked) { node.setCheckedState(checked ? CHECKED_STATE.CHECKED : CHECKED_STATE.UNCHECKED); } }]); return Tree; }(); export default Tree;