UNPKG

@jmarvinr/treejs

Version:

a lightweight tree widget, compatible with originaljs/react/vue, 9.6kb size for tree.min.js&tree.min.css without gzip.

504 lines (469 loc) 12.7 kB
import ajax from './ajax'; import './index.less'; function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } function uniq(arr) { const map = {}; return arr.reduce((acc, item) => { if (!map[item]) { map[item] = true; acc.push(item); } return acc; }, []); } function empty(ele) { while (ele.firstChild) { ele.removeChild(ele.firstChild); } } function animation(duration, callback) { requestAnimationFrame(() => { callback.enter(); requestAnimationFrame(() => { callback.active(); setTimeout(() => { callback.leave(); }, duration); }); }); } export default function Tree(container, options) { const defaultOptions = { selectMode: 'checkbox', values: [], disables: [], beforeLoad: null, loaded: null, url: null, method: 'GET', closeDepth: null, }; this.treeNodes = []; this.nodesById = {}; this.leafNodesById = {}; this.liElementsById = {}; this.willUpdateNodesById = {}; this.container = container; this.options = Object.assign(defaultOptions, options); Object.defineProperties(this, { values: { get() { return this.getValues(); }, set(values) { return this.setValues(uniq(values)); }, }, disables: { get() { return this.getDisables(); }, set(values) { return this.setDisables(uniq(values)); }, }, selectedNodes: { get() { let nodes = []; let nodesById = this.nodesById; for (let id in nodesById) { if ( nodesById.hasOwnProperty(id) && (nodesById[id].status === 1 || nodesById[id].status === 2) ) { const node = Object.assign({}, nodesById[id]); delete node.parent; delete node.children; nodes.push(node); } } return nodes; }, }, disabledNodes: { get() { let nodes = []; let nodesById = this.nodesById; for (let id in nodesById) { if (nodesById.hasOwnProperty(id) && nodesById[id].disabled) { let node = Object.assign({}, nodesById[id]); delete node.parent; nodes.push(node); } } return nodes; }, }, }); if (this.options.url) { this.load(data => { this.init(data); }); } else { this.init(this.options.data); } } Tree.prototype.init = function(data) { console.time('init'); let { treeNodes, nodesById, leafNodesById, defaultValues, defaultDisables, } = Tree.parseTreeData(data); this.treeNodes = treeNodes; this.nodesById = nodesById; this.leafNodesById = leafNodesById; this.render(this.treeNodes); const {values, disables, loaded} = this.options; if (values && values.length) defaultValues = values; defaultValues.length && this.setValues(defaultValues); if (disables && disables.length) defaultDisables = disables; defaultDisables.length && this.setDisables(defaultDisables); loaded && loaded.call(this); console.timeEnd('init'); }; Tree.prototype.load = function(callback) { console.time('load'); const {url, method, beforeLoad} = this.options; ajax({ url, method, success: result => { let data = result; console.timeEnd('load'); if (beforeLoad) { data = beforeLoad(result); } callback(data); }, }); }; Tree.prototype.render = function(treeNodes) { const treeEle = Tree.createRootEle(); treeEle.appendChild(this.buildTree(treeNodes, 0)); this.bindEvent(treeEle); const ele = document.querySelector(this.container); empty(ele); ele.appendChild(treeEle); }; Tree.prototype.buildTree = function(nodes, depth) { const rootUlEle = Tree.createUlEle(); if (nodes && nodes.length) { nodes.forEach(node => { const liEle = Tree.createLiEle( node, depth === this.options.closeDepth - 1 ); this.liElementsById[node.id] = liEle; let ulEle = null; if (node.children && node.children.length) { ulEle = this.buildTree(node.children, depth + 1); } ulEle && liEle.appendChild(ulEle); rootUlEle.appendChild(liEle); }); } return rootUlEle; }; Tree.prototype.bindEvent = function(ele) { ele.addEventListener( 'click', e => { const {target} = e; if ( target.nodeName === 'SPAN' && (target.classList.contains('treejs-checkbox') || target.classList.contains('treejs-label')) ) { this.onItemClick(target.parentNode.nodeId); } else if ( target.nodeName === 'LI' && target.classList.contains('treejs-node') ) { this.onItemClick(target.nodeId); } else if ( target.nodeName === 'SPAN' && target.classList.contains('treejs-switcher') ) { this.onSwitcherClick(target); } }, false ); }; Tree.prototype.onItemClick = function(id) { console.time('onItemClick'); const node = this.nodesById[id]; const {onChange} = this.options; if (!node.disabled) { this.setValue(id); this.updateLiElements(); } onChange && onChange.call(this); console.timeEnd('onItemClick'); }; Tree.prototype.setValue = function(value) { const node = this.nodesById[value]; if (!node) return; const prevStatus = node.status; const status = prevStatus === 1 || prevStatus === 2 ? 0 : 2; node.status = status; this.markWillUpdateNode(node); this.walkUp(node, 'status'); this.walkDown(node, 'status'); }; Tree.prototype.getValues = function() { const values = []; for (let id in this.leafNodesById) { if (this.leafNodesById.hasOwnProperty(id)) { if ( this.leafNodesById[id].status === 1 || this.leafNodesById[id].status === 2 ) { values.push(id); } } } return values; }; Tree.prototype.setValues = function(values) { this.emptyNodesCheckStatus(); values.forEach(value => { this.setValue(value); }); this.updateLiElements(); const {onChange} = this.options; onChange && onChange.call(this); }; Tree.prototype.setDisable = function(value) { const node = this.nodesById[value]; if (!node) return; const prevDisabled = node.disabled; if (!prevDisabled) { node.disabled = true; this.markWillUpdateNode(node); this.walkUp(node, 'disabled'); this.walkDown(node, 'disabled'); } }; Tree.prototype.getDisables = function() { const values = []; for (let id in this.leafNodesById) { if (this.leafNodesById.hasOwnProperty(id)) { if (this.leafNodesById[id].disabled) { values.push(id); } } } return values; }; Tree.prototype.setDisables = function(values) { this.emptyNodesDisable(); values.forEach(value => { this.setDisable(value); }); this.updateLiElements(); }; Tree.prototype.emptyNodesCheckStatus = function() { this.willUpdateNodesById = this.getSelectedNodesById(); Object.values(this.willUpdateNodesById).forEach(node => { if (!node.disabled) node.status = 0; }); }; Tree.prototype.emptyNodesDisable = function() { this.willUpdateNodesById = this.getDisabledNodesById(); Object.values(this.willUpdateNodesById).forEach(node => { node.disabled = false; }); }; Tree.prototype.getSelectedNodesById = function() { return Object.entries(this.nodesById).reduce((acc, [id, node]) => { if (node.status === 1 || node.status === 2) { acc[id] = node; } return acc; }, {}); }; Tree.prototype.getDisabledNodesById = function() { return Object.entries(this.nodesById).reduce((acc, [id, node]) => { if (node.disabled) { acc[id] = node; } return acc; }, {}); }; Tree.prototype.updateLiElements = function() { Object.values(this.willUpdateNodesById).forEach(node => { this.updateLiElement(node); }); this.willUpdateNodesById = {}; }; Tree.prototype.markWillUpdateNode = function(node) { this.willUpdateNodesById[node.id] = node; }; Tree.prototype.onSwitcherClick = function(target) { const liEle = target.parentNode; const ele = liEle.lastChild; const height = ele.scrollHeight; if (liEle.classList.contains('treejs-node__close')) { animation(150, { enter() { ele.style.height = 0; ele.style.opacity = 0; }, active() { ele.style.height = `${height}px`; ele.style.opacity = 1; }, leave() { ele.style.height = ''; ele.style.opacity = ''; liEle.classList.remove('treejs-node__close'); }, }); } else { animation(150, { enter() { ele.style.height = `${height}px`; ele.style.opacity = 1; }, active() { ele.style.height = 0; ele.style.opacity = 0; }, leave() { ele.style.height = ''; ele.style.opacity = ''; liEle.classList.add('treejs-node__close'); }, }); } }; Tree.prototype.walkUp = function(node, changeState) { const {parent} = node; if (parent) { if (changeState === 'status') { let pStatus = null; const statusCount = parent.children.reduce((acc, child) => { if (!isNaN(child.status)) return acc + child.status; return acc; }, 0); if (statusCount) { pStatus = statusCount === parent.children.length * 2 ? 2 : 1; } else { pStatus = 0; } if (parent.status === pStatus) return; parent.status = pStatus; } else { const pDisabled = parent.children.reduce( (acc, child) => acc && child.disabled, true ); if (parent.disabled === pDisabled) return; parent.disabled = pDisabled; } this.markWillUpdateNode(parent); this.walkUp(parent, changeState); } }; Tree.prototype.walkDown = function(node, changeState) { if (node.children && node.children.length) { node.children.forEach(child => { if (changeState === 'status' && child.disabled) return; child[changeState] = node[changeState]; this.markWillUpdateNode(child); this.walkDown(child, changeState); }); } }; Tree.prototype.updateLiElement = function(node) { const {classList} = this.liElementsById[node.id]; switch (node.status) { case 0: classList.remove('treejs-node__halfchecked', 'treejs-node__checked'); break; case 1: classList.remove('treejs-node__checked'); classList.add('treejs-node__halfchecked'); break; case 2: classList.remove('treejs-node__halfchecked'); classList.add('treejs-node__checked'); break; } switch (node.disabled) { case true: if (!classList.contains('treejs-node__disabled')) classList.add('treejs-node__disabled'); break; case false: if (classList.contains('treejs-node__disabled')) classList.remove('treejs-node__disabled'); break; } }; Tree.parseTreeData = function(data) { const treeNodes = deepClone(data); const nodesById = {}; const leafNodesById = {}; const values = []; const disables = []; const walkTree = function(nodes, parent) { nodes.forEach(node => { nodesById[node.id] = node; if (node.checked) values.push(node.id); if (node.disabled) disables.push(node.id); if (parent) node.parent = parent; if (node.children && node.children.length) { walkTree(node.children, node); } else { leafNodesById[node.id] = node; } }); }; walkTree(treeNodes); return { treeNodes, nodesById, leafNodesById, defaultValues: values, defaultDisables: disables, }; }; Tree.createRootEle = function() { const div = document.createElement('div'); div.classList.add('treejs'); return div; }; Tree.createUlEle = function() { const ul = document.createElement('ul'); ul.classList.add('treejs-nodes'); return ul; }; Tree.createLiEle = function(node, closed) { const li = document.createElement('li'); li.classList.add('treejs-node'); if (closed) li.classList.add('treejs-node__close'); if (node.children && node.children.length) { const switcher = document.createElement('span'); switcher.classList.add('treejs-switcher'); li.appendChild(switcher); } else { li.classList.add('treejs-placeholder'); } const checkbox = document.createElement('span'); checkbox.classList.add('treejs-checkbox'); li.appendChild(checkbox); const label = document.createElement('span'); label.classList.add('treejs-label'); const text = document.createTextNode(node.text); label.appendChild(text); li.appendChild(label); li.nodeId = node.id; return li; };