diff --git a/src/tree-select/tree-select.jsx b/src/tree-select/tree-select.jsx index 92e379d8c1..7752cbdd78 100644 --- a/src/tree-select/tree-select.jsx +++ b/src/tree-select/tree-select.jsx @@ -319,10 +319,10 @@ export default class TreeSelect extends Component { switch (treeCheckedStrategy) { case 'parent': - keys = filterChildKey(keys, this._k2n); + keys = filterChildKey(keys, this._k2n, this._p2n); break; case 'child': - keys = filterParentKey(keys, this._k2n); + keys = filterParentKey(keys, this._k2n, this._p2n); break; default: break; diff --git a/src/tree/view/tree.jsx b/src/tree/view/tree.jsx index 0fa29001e5..551d53bada 100644 --- a/src/tree/view/tree.jsx +++ b/src/tree/view/tree.jsx @@ -1,3 +1,4 @@ +/* eslint-disable max-depth */ import React, { Component, Children, cloneElement } from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; @@ -10,6 +11,8 @@ import { filterChildKey, filterParentKey, getAllCheckedKeys, + forEachEnableNode, + isNodeChecked, } from './util'; const { bindCtx, noop } = func; @@ -703,19 +706,32 @@ export default class Tree extends Component { const pos = this._k2n[key].pos; - const ps = Object.keys(this._p2n); - ps.forEach(p => { - if (isDescendantOrSelf(pos, p)) { - this.processKey(checkedKeys, this._p2n[p].key, check); - } + forEachEnableNode(this._k2n[key], node => { + if (node.checkable === false) return; + this.processKey(checkedKeys, node.key, check); }); + const ps = Object.keys(this._p2n); + // ps.forEach(p => { + // if (this._p2n[p].checkable !== false && !this._p2n[p].disabled && isDescendantOrSelf(pos, p)) { + // this.processKey(checkedKeys, this._p2n[p].key, check); + // } + // }); + let currentPos = pos; const nums = pos.split('-'); for (let i = nums.length; i > 2; i--) { let parentCheck = true; const parentPos = nums.slice(0, i - 1).join('-'); + if ( + this._p2n[parentPos].disabled || + this._p2n[parentPos].checkboxDisabled || + this._p2n[parentPos].checkable === false + ) { + currentPos = parentPos; + continue; + } const parentKey = this._p2n[parentPos].key; const parentChecked = checkedKeys.indexOf(parentKey) > -1; if (!check && !parentChecked) { @@ -724,12 +740,34 @@ export default class Tree extends Component { for (let j = 0; j < ps.length; j++) { const p = ps[j]; - if (isSiblingOrSelf(currentPos, p)) { - const k = this._p2n[p].key; - if (checkedKeys.indexOf(k) === -1) { + const pnode = this._p2n[p]; + if ( + isSiblingOrSelf(currentPos, p) && + !pnode.disabled && + !pnode.checkboxDisabled + ) { + const k = pnode.key; + if (pnode.checkable === false) { + // eslint-disable-next-line max-depth + if (!pnode.children || pnode.children.length === 0) + continue; + + // eslint-disable-next-line max-depth + for (let m = 0; m < pnode.children.length; m++) { + if ( + !pnode.children.every(child => + isNodeChecked(child, checkedKeys) + ) + ) { + parentCheck = false; + break; + } + } + } else if (checkedKeys.indexOf(k) === -1) { parentCheck = false; - break; } + + if (!parentCheck) break; } } @@ -749,10 +787,18 @@ export default class Tree extends Component { let newCheckedKeys; switch (checkedStrategy) { case 'parent': - newCheckedKeys = filterChildKey(checkedKeys, this._k2n); + newCheckedKeys = filterChildKey( + checkedKeys, + this._k2n, + this._p2n + ); break; case 'child': - newCheckedKeys = filterParentKey(checkedKeys, this._k2n); + newCheckedKeys = filterParentKey( + checkedKeys, + this._k2n, + this._p2n + ); break; default: newCheckedKeys = checkedKeys; @@ -819,17 +865,24 @@ export default class Tree extends Component { } getIndeterminateKeys(checkedKeys) { + if (this.props.checkStrictly) { + return []; + } + const indeterminateKeys = []; const poss = filterChildKey( checkedKeys.filter(key => !!this._k2n[key]), - this._k2n + this._k2n, + this._p2n ).map(key => this._k2n[key].pos); poss.forEach(pos => { const nums = pos.split('-'); for (let i = nums.length; i > 2; i--) { const parentPos = nums.slice(0, i - 1).join('-'); - const parentKey = this._p2n[parentPos].key; + const parent = this._p2n[parentPos]; + if (parent.disabled || parent.checkboxDisabled) break; + const parentKey = parent.key; if (indeterminateKeys.indexOf(parentKey) === -1) { indeterminateKeys.push(parentKey); } diff --git a/src/tree/view/util.js b/src/tree/view/util.js index 2ba679cfea..89dbbd7f68 100644 --- a/src/tree/view/util.js +++ b/src/tree/view/util.js @@ -10,49 +10,109 @@ export function normalizeToArray(keys) { return []; } + +/** + * 判断子节点是否是选中状态,如果 checkable={false} 则向下递归, + * @param {Node} child + * @param {Array} checkedKeys + */ +export function isNodeChecked(node, checkedKeys) { + if (node.disabled || parent.checkboxDisabled) return true; + /* istanbul ignore next */ + if (node.checkable === false) { + return ( + !node.children || + node.children.length === 0 || + node.children.every(c => isNodeChecked(c, checkedKeys)) + ); + } + return checkedKeys.indexOf(node.key) > -1; +} + +/** + * 遍历所有可用的子节点 + * @param {Node} + * @param {Function} callback + */ +export function forEachEnableNode(node, callback = () => {}) { + if (node.disabled || node.checkboxDisabled) return; + // eslint-disable-next-line callback-return + callback(node); + if (node.children && node.children.length > 0) { + node.children.forEach(child => forEachEnableNode(child, callback)); + } +} +/** + * 判断节点是否禁用checked + * @param {Node} node + * @returns {Boolean} + */ +export function isNodeDisabledChecked(node) { + if (node.disabled || node.checkboxDisabled) return true; + /* istanbul ignore next */ + if (node.checkable === false) { + return ( + !node.children || + node.children.length === 0 || + node.children.every(isNodeDisabledChecked) + ); + } + + return false; +} + +/** + * 递归获取一个 checkable = {true} 的父节点,当 checkable={false} 时继续往上查找 + * @param {Node} node + * @param {Map} _p2n + * @return {Node} + */ +export function getCheckableParentNode(node, _p2n) { + let parentPos = node.pos.split(['-']); + if (parentPos.length === 2) return node; + parentPos.splice(parentPos.length - 1, 1); + parentPos = parentPos.join('-'); + const parentNode = _p2n[parentPos]; + if (parentNode.disabled || parentNode.checkboxDisabled) return false; + /* istanbul ignore next */ + if (parentNode.checkable === false) { + return getCheckableParentNode(parentNode, _p2n); + } + + return parentNode; +} /** * 过滤子节点 * @param {Array} keys * @param {Object} _k2n */ -export function filterChildKey(keys, _k2n) { - const newKeys = [...keys] - .filter(key => !!_k2n[key]) - .sort((prev, next) => { - return getDepth(prev, _k2n) - getDepth(next, _k2n); - }); - - for (let i = 0; i < newKeys.length; i++) { - for (let j = 0; j < newKeys.length; j++) { - if ( - i !== j && - isDescendantOrSelf(_k2n[newKeys[i]].pos, _k2n[newKeys[j]].pos) - ) { - newKeys.splice(j, 1); - j--; - } +export function filterChildKey(keys, _k2n, _p2n) { + const newKeys = []; + keys.forEach(key => { + const node = getCheckableParentNode(_k2n[key], _p2n); + if ( + !node || + node.checkable === false || + node === _k2n[key] || + keys.indexOf(node.key) === -1 + ) { + newKeys.push(key); } - } - + }); return newKeys; } -export function filterParentKey(keys, _k2n) { - const newKeys = [...keys] - .filter(key => !!_k2n[key]) - .sort((prev, next) => { - return getDepth(next, _k2n) - getDepth(prev, _k2n); - }); +export function filterParentKey(keys, _k2n, _p2n) { + const newKeys = []; - for (let i = 0; i < newKeys.length; i++) { - for (let j = 0; j < newKeys.length; j++) { - if ( - i !== j && - isDescendantOrSelf(_k2n[newKeys[j]].pos, _k2n[newKeys[i]].pos) - ) { - newKeys.splice(j, 1); - j--; - } + for (let i = 0; i < keys.length; i++) { + const node = _k2n[keys[i]]; + if ( + !node.children || + node.children.length === 0 || + node.children.every(isNodeDisabledChecked) + ) { + newKeys.push(keys[i]); } } @@ -91,10 +151,22 @@ export function isSiblingOrSelf(currentPos, targetPos) { export function getAllCheckedKeys(checkedKeys, _k2n, _p2n) { checkedKeys = normalizeToArray(checkedKeys); const filteredKeys = checkedKeys.filter(key => !!_k2n[key]); - let flatKeys = filterChildKey(filteredKeys, _k2n); - const childChecked = child => flatKeys.indexOf(child.key) > -1; - const removeKey = child => flatKeys.splice(flatKeys.indexOf(child.key), 1); + const flatKeys = filterChildKey(filteredKeys, _k2n, _p2n); + + const removeKey = child => { + if (child.disabled || child.checkboxDisabled) return; + if ( + child.checkable === false && + child.children && + child.children.length > 0 + ) { + return child.children.forEach(removeKey); + } + flatKeys.splice(flatKeys.indexOf(child.key), 1); + }; + const addParentKey = (i, parent) => flatKeys.splice(i, 0, parent.key); + const keys = [...flatKeys]; for (let i = 0; i < keys.length; i++) { const pos = _k2n[keys[i]].pos; @@ -105,7 +177,15 @@ export function getAllCheckedKeys(checkedKeys, _k2n, _p2n) { for (let j = nums.length - 2; j > 0; j--) { const parentPos = nums.slice(0, j + 1).join('-'); const parent = _p2n[parentPos]; - const parentChecked = parent.children.every(childChecked); + if ( + parent.checkable === false || + parent.disabled || + parent.checkboxDisabled + ) + continue; + const parentChecked = parent.children.every(child => + isNodeChecked(child, flatKeys) + ); if (parentChecked) { parent.children.forEach(removeKey); addParentKey(i, parent); @@ -116,20 +196,12 @@ export function getAllCheckedKeys(checkedKeys, _k2n, _p2n) { } const newKeys = []; - if (flatKeys.length) { - flatKeys = flatKeys.reverse(); - const ps = Object.keys(_p2n); - for (let i = 0; i < flatKeys.length; i++) { - const pos = _k2n[flatKeys[i]].pos; - for (let j = 0; j < ps.length; j++) { - if (isDescendantOrSelf(pos, ps[j])) { - newKeys.push(_p2n[ps[j]].key); - ps.splice(j, 1); - j--; - } - } - } - } + flatKeys.forEach(key => { + forEachEnableNode(_k2n[key], node => { + if (node.checkable === false) return; + newKeys.push(node.key); + }); + }); return newKeys; } diff --git a/test/tree-select/index-spec.js b/test/tree-select/index-spec.js index 1f60cdc2f6..5b93db90f5 100644 --- a/test/tree-select/index-spec.js +++ b/test/tree-select/index-spec.js @@ -310,19 +310,20 @@ describe('TreeSelect', () => { let triggered = false; const initValue = '4'; const appendValue = '6'; - const expectValue = ['3', '4']; + const expectValue = ['4', '3']; const handleChange = (value, data) => { triggered = true; assert.deepEqual(value, expectValue); assert.deepEqual(data, expectValue.map(v => _v2n[v])); }; - wrapper = mount( @@ -361,7 +362,9 @@ describe('TreeSelect', () => { wrapper = mount( @@ -387,12 +390,14 @@ describe('TreeSelect', () => { defaultVisible treeDefaultExpandAll treeCheckable - dataSource={dataSource} + dataSource={cloneData(dataSource, { + 2: { disabled: false } + })} defaultValue={['6']} treeCheckedStrategy="all" /> ); - assert.deepEqual(getLabels(wrapper), ['裙子', '女装']); + assert.deepEqual(getLabels(wrapper), ['女装', '裙子']); wrapper .find('div.next-tag') @@ -567,13 +572,13 @@ describe('TreeSelect', () => { }); }); -function cloneData(data, keyMap = {}) { +function cloneData(data, valueMap = {}) { const loop = data => data.map(item => { let newItem; - if (item.key in keyMap) { - newItem = { ...item, ...keyMap[item.key] }; + if (item.value in valueMap) { + newItem = { ...item, ...valueMap[item.value] }; } else { newItem = { ...item }; } diff --git a/test/tree/index-spec.js b/test/tree/index-spec.js index 5c7a78810c..5b3e0b063a 100644 --- a/test/tree/index-spec.js +++ b/test/tree/index-spec.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import assert from 'power-assert'; import ReactTestUtils from 'react-dom/test-utils'; -import { dom, KEYCODE, func } from '../../src/util'; +import { dom, KEYCODE } from '../../src/util'; import Tree from '../../src/tree/index'; import '../../src/tree/style.js'; @@ -111,7 +111,9 @@ class CheckDemo extends Component { defaultExpandAll checkable checkedKeys={this.state.checkedKeys} - dataSource={dataSource} + dataSource={cloneData(dataSource, { + 2: { disabled: false } + })} onCheck={this.handleCheck} {...this.props} /> @@ -411,8 +413,8 @@ describe('Tree', () => { ); assertChecked('3', true); assertChecked('6', true); - assertChecked('1', false); - assertIndeterminate('1', true); + assertChecked('1', true); + assertIndeterminate('1', false); assert( hasClass( findTreeNodeByKey('4').querySelector('.next-checkbox-wrapper'), @@ -452,7 +454,9 @@ describe('Tree', () => { , mountNode @@ -474,7 +478,9 @@ describe('Tree', () => { checkable checkedStrategy="parent" defaultExpandAll - dataSource={dataSource} + dataSource={cloneData(dataSource, { + 2: { disabled: false } + })} onCheck={handleCheck} />, mountNode @@ -704,6 +710,47 @@ describe('Tree', () => { assert(hasClass(findTreeNodeByKey('1'), 'next-filtered')); }); + it('should support disabled', () => { + ReactDOM.render( + , + mountNode + ); + + ['1', '3', '6'].forEach(key => assertChecked(key, true)); + checkTreeNode('6'); + checkTreeNode('4'); + ['1', '3', '6'].forEach(key => assertChecked(key, false)); + assertChecked('4', true); + }) + + it('should support checkable = false', () => { + ReactDOM.render( + , + mountNode + ); + ['3', '6'].forEach(key => assertChecked(key, true)); + assertIndeterminate('1', true); + checkTreeNode('4'); + checkTreeNode('5'); + ['1', '3', '6'].forEach(key => assertChecked(key, true)); + assertIndeterminate('1', false); + }); + it('should support keyboard', () => { ReactDOM.render(