UNPKG

zent

Version:

一套前端设计语言和基于React的实现

460 lines (417 loc) 12.4 kB
import React, { Component, PropTypes } from 'react'; import assign from 'zent-utils/lodash/assign'; import classnames from 'zent-utils/classnames'; import Checkbox from './components/Checkbox'; // 记录是否已经触发收起展开逻辑 // 防止出现闪烁的bug let isTriggerSlide = false; const deepClone = (arr) => { let i; let copy; if (Array.isArray(arr)) { copy = arr.slice(0); for (i = 0; i < copy.length; i += 1) { copy[i] = deepClone(copy[i]); } return copy; } else if (typeof arr === 'object') { return assign({}, arr); } return arr; }; const toggleSlide = (el, isClose) => { if (!isClose) { el.style.display = 'block'; el.style.height = 0; } const maxDelay = 300; const height = el.scrollHeight; const speed = Math.max(height / maxDelay, 0.5); // px/ms let sum = 0; let start = null; const animate = (timestamp) => { if (!start) start = timestamp; const progress = timestamp - start; sum = progress * speed; el.style.height = `${isClose ? (height - sum) : sum}px`; if (height < sum) { if (isClose) { el.style.display = 'none'; } el.style.height = ''; isTriggerSlide = false; } else { window.requestAnimationFrame(animate); } }; window.requestAnimationFrame(animate); }; export default class Tree extends Component { constructor(props) { super(props); this.isInitial = true; this.isDataUpdate = false; this.state = { checkedTree: {} }; } static propTypes = { dataType: PropTypes.oneOf([ 'plain', 'tree' ]), data: PropTypes.arrayOf(PropTypes.object), isRoot: PropTypes.func, loadMore: PropTypes.func, foldable: PropTypes.bool, checkable: PropTypes.bool, autoExpandOnSelect: PropTypes.bool, defaultCheckedKeys: PropTypes.arrayOf(PropTypes.any), disabledCheckedKeys: PropTypes.arrayOf(PropTypes.any), onCheck: PropTypes.func, onExpand: PropTypes.func, onSelect: PropTypes.func, size: PropTypes.oneOf([ 'large', 'medium', 'small' ]), operations: PropTypes.arrayOf(PropTypes.object), render: PropTypes.func, prefix: PropTypes.string } static defaultProps = { autoExpandOnSelect: true, dataType: 'tree', foldable: true, checkable: false, size: 'medium', prefix: 'zent' } componentWillMount() { // init checkedTree const { data, dataType } = this.props; const formatData = this.formatDataIntoTree(data, dataType); this.setState({ checkedTree: this.formatDataIntoCheckedTree(formatData) }); } componentWillReceiveProps(nextProps) { if (nextProps.data !== this.props.data) { // update checkTree this.isDataUpdate = true; const data = this.formatDataIntoTree(nextProps.data, nextProps.dataType); this.setState({ checkedTree: this.formatDataIntoCheckedTree(data) }); } } formatDataIntoTree(data, dataType) { let roots = []; if (dataType === 'plain') { const { isRoot } = this.props; let map = {}; data.forEach((node) => { if (!node.isLeaf) { node.children = []; } map[node.id] = node; }); Object.keys(map).forEach((key) => { let node = map[key]; const isRootNode = (isRoot && isRoot(node)) || node.parentId === 0 || node.parentId === undefined || node.parentId === '0'; if (isRootNode) { roots.push(node); } else if (map[node.parentId]) { // 防止只删除父节点没有子节点的情况 map[node.parentId].children.push(node); } }); } else if (dataType === 'tree') { roots = data; } else { // console.error(`The dataType should be declared as plain/tree, but your dataType is ${dataType}, Please check your config.`) } return roots; } formatDataIntoCheckedTree(data) { let checkedTree = {}; if (this.isInitial) { checkedTree = this.initialCheckedTree(data); this.isInitial = false; } else if (this.isDataUpdate) { checkedTree = this.reloadCheckedTree(data); this.isDataUpdate = false; } this.updateWholeCheckedTree(checkedTree); return checkedTree; } isSwitcherExpanded(node) { return !node.parentNode.classList.contains('off'); } handleExpandClick(root, e) { const { loadMore } = this.props; if (loadMore) { if (!root.children || root.children.length === 0) { e.persist(); loadMore(root) .then(() => { this.handleFoldClick(root, e); }) .catch(() => {}); return; } } this.handleFoldClick(root, e); } handleFoldClick(root, e) { if (!isTriggerSlide) { const { onExpand } = this.props; const switcher = e.target; const elp = switcher.parentNode; elp.classList.toggle('off'); const isClose = !this.isSwitcherExpanded(switcher); if (onExpand) { onExpand(root, { isExpanded: !isClose }); } const el = elp.nextSibling; // no content, unmount switcher if (!el) { switcher.remove(); return; } isTriggerSlide = true; toggleSlide(el, isClose); } } triggerSwitcherClick(root, e) { const { autoExpandOnSelect, onSelect } = this.props; const target = e.currentTarget; if (onSelect) { onSelect(root, target); } if (target && autoExpandOnSelect) { const switcher = target.parentNode.previousSibling; if (switcher) { switcher.click(); } } } handleCheckboxClick(root) { const { onCheck } = this.props; const { checkedTree } = this.state; this.updateCheckedTree(root.id, checkedTree[root.id].t !== 2 ? 2 : 0); if (onCheck) { onCheck( Object .keys(checkedTree) .filter(k => checkedTree[k].t === 2) .map((x) => { if (typeof root.id === 'number') { x = +x; } return x; } )); } } updateUpstream(id, type, checkedTree) { if (!id) return; if (type === 2) { checkedTree[id].t = Object .keys(checkedTree) .filter(x => checkedTree[x].p === id) .every(x => checkedTree[x].t === 2) ? 2 : 1; } else if (type === 1) { checkedTree[id].t = 1; } else if (type === 0) { checkedTree[id].t = Object .keys(checkedTree) .filter(x => checkedTree[x].p === id) .every(x => checkedTree[x].t === 0) ? 0 : 1; } if (checkedTree[id].p) { this.updateUpstream(checkedTree[id].p, checkedTree[id].t, checkedTree); } } updateDownstream(id, type, checkedTree) { if (!id) return; checkedTree[id].t = type; const childrenId = Object .keys(checkedTree) .filter(x => checkedTree[x].p === id); if (childrenId.length > 0) { childrenId.forEach((childId) => { this.updateDownstream(childId, type, checkedTree); }); } } updateCheckedTree(id, type) { const { checkedTree } = this.state; const parentId = checkedTree[id].p; const childrenId = Object .keys(checkedTree) .filter(x => checkedTree[x].p === id.toString()); checkedTree[id].t = type; this.updateUpstream(parentId, type, checkedTree); childrenId.forEach((childId) => { this.updateDownstream(childId, type, checkedTree); }); this.setState({ checkedTree }); } updateCheckedTreeRecursive(root, parentId, func) { func(root, parentId); if (root.children && root.children.length > 0) { root.children.forEach((child) => { this.updateCheckedTreeRecursive(child, root.id, func); }); } } updateWholeCheckedTree(checkedTree) { Object.keys(checkedTree).forEach((id) => { if (checkedTree[id].t === 2) { this.updateUpstream(id, 2, checkedTree); this.updateDownstream(id, 2, checkedTree); } }); } initialCheckedTree(data) { let newCheckedTree = {}; const { defaultCheckedKeys } = this.props; data.forEach((tree) => { this.updateCheckedTreeRecursive(tree, '', (root, parentId) => { const isSetDefault = defaultCheckedKeys && defaultCheckedKeys.find(x => x === root.id) >= 0; newCheckedTree[root.id] = { p: parentId.toString(), t: isSetDefault ? 2 : 0 }; }); }); return newCheckedTree; } reloadCheckedTree(data) { let newCheckedTree = {}; const { checkedTree } = this.state; data.forEach((tree) => { this.updateCheckedTreeRecursive(tree, '', (root, parentId) => { newCheckedTree[root.id] = { p: parentId.toString(), t: checkedTree[root.id] ? checkedTree[root.id].t : 0 }; }); }); return newCheckedTree; } renderSwitcher(root) { const { foldable, loadMore } = this.props; const className = classnames('switcher'); if (!root.isLeaf && (loadMore || root.children && root.children.length > 0)) { return ( <icon className={className} onClick={foldable && this.handleExpandClick.bind(this, root)} /> ); } } renderCheckbox(root) { const { checkable, disabledCheckedKeys } = this.props; const isDisabled = (disabledCheckedKeys || []).find(key => key === root.id) >= 0; if (checkable) { return ( <Checkbox onCheck={this.handleCheckboxClick.bind(this, root)} type={this.state.checkedTree[root.id].t} disabled={isDisabled} /> ); } } renderOperations(root) { const opts = this.props.operations; if (opts) { const optNodes = opts.map((opt) => { const shouldRender = opt.shouldRender || (() => true); return shouldRender(root) && ( <span key={`${opt.name}-${root.id}`} onClick={opt.action.bind(null, root)} className="opt"> <icon className={opt.icon} />{opt.name} </span> ); }); return ( <div className="operation"> {optNodes} </div> ); } } // TODO: // Support selectable // Support disable select // Custom switcher // Add Cursor Style // make style beautiful renderTreeNodes(roots) { const { loadMore, prefix, expandAll } = this.props; if (roots && roots.length > 0) { return roots.map((root) => { // 单独节点的expand属性具有最高优先级,如果expand没有设置会根据是否设置loadMore // 来判断是否收起,因为需要loadMore的节点是没有内容的,需要收起。在以上情况都不发生 // 的情况下以expandAll为准 let isShowChildren = expandAll; if (loadMore) { isShowChildren = root.expand; } const barClassName = classnames(`${prefix}-tree-bar`, { off: !isShowChildren }); return ( <li key={`${root.id}`}> <div className={barClassName}> {this.renderSwitcher(root)} <div className="zent-tree-node"> {this.renderCheckbox(root)} <span className="content" onClick={this.triggerSwitcherClick.bind(this, root)}> { this.props.render ? this.props.render(root) : root.title } </span> {this.renderOperations(root)} </div> </div> { root.children && root.children.length > 0 && ( <ul key={`ul-${root.id}`} className={`${prefix}-tree-child`} style={isShowChildren ? {} : { display: 'none' }}> {this.renderTreeNodes(root.children)} </ul> ) } </li> ); }); } } render() { const { commonStyle, data, dataType, prefix, size } = this.props; const roots = this.formatDataIntoTree(deepClone(data), dataType); const treeNodes = this.renderTreeNodes(roots); const classNames = classnames(`${prefix}-tree`, { [`${prefix}-tree-${size}`]: size !== 'medium' }); return ( <ul className={classNames} style={commonStyle}> {treeNodes} </ul> ); } }