balm-ui
Version:
A modular and customizable UI library based on Material Design and Vue 3
558 lines (463 loc) • 15.3 kB
JavaScript
const checkLeaf = (item, isLeaf, hasChildren) => item[isLeaf] || !hasChildren;
const getNode = (
{ selectedValue, nodeMap, dataFormat },
originNodeData,
{ level, parentKey }
) => {
let item = Object.assign({}, originNodeData);
const { value, children, hasChildren, isLeaf, disabled } = dataFormat;
const nodeKey = item[value];
const nodeChildren = Array.isArray(item[children]) ? item[children] : [];
const nodeHasChildren = item[hasChildren] || nodeChildren.length;
const nodeIsLeaf = checkLeaf(item, isLeaf, nodeHasChildren);
item.level = level;
item.isRoot = !level;
item.isLeaf = nodeIsLeaf;
item.expanded = false;
item.selected = !Array.isArray(selectedValue) && nodeKey === selectedValue;
item.checked =
Array.isArray(selectedValue) && selectedValue.includes(nodeKey);
item.parentKey = parentKey;
item.disabled = item[disabled];
if (!nodeChildren.length) {
item[children] = [];
}
if (!nodeIsLeaf) {
item.indeterminate = false;
}
if (!nodeMap.has(nodeKey)) {
nodeMap.set(nodeKey, item);
}
return item;
};
let selectedNodes = [];
let parentKeys = [];
class MdcTree {
constructor(treeData) {
this.treeData = treeData;
}
getData(nodes, level = 0, parentKey = '') {
const { dataFormat, maxLevel } = this.treeData;
const list = [];
const { value, children, hasChildren } = dataFormat;
for (let i = 0, len = nodes.length; i < len; i++) {
let item = getNode(this.treeData, nodes[i], {
level,
parentKey
});
const nodeChildren = Array.isArray(item[children]) ? item[children] : [];
const nodeHasChildren = item[hasChildren] || nodeChildren.length;
if (level < maxLevel && nodeHasChildren) {
item[children] = this.getData(nodeChildren, level + 1, item[value]);
}
list.push(item);
}
return list;
}
/** For tree node */
static addData(treeData, item, nodes) {
const { dataFormat, nodeMap } = treeData;
const list = [];
const { value, children } = dataFormat;
const level = item.level + 1;
const parentKey = item[value];
for (let i = 0, len = nodes.length; i < len; i++) {
let subitem = getNode(treeData, nodes[i], {
level,
parentKey
});
if (subitem.checked) {
this.setMultipleSelectedValue(treeData, subitem[value], true);
}
list.push(subitem);
}
item[children] = list;
item.expanded = true;
nodeMap.set(parentKey, item);
}
static async onExpand(treeData, item) {
if (treeData.loadData) {
const { dataFormat } = treeData;
const hasChildren =
item[dataFormat.children] && item[dataFormat.children].length;
if (hasChildren) {
item.expanded = !item.expanded;
} else {
const nodes = await treeData.loadData(item[dataFormat.value], item);
if (Array.isArray(nodes)) {
this.addData(treeData, item, nodes);
// review parent node
const allChecked = nodes.every((node) =>
treeData.selectedValue.includes(node[dataFormat.value])
);
const hasChecked = allChecked
? true
: nodes.some((node) =>
treeData.selectedValue.includes(node[dataFormat.value])
);
if (hasChecked) {
if (allChecked || nodes.length === 1) {
const parentNodeKey = nodes[0][dataFormat.parentKey];
if (parentNodeKey) {
treeData.selectedValue.push(parentNodeKey);
item.checked = true;
} else {
console.warn('[UiTree]', 'Missing `parentKey`');
}
} else {
item.indeterminate = true;
}
}
} else {
console.warn('[UiTree]', 'Invalid data');
}
}
} else {
item.expanded = !item.expanded;
}
}
static async collapseAllNode(treeData, nodes) {
const { dataFormat, nodeMap } = treeData;
for await (let node of nodes) {
const nodeKey = node[dataFormat.value];
const item = nodeMap.get(nodeKey);
item.expanded = false;
if (item.children && item.children.length) {
this.collapseAllNode(treeData, item.children);
}
}
return true;
}
/** For single tree **/
static setSingleSelectedValue(treeData, nodeKey, selected) {
const { nodeMap } = treeData;
const item = nodeMap.get(nodeKey);
if (item) {
item.selected = selected;
treeData.selectedEvent = {
selected,
selectedNodes: nodeKey,
node: item
};
}
}
static onSelect(treeData, item) {
const { dataFormat, selectedValue } = treeData;
const nodeKey = item[dataFormat.value];
if (selectedValue) {
this.setSingleSelectedValue(treeData, selectedValue, false);
}
treeData.selectedValue = nodeKey;
this.setSingleSelectedValue(treeData, nodeKey, true);
}
/** For multiple tree **/
static setMultipleSelectedValue(treeData, currentNodeKey, checked) {
const { dataFormat, nodeMap, filterParentNode } = treeData;
const item = nodeMap.get(currentNodeKey);
if (checked && !item.indeterminate) {
if (!treeData.selectedValue.includes(currentNodeKey)) {
if (filterParentNode) {
item.isLeaf && treeData.selectedValue.push(currentNodeKey);
} else {
treeData.selectedValue.push(currentNodeKey);
}
}
} else {
treeData.selectedValue = treeData.selectedValue.filter(
(value) => value !== currentNodeKey
);
}
}
static setChildrenCheckedValue(treeData, nodes, checked) {
const { dataFormat, nodeMap } = treeData;
const { value, children } = dataFormat;
for (let i = 0, len = nodes.length; i < len; i++) {
let item = Object.assign({}, nodes[i]);
const nodeKey = item[value];
const nodeChildren = item[children];
const subitem = nodeMap.get(nodeKey);
if (subitem) {
if (checked) {
!subitem.checked && selectedNodes.push(nodeKey);
} else {
subitem.checked && selectedNodes.push(nodeKey);
}
subitem.indeterminate = false;
subitem.checked = checked;
this.setMultipleSelectedValue(treeData, nodeKey, checked);
}
if (!item.isLeaf && nodeChildren.length) {
this.setChildrenCheckedValue(treeData, nodeChildren, checked);
}
}
}
static setParentCheckedValue(treeData, item) {
const { dataFormat, nodeMap } = treeData;
const { value, children } = dataFormat;
if (item) {
const nodeKey = item[value];
const nodeChildren = item[children];
const nodeCheckedList = nodeChildren.filter(
(subitem) => subitem.checked || subitem.indeterminate
);
const subitem = nodeMap.get(nodeKey);
if (nodeCheckedList.length) {
const checkedAllList = nodeCheckedList.filter(
(subitem) => subitem.checked
).length;
const checkedAll = checkedAllList === nodeChildren.length;
if (checkedAll) {
!subitem.checked && selectedNodes.push(nodeKey);
} else {
subitem.checked && selectedNodes.push(nodeKey);
}
subitem.checked = checkedAll;
subitem.indeterminate = !checkedAll;
this.setMultipleSelectedValue(treeData, nodeKey, checkedAll);
} else {
subitem.checked = false;
subitem.indeterminate = false;
this.setMultipleSelectedValue(treeData, nodeKey, subitem.checked);
}
if (!item.isRoot) {
this.setParentCheckedValue(treeData, nodeMap.get(item.parentKey));
}
}
}
static onCheck(treeData, item, forceChecked = null) {
let checked = !item.checked;
if (typeof forceChecked === 'boolean') {
checked = forceChecked;
}
const { dataFormat, nodeMap, singleChecked } = treeData;
const { value, children } = dataFormat;
const nodeKey = item[value];
const nodeChildren = item[children];
if (singleChecked) {
item.checked = checked;
this.setMultipleSelectedValue(treeData, nodeKey, checked);
treeData.selectedEvent = {
checked,
checkedNodes: [nodeKey],
node: item
};
} else {
selectedNodes = [nodeKey];
if (item.isLeaf) {
item.checked = checked;
this.setMultipleSelectedValue(treeData, nodeKey, checked);
} else {
if (item.indeterminate) {
item.indeterminate = false;
checked = true;
}
item.checked = checked;
this.setMultipleSelectedValue(treeData, nodeKey, checked);
this.setChildrenCheckedValue(treeData, nodeChildren, checked);
}
if (!item.isRoot) {
this.setParentCheckedValue(treeData, nodeMap.get(item.parentKey));
}
treeData.selectedEvent = {
checked,
checkedNodes: selectedNodes,
node: item
};
}
}
/** For expanding **/
static async handleExpandKeys(treeData, nodes, defaultExpandedKeys) {
const { dataFormat, nodeMap } = treeData;
for await (let node of nodes) {
const nodeKey = node[dataFormat.value];
const item = nodeMap.get(nodeKey);
defaultExpandedKeys.includes(nodeKey) && this.onExpand(treeData, item);
if (node.children && node.children.length) {
this.handleExpandKeys(treeData, node.children, defaultExpandedKeys);
}
}
}
static async handleExpandAll(treeData, nodes) {
const { dataFormat, nodeMap } = treeData;
for await (let node of nodes) {
const nodeKey = node[dataFormat.value];
const item = nodeMap.get(nodeKey);
this.onExpand(treeData, item);
if (item.children && item.children.length) {
this.handleExpandAll(treeData, item.children);
}
}
}
static async findTreeNode(tree, key, value) {
if (tree[key] === value) return tree;
if (tree.children && tree.children.length) {
for (let i = 0; i < tree.children.length; i++) {
const node = await this.findTreeNode(tree.children[i], key, value);
if (node !== null) return node;
}
}
return null;
}
static toReverseArray(arr) {
const newArr = [];
for (let i = arr.length - 1; i >= 0; i--) {
newArr.push(arr[i]);
}
return newArr;
}
static async handleAutoExpandSelected(
nodeList,
key,
selectedValue,
treeData
) {
const allNodeCollapsed = await this.collapseAllNode(treeData, nodeList);
if (allNodeCollapsed) {
const result = await this.findTreeNode(nodeList[0], key, selectedValue);
parentKeys.push(result[key]);
if (result.parentKey) {
this.handleAutoExpandSelected(
nodeList,
key,
result.parentKey,
treeData
);
}
if (!result.parentKey) {
const reversedArr = this.toReverseArray(parentKeys);
treeData && this.handleExpandKeys(treeData, nodeList, reversedArr);
}
}
}
/** For init tree **/
static async setExpanded(
treeData,
nodeList,
{ autoExpandParent, defaultExpandedKeys, autoExpandAll }
) {
const { dataFormat, nodeMap } = treeData;
if (autoExpandAll) {
this.handleExpandAll(treeData, nodeList);
}
if (autoExpandParent) {
if (defaultExpandedKeys.length) {
this.handleExpandKeys(treeData, nodeList, defaultExpandedKeys);
} else {
for await (let node of nodeList) {
const nodeKey = node[dataFormat.value];
const item = nodeMap.get(nodeKey);
this.onExpand(treeData, item);
}
}
}
}
static resetSelected(treeData, oldSelectedKeys) {
const { nodeMap } = treeData;
for (let i = 0, len = oldSelectedKeys.length; i < len; i++) {
const nodeKey = oldSelectedKeys[i];
const item = nodeMap.get(nodeKey);
if (item) {
this.onCheck(treeData, item, false);
}
}
}
static setSelected(
treeData,
newSelectedKeys,
{ nodeList, autoExpandSelected }
) {
const { dataFormat, nodeMap, multiple } = treeData;
const selectedKeys = Array.isArray(newSelectedKeys)
? newSelectedKeys
: [newSelectedKeys];
for (let i = 0, len = selectedKeys.length; i < len; i++) {
const nodeKey = selectedKeys[i];
const item = nodeMap.get(nodeKey);
if (item) {
multiple
? this.onCheck(treeData, item, true)
: this.onSelect(treeData, item);
}
}
if (autoExpandSelected && !Array.isArray(newSelectedKeys)) {
parentKeys = [];
this.handleAutoExpandSelected(
nodeList,
dataFormat.value,
newSelectedKeys,
treeData
);
}
}
/** For tree operation **/
static async createNode(treeData, parentKey, originNodeData) {
const { dataFormat, nodeMap } = treeData;
const { value, children, hasChildren } = dataFormat;
const parentItem = nodeMap.get(parentKey);
const nodeKey = originNodeData[value];
let item = getNode(treeData, originNodeData, {
level: parentItem.level + 1,
parentKey,
checked: false
});
if (parentItem.isLeaf) {
parentItem[children].unshift(item);
if (!parentItem[hasChildren]) {
parentItem[hasChildren] = true;
}
parentItem.isLeaf = false;
} else {
if (parentItem[hasChildren]) {
if (parentItem[children].length) {
parentItem[children].unshift(item);
} else {
await this.onExpand(treeData, parentItem);
}
} else {
parentItem[children].unshift(item);
parentItem[hasChildren] = true;
parentItem.expanded = true;
}
}
nodeMap.set(parentKey, parentItem);
nodeMap.set(nodeKey, item);
}
static updateNode(treeData, parentKey, originNodeData) {
const { dataFormat, nodeMap } = treeData;
const { value, children } = dataFormat;
const nodeKey = originNodeData[value];
const item = nodeMap.get(nodeKey);
Object.keys(item).forEach((key) => {
if (typeof originNodeData[key] !== 'undefined') {
item[key] = originNodeData[key];
}
});
const parentItem = nodeMap.get(parentKey);
const parentChildren = parentItem[children];
const index = parentChildren.findIndex((item) => item[value] === nodeKey);
parentItem[children][index] = item;
nodeMap.set(parentKey, parentItem);
nodeMap.set(nodeKey, item);
}
static deleteNode(treeData, parentKey, originNodeData) {
const { dataFormat, nodeMap } = treeData;
const { value, children, hasChildren } = dataFormat;
const nodeKey = originNodeData[value];
if (nodeMap.has(nodeKey)) {
const parentItem = nodeMap.get(parentKey);
const parentChildren = parentItem[children];
parentChildren.splice(
parentChildren.findIndex((item) => item[value] === nodeKey),
1
);
parentItem[hasChildren] = parentChildren.length;
if (!parentItem[hasChildren]) {
parentItem.isLeaf = true;
parentItem.expanded = false;
}
nodeMap.set(parentKey, parentItem);
nodeMap.delete(nodeKey);
}
}
}
export { MdcTree };