@gitlab/ui
Version:
GitLab UI Components
362 lines (310 loc) • 10.9 kB
JavaScript
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;