├── .eslintignore ├── .yo-rc.json ├── tests ├── index.js ├── utils.spec.js ├── CascadeMultiModal.spec.js ├── CascadeMultiPanel.spec.js ├── const.js └── CascadeMultiSelect.spec.js ├── .eslintrc.json ├── src ├── index.js ├── locale.js ├── utils.js ├── CascadeMultiModal.jsx ├── CascadeMultiSelect.less ├── CascadeMultiSelect.jsx └── CascadeMultiPanel.jsx ├── .npmignore ├── .gitignore ├── demo ├── index.js ├── CascadeMultiSelectDemo.less ├── DemoForReact16.js ├── const.js └── CascadeMultiSelectDemo.js ├── index.html ├── .travis.yml ├── mock └── query │ ├── child.json │ └── firstLevel.json ├── package.json ├── HISTORY.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | build/ 3 | mock/ 4 | dist/ -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-uxcore": {} 3 | } 4 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * only require other specs here 3 | */ 4 | 5 | const req = require.context('.', false, /\.spec\.js(x)?$/); 6 | req.keys().forEach(req); 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "plugins": [ 5 | "react" 6 | ], 7 | "env": { 8 | "browser": true 9 | }, 10 | "rules": { 11 | "import/no-extraneous-dependencies": "off", 12 | "react/jsx-no-bind": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMultiSelect Component for uxcore 3 | * @author changming 4 | * 5 | * Copyright 2015-2017, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | import CascadeMultiSelect from './CascadeMultiSelect'; 9 | 10 | export default CascadeMultiSelect; 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | *.cfg 3 | node_modules/ 4 | nohup.out 5 | *.iml 6 | .idea/ 7 | .ipr 8 | .iws 9 | *~ 10 | ~* 11 | *.diff 12 | *.log 13 | *.patch 14 | *.bak 15 | .DS_Store 16 | Thumbs.db 17 | .project 18 | .*proj 19 | .svn/ 20 | *.swp 21 | out/ 22 | .build 23 | .happypack 24 | node_modules 25 | _site 26 | sea-modules 27 | spm_modules 28 | .cache 29 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea/ 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | .build 21 | node_modules 22 | _site 23 | sea-modules 24 | spm_modules 25 | .cache 26 | .happypack 27 | dist 28 | build 29 | assets/**/*.css 30 | coverage 31 | package-lock.json 32 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMultiSelect Component Demo for uxcore 3 | * @author guyunxiang 4 | * 5 | * Copyright 2015-2016, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | 9 | const ReactDOM = require('react-dom'); 10 | const React = require('react'); 11 | const Demo = require('./CascadeMultiSelectDemo'); 12 | ReactDOM.render(, document.getElementById('UXCoreDemo')); 13 | -------------------------------------------------------------------------------- /demo/CascadeMultiSelectDemo.less: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMultiSelect Component Demo Style for Uxcore 3 | * @author guyunxiang 4 | * 5 | * Copyright 2015-2016, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | 9 | @import "../node_modules/kuma-base/theme/orange"; 10 | @import "../node_modules/uxcore-cascade-select/src/CascadeSelect"; 11 | @import "../src/CascadeMultiSelect.less"; 12 | 13 | body { 14 | margin-bottom: 400px; 15 | background-color: #fff; 16 | p { 17 | margin: 10px 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | uxcore-cascade-multi-select 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/utils.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import { getDisabledValueLabel, getCascadeSelected } from '../src/utils'; 3 | import { options } from './const'; 4 | 5 | describe('utils function', () => { 6 | it('getDisabledValueLabel: return correct disabledNodes and leafNodes', () => { 7 | const leafNodes = [{ value: 'baotuquan', label: '趵突泉' }]; 8 | const result = { 9 | disabledNodes: [{ 10 | value: 'jinan', 11 | label: '济南', 12 | disabled: true, 13 | children: leafNodes, 14 | }], 15 | leafNodes, 16 | }; 17 | expect(getDisabledValueLabel(options, 'jinan')).to.eql(result); 18 | }); 19 | 20 | it('getCascadeSelected: return CascadeSelected Array', () => { 21 | expect(getCascadeSelected(options, 'shandong')[0].value).to.be('shandong'); 22 | }); 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - xvfb 9 | 10 | notification: 11 | email: 12 | - wsj7552715@hotmail.com 13 | 14 | node_js: 15 | - 6.9.0 16 | 17 | before_install: 18 | - | 19 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qve '(\.md$)|(\.html$)' 20 | then 21 | echo "Only docs were updated, stopping build process." 22 | exit 23 | fi 24 | phantomjs --version 25 | install: 26 | - export DISPLAY=':99.0' 27 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 28 | - npm install 29 | 30 | 31 | script: 32 | - | 33 | if [ "$TEST_TYPE" = test ]; then 34 | npm test 35 | else 36 | npm run $TEST_TYPE 37 | fi 38 | env: 39 | matrix: 40 | - TEST_TYPE=test 41 | - TEST_TYPE=coverage 42 | - TEST_TYPE=saucelabs 43 | 44 | matrix: 45 | allow_failures: 46 | - env: "TEST_TYPE=saucelabs" -------------------------------------------------------------------------------- /src/locale.js: -------------------------------------------------------------------------------- 1 | const locale = { 2 | 'zh-cn': { 3 | noData: '', 4 | selected: '已选择', 5 | clean: '清空', 6 | haveAll: '已全选', 7 | all: '全部', 8 | placeholder: '请选择', 9 | delete: '删除', 10 | close: '收起', 11 | expandAll: '展开全部', 12 | item: '项', 13 | title: '级联选择', 14 | ok: '确定', 15 | cancel: '取消', 16 | filter: '请输入关键词过滤', 17 | }, 18 | 'en-us': { 19 | noData: '', 20 | selected: 'Selected', 21 | clean: 'Clean', 22 | haveAll: 'Have All', 23 | all: 'All', 24 | placeholder: 'Please Select', 25 | delete: 'Delete', 26 | close: 'Close', 27 | expandAll: 'Expand All ', 28 | item: ' Item', 29 | title: 'Cascade Multi Select', 30 | ok: 'Ok', 31 | cancel: 'Cancel', 32 | filter: 'Please input keywords to filter', 33 | }, 34 | }; 35 | 36 | export default (key) => { 37 | if (locale[key]) { 38 | return locale[key]; 39 | } 40 | return locale['zh-cn']; 41 | }; 42 | -------------------------------------------------------------------------------- /mock/query/child.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [{ 3 | "value": "测试二级类目", 4 | "key": "TEST00", 5 | "parentName": "TEST", 6 | "leaf": false, 7 | "fullNamePath": "0\/TEST\/TEST00" 8 | }, { 9 | "value": "测试二级类目超长名称样式测试,二级类目超长名称样式测试", 10 | "key": "TEST01", 11 | "parentName": "TEST", 12 | "leaf": false, 13 | "fullNamePath": "0\/TEST\/TEST01" 14 | }, { 15 | "value": "测试二级类目超长名称样式测试,二级类目超长名称样式测试", 16 | "key": "TEST02", 17 | "parentName": "TEST", 18 | "leaf": false, 19 | "fullNamePath": "0\/TEST\/TEST02" 20 | }, { 21 | "value": "测试二级类目超长名称样式测试,二级类目超长名称样式测试", 22 | "key": "TEST03", 23 | "parentName": "TEST", 24 | "leaf": false, 25 | "fullNamePath": "0\/TEST\/TEST03" 26 | }, { 27 | "value": "测试二级类目超长名称样式测试,二级类目超长名称样式测试", 28 | "key": "TEST04", 29 | "parentName": "TEST", 30 | "leaf": false, 31 | "fullNamePath": "0\/TEST\/TEST04" 32 | }], 33 | "hasError": false 34 | } 35 | -------------------------------------------------------------------------------- /demo/DemoForReact16.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMultiSelect Component Demo for uxcore 3 | * @author guyunxiang 4 | * 5 | * Copyright 2015-2016, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | import React from 'react'; 9 | import CascadeMultiSelect from '../src'; 10 | 11 | const dynamicData = [ 12 | { 13 | value: 1, 14 | label: 'one', 15 | children: [ 16 | { 17 | value: 2, 18 | label: 'two', 19 | children: [{ 20 | value: 3, 21 | label: 'three', 22 | children: null, 23 | }], 24 | }, 25 | ], 26 | }, 27 | ]; 28 | 29 | class Demo extends React.Component { 30 | 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | dynamicData, 35 | }; 36 | } 37 | 38 | render() { 39 | return ( 40 |
41 |
42 |

动态

43 |
44 |
45 | console.log('onOk', params)} 48 | onChange={(...params) => console.log('onChange', params)} 49 | cascadeSize={4} 50 | onItemClick={(s, level) => { 51 | if (level === 3) { 52 | const newData = this.state.dynamicData.concat([]); 53 | newData[0].children[0].children[0].children = [{ value: 5, label: 'five' }]; 54 | this.setState({ dynamicData: newData }, () => { 55 | // console.log(this.state.dynamicData); 56 | }); 57 | } 58 | }} 59 | /> 60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | export default Demo; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uxcore-cascade-multi-select", 3 | "version": "0.7.2", 4 | "description": "uxcore-cascade-multi-select component for uxcore.", 5 | "repository": "https://github.com/uxcore/uxcore-cascade-multi-select.git", 6 | "author": "eternalsky", 7 | "main": "build/index.js", 8 | "scripts": { 9 | "start": "uxcore-tools run start", 10 | "server": "uxcore-tools run server", 11 | "lint": "uxcore-tools run lint", 12 | "build": "uxcore-tools run build", 13 | "test": "uxcore-tools run test", 14 | "coverage": "uxcore-tools run coverage", 15 | "pub": "uxcore-tools run pub", 16 | "dep": "uxcore-tools run dep", 17 | "tnpm-dep": "uxcore-tools run tnpm-dep", 18 | "chrome": "uxcore-tools run chrome", 19 | "browsers": "uxcore-tools run browsers", 20 | "saucelabs": "uxcore-tools run saucelabs", 21 | "update": "uxcore-tools run update", 22 | "tnpm-update": "uxcore-tools run tnpm-update" 23 | }, 24 | "bugs": { 25 | "url": "http://github.com/uxcore/uxcore-cascade-multi-select/issues" 26 | }, 27 | "keywords": [ 28 | "react", 29 | "react-component", 30 | "uxcore-cascade-multi-select", 31 | "CascadeMultiSelect", 32 | "component" 33 | ], 34 | "devDependencies": { 35 | "console-polyfill": "*", 36 | "enzyme": "*", 37 | "expect.js": "*", 38 | "kuma-base": "^1.10.1", 39 | "react": "16.x", 40 | "react-dom": "16.x", 41 | "react-test-renderer": "16.x", 42 | "uxcore-tools": "^0.3.0", 43 | "uxcore-cascade-select": "*", 44 | "uxcore-kuma": "*", 45 | "babel-polyfill": "6.x", 46 | "enzyme-adapter-react-16": "1.x" 47 | }, 48 | "dependencies": { 49 | "classnames": "^2.1.2", 50 | "lodash": "^4.17.4", 51 | "object-assign": "^4.0.0", 52 | "prop-types": "15.x", 53 | "uxcore-button": "^0.4.16", 54 | "uxcore-dialog": "^0.7.7", 55 | "uxcore-dropdown": "^0.4.1" 56 | }, 57 | "contributors": [], 58 | "license": "MIT" 59 | } -------------------------------------------------------------------------------- /tests/CascadeMultiModal.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import React from 'react'; 3 | import Enzyme from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import deepcopy from 'lodash/cloneDeep'; 6 | import CascadeMultiModal from '../src/CascadeMultiModal'; 7 | import { options } from './const'; 8 | const { mount, shallow } = Enzyme; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | describe('CascadeMultiModal', () => { 13 | it('default displayValue', () => { 14 | const wrapper = shallow( 15 | 19 | ); 20 | expect(wrapper.find('.kuma-cascade-multi-model-result-ul-list-content').text()) 21 | .to.be('西湖'); 22 | }); 23 | 24 | it('delete label', () => { 25 | const wrapper = shallow( 26 | 30 | ); 31 | wrapper.find('.kuma-cascade-multi-model-result-ul-list-remove') 32 | .at(0).simulate('click'); 33 | expect(wrapper.state('value')).to.eql([]); 34 | }); 35 | 36 | it('test onOk', () => { 37 | class Demo extends React.Component { 38 | constructor(props) { 39 | super(props); 40 | this.state = { 41 | value: [] 42 | }; 43 | } 44 | 45 | onOk = (valueList, labelList, leafList) => { 46 | this.setState({ value: leafList.map(item => item.value) }); 47 | } 48 | 49 | render() { 50 | return ( 51 | 56 | ); 57 | } 58 | } 59 | const wrapper = mount(); 60 | wrapper.find('button').simulate('click'); 61 | const overlay = mount(wrapper.find('Dialog').props().children); 62 | overlay.find('.kuma-cascade-multi-content').at(0) 63 | .find('.kuma-cascade-multi-list-item').at(1) 64 | .find('s').simulate('click'); 65 | wrapper.find('CascadeMultiModal').instance().onOk(); 66 | expect(wrapper.state('value')).to.eql(['zhonghuamen']); 67 | }); 68 | }); -------------------------------------------------------------------------------- /tests/CascadeMultiPanel.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import React from 'react'; 3 | import Enzyme from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import deepcopy from 'lodash/cloneDeep'; 6 | import CascadeMultiPanel from '../src/CascadeMultiPanel'; 7 | import { options } from './const'; 8 | const { mount, shallow } = Enzyme; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | describe('CascadeMultiPanel', () => { 13 | it('default render Panel', () => { 14 | const wrapper = shallow( 15 | 19 | ); 20 | expect(wrapper.find('.kuma-cascade-multi-content').at(2) 21 | .find('.kuma-cascade-multi-list-item-active') 22 | .prop('title')) 23 | .to.be('西湖'); 24 | }); 25 | 26 | it('change list when item is Clicked', () => { 27 | const wrapper = mount( 28 | 32 | ); 33 | const lists = wrapper.find('.kuma-cascade-multi-content'); 34 | const clickItem = lists.at(0).find('.kuma-cascade-multi-list-item').at(1); 35 | clickItem.simulate('click'); 36 | expect(wrapper.find('.kuma-cascade-multi-content').at(0).find('.kuma-cascade-multi-list-item') 37 | .at(1) 38 | .prop('className') 39 | .indexOf('active')) 40 | .to.be.greaterThan(-1); 41 | expect(wrapper.find('.kuma-cascade-multi-content').at(1) 42 | .find('.kuma-cascade-multi-list-item-active') 43 | .at(0) 44 | .prop('title')).to.be('南京'); 45 | }); 46 | 47 | it('trigger ResultPanel node', () => { 48 | const wrapper = mount( 49 | 53 | ); 54 | wrapper.find('.tree-node-ul-li-open').at(0).simulate('click'); 55 | expect(wrapper.find('.tree-node-ul').length).to.be(1); 56 | }); 57 | 58 | it('keywords search feature will be ok', () => { 59 | const wrapper = mount( 60 | 64 | ); 65 | wrapper.find('.kuma-input-small-size').at(0).value = '浙江'; 66 | setTimeout(() => { 67 | expect(wrapper.find('.kuma-cascade-multi-content').at(0).find('.kuma-cascade-multi-list-item').length).to.be(1); 68 | }, 300); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # history 2 | 3 | ## 0.8.0 4 | 5 | * `UPDATE`: remove deprecated lifecycle methods in react 16 6 | 7 | ## 0.7.2 8 | 9 | * `ADD`: add props.size 10 | 11 | ## 0.7.1 12 | 13 | * `FEATURE`: props.config add showSearch property 14 | 15 | ## 0.7.0 16 | 17 | * `ADD`: isCleanDisabledLabel prop 18 | 19 | ## 0.6.3 20 | 21 | * `ADD`: keyCouldDuplicated prop 22 | * `FIX`: remove Clear Button when all items are disabled 23 | 24 | ## 0.6.2 25 | 26 | * `ADD`: display all selection levels when use the independent Panel. 27 | 28 | ## 0.6.1 29 | 30 | * `ADD`: add a new attribute "disabled" to item of options array to enable/disable checkbox. 31 | 32 | ## 0.6.0 33 | 34 | * `UPDATE`: react 15 35 | 36 | ## 0.5.8 37 | 38 | * `FIX`: Panel position can not be float:left when used independently 39 | 40 | ## 0.5.7 41 | 42 | * `FIX`: trigger the onSelect event when click the close icon. 43 | 44 | ## 0.5.6 45 | 46 | * `FEAT`: add readOnly prop 47 | 48 | ## 0.5.5 49 | 50 | * `FEAT`: add beforeRender prop 51 | 52 | ## 0.5.4 53 | 54 | * `FEAT`: 添加选中的级联结构数据 55 | 56 | ## 0.5.3 57 | 58 | * `FIX`: remove console 59 | 60 | ## 0.5.2 61 | 62 | * `FIX`: 修复 options 动态改变无法重新渲染的问题 63 | 64 | ## 0.5.1 65 | 66 | * `FEAT`: onItemClick 第三个参数为当前选中的所有数据 67 | 68 | ## 0.5.0 69 | 70 | * `FIXED`: dom-align fail to work 71 | 72 | ## 0.4.0 73 | 74 | * `CHANGED`: update `uxcore-dialog` to ^0.7.0 75 | 76 | ## 0.3.3 77 | 78 | * `Fixed`: fix issue #13. 79 | 80 | ## 0.3.2 81 | 82 | * `FIXED`: fix visual problems. 83 | 84 | ## 0.3.1 85 | 86 | * `FIXED`: fix cascader width 87 | 88 | ## 0.3.0 89 | 90 | * `New Feature`: update to new Style. 91 | 92 | ## 0.2.7 93 | 94 | * `FIXED`: remove transparent split. 95 | 96 | ## 0.2.6 97 | 98 | * `FIXED` ie9+ result panel width style error 99 | 100 | ## 0.2.3 101 | 102 | * `FIXED` footer's button click, dropdown not hidden 103 | * `FIXED` demo async options error 104 | * `FIXED` demo input value sync update error 105 | 106 | ## 0.2.2 107 | 108 | * `FIXED` button not in center 109 | 110 | ## 0.2.1 111 | 112 | * `NEW` add ok button 113 | * `FIXED` missing style 114 | * `CHANGED` onSelect will pass leafList 115 | 116 | ## 0.2.0 117 | 118 | * `NEW` modal view 119 | * `NEW` add footer, add confirm button 120 | * `UPDATE` result panel style 121 | 122 | ## 0.1.5 123 | 124 | * `FIXED` props.value rerender error 125 | 126 | ## 0.1.2 127 | 128 | * `FIXED` fix style file name 129 | -------------------------------------------------------------------------------- /tests/const.js: -------------------------------------------------------------------------------- 1 | export const options = [ 2 | { 3 | value: 'zhejiang', 4 | label: '浙江', 5 | children: [{ 6 | value: 'hangzhou', 7 | label: '杭州', 8 | children: [{ 9 | value: 'xihu', 10 | label: '西湖', 11 | disabled: true, 12 | }, { 13 | value: 'bingjiang', 14 | label: '滨江', 15 | }], 16 | }, { 17 | value: 'ningbo', 18 | label: '宁波', 19 | children: [{ 20 | value: 'zhoushan', 21 | label: '舟山', 22 | }], 23 | }, { 24 | value: 'yiwu', 25 | label: '义乌', 26 | children: [{ 27 | value: 'jinhua', 28 | label: '金华', 29 | }], 30 | }, { 31 | value: 'changxing', 32 | label: '长兴', 33 | children: [], 34 | }], 35 | }, { 36 | value: 'jiangsu', 37 | label: '江苏', 38 | children: [{ 39 | value: 'nanjing', 40 | label: '南京', 41 | children: [{ 42 | value: 'zhonghuamen', 43 | label: '中华门', 44 | }], 45 | }], 46 | }, { 47 | value: 'shandong', 48 | label: '山东', 49 | children: [{ 50 | value: 'jinan', 51 | label: '济南', 52 | disabled: true, 53 | children: [{ 54 | value: 'baotuquan', 55 | label: '趵突泉', 56 | }], 57 | }, { 58 | value: 'test', 59 | label: 'test', 60 | }], 61 | }, { 62 | value: 'longname-0', 63 | label: '名称很长的选项展示效果0', 64 | children: [{ 65 | value: 'longname-0-0', 66 | label: '名称很长的选项展示效果0-0', 67 | children: [{ 68 | value: 'longname-0-0-0', 69 | label: '名称很长的选项展示效果0-0-0', 70 | }], 71 | }], 72 | }, 73 | ]; 74 | 75 | export const optionsWithDescription = [ 76 | { 77 | value: 'zhejiang', 78 | label: '浙江', 79 | description: '这是浙江', 80 | children: [{ 81 | value: 'hangzhou', 82 | label: '杭州', 83 | description: '这是杭州', 84 | children: [{ 85 | value: 'xihu', 86 | label: '西湖', 87 | description: '这是西湖', 88 | disabled: true, 89 | }, { 90 | value: 'bingjiang', 91 | label: '滨江', 92 | description: '这是滨江', 93 | }], 94 | }], 95 | }, { 96 | value: 'jiangsu', 97 | label: '江苏', 98 | description: '这是江苏', 99 | children: [{ 100 | value: 'nanjing', 101 | label: '南京', 102 | description: '这是南京', 103 | children: [{ 104 | value: 'zhonghuamen', 105 | label: '中华门', 106 | description: '这是中华门', 107 | }], 108 | }], 109 | } 110 | ]; 111 | -------------------------------------------------------------------------------- /mock/query/firstLevel.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [{ 3 | "value": "测试", 4 | "key": "TEST", 5 | "parentName": "ROOT", 6 | "leaf": false, 7 | "fullNamePath": "0\/TEST" 8 | }, { 9 | "value": "测试1", 10 | "key": "TEST1", 11 | "parentName": "ROOT", 12 | "leaf": false, 13 | "fullNamePath": "0\/TEST1" 14 | }, { 15 | "value": "测试2", 16 | "key": "TEST2", 17 | "parentName": "ROOT", 18 | "leaf": false, 19 | "fullNamePath": "0\/TEST2" 20 | }, { 21 | "value": "测试3", 22 | "key": "TEST3", 23 | "parentName": "ROOT", 24 | "leaf": false, 25 | "fullNamePath": "0\/TEST3" 26 | }, { 27 | "value": "测试4", 28 | "key": "TEST4", 29 | "parentName": "ROOT", 30 | "leaf": false, 31 | "fullNamePath": "0\/TEST4" 32 | }, { 33 | "value": "测试5", 34 | "key": "TEST5", 35 | "parentName": "ROOT", 36 | "leaf": false, 37 | "fullNamePath": "0\/TEST5" 38 | }, { 39 | "value": "测试6", 40 | "key": "TEST6", 41 | "parentName": "ROOT", 42 | "leaf": false, 43 | "fullNamePath": "0\/TEST6" 44 | }, { 45 | "value": "测试7", 46 | "key": "TEST7", 47 | "parentName": "ROOT", 48 | "leaf": false, 49 | "fullNamePath": "0\/TEST7" 50 | }, { 51 | "value": "测试8", 52 | "key": "TEST8", 53 | "parentName": "ROOT", 54 | "leaf": false, 55 | "fullNamePath": "0\/TEST8" 56 | }, { 57 | "value": "测试9", 58 | "key": "TEST9", 59 | "parentName": "ROOT", 60 | "leaf": false, 61 | "fullNamePath": "0\/TEST9" 62 | }, { 63 | "value": "测试10", 64 | "key": "TEST10", 65 | "parentName": "ROOT", 66 | "leaf": false, 67 | "fullNamePath": "0\/TEST10" 68 | }, { 69 | "value": "测试11", 70 | "key": "TEST11", 71 | "parentName": "ROOT", 72 | "leaf": false, 73 | "fullNamePath": "0\/TEST11" 74 | }, { 75 | "value": "测试12", 76 | "key": "TEST12", 77 | "parentName": "ROOT", 78 | "leaf": false, 79 | "fullNamePath": "0\/TEST12" 80 | }, { 81 | "value": "测试13", 82 | "key": "TEST13", 83 | "parentName": "ROOT", 84 | "leaf": false, 85 | "fullNamePath": "0\/TEST13" 86 | }, { 87 | "value": "测试一级类目超长名称展示效果,一级类目超长名称展示效果,一级类目超长名称展示效果", 88 | "key": "TEST14", 89 | "parentName": "ROOT", 90 | "leaf": false, 91 | "fullNamePath": "0\/TEST14" 92 | }], 93 | "hasError": false 94 | } 95 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import deepcopy from 'lodash/cloneDeep'; 2 | 3 | /** 4 | * 获取选中的disabled节点 5 | * @param {*} dataList 6 | * @param {*} value 7 | */ 8 | export function getDisabledValueLabel(dataList, value) { 9 | const disabledNodes = []; 10 | let leafNodes = []; 11 | 12 | /** 13 | * @param {*} list 14 | * @param {*} isNoNeedCheck 父级是被选中,则子级默认为选中状态 15 | * @param {*} isLeafNode 当为true时,直接进入为「筛选叶子节点的」方法 16 | */ 17 | function recursion(list, isNoNeedCheck = false, isLeafNode = false) { 18 | list.forEach(item => { 19 | const isChecked = value.indexOf(item.value) > -1 || isNoNeedCheck; 20 | const hasChildren = item.children && item.children.length; 21 | const disabled = item.disabled; 22 | if (isLeafNode) { 23 | if (hasChildren) { 24 | recursion(item.children, false, isLeafNode); 25 | } else { 26 | leafNodes.push(item); 27 | } 28 | return; 29 | } 30 | if (isChecked && disabled) { 31 | disabledNodes.push(item); 32 | if (hasChildren) { 33 | recursion(item.children, false, true); 34 | } else { 35 | leafNodes.push(item); 36 | } 37 | } else if (hasChildren) { 38 | recursion(item.children, isChecked, false); 39 | } 40 | }); 41 | } 42 | 43 | recursion(dataList); 44 | leafNodes = leafNodes.map(item => 45 | ({ 46 | value: item.value, 47 | label: item.label, 48 | }) 49 | ); 50 | 51 | return { 52 | disabledNodes, leafNodes, 53 | }; 54 | } 55 | 56 | const getCheckedIndexs = (dataList, values) => { 57 | const result = []; 58 | function recursion(data, level = '0') { 59 | data.forEach((item, i) => { 60 | const index = `${level}-${i}`; 61 | item.pos = index; // eslint-disable-line 62 | if (values.indexOf(item.value) > -1) { 63 | result.push(index); 64 | } else if (item.children && item.children.length) { 65 | recursion(item.children, index); 66 | } 67 | }); 68 | } 69 | recursion(dataList); 70 | return result; 71 | }; 72 | 73 | function checkStr(values, str) { 74 | return values.some((value) => value.indexOf(str) === 0); 75 | } 76 | 77 | export const getCascadeSelected = (data, values) => { 78 | const ret = deepcopy(data); 79 | const checkedIndex = getCheckedIndexs(ret, values); 80 | function recursion(dataList) { 81 | for (let i = 0; i < dataList.length;) { 82 | if (!checkStr(checkedIndex, dataList[i].pos)) { 83 | dataList.splice(i, 1); 84 | continue; 85 | } 86 | if (dataList[i].children && dataList[i].children.length) { 87 | recursion(dataList[i].children); 88 | } 89 | i += 1; 90 | } 91 | } 92 | 93 | recursion(ret); 94 | return ret; 95 | }; 96 | 97 | export const getWidthStyle = (dom, defaultWidth) => { 98 | const reg = /[0-9]+/g; 99 | if (dom) { 100 | const width = getComputedStyle(dom).width; 101 | if (width) { 102 | return width.match(reg)[0]; 103 | } 104 | } 105 | return defaultWidth; 106 | }; 107 | -------------------------------------------------------------------------------- /demo/const.js: -------------------------------------------------------------------------------- 1 | export const options = [ 2 | { 3 | value: 'zhejiang', 4 | label: '浙江', 5 | children: [{ 6 | value: 'hangzhou', 7 | label: '杭州', 8 | children: [{ 9 | value: 'xihu', 10 | label: '西湖', 11 | disabled: true, 12 | }, { 13 | value: 'bingjiang', 14 | label: '滨江', 15 | }], 16 | }, { 17 | value: 'ningbo', 18 | label: '宁波', 19 | children: [{ 20 | value: 'zhoushan', 21 | label: '舟山', 22 | }], 23 | }, { 24 | value: 'yiwu', 25 | label: '义乌', 26 | children: [{ 27 | value: 'jinhua', 28 | label: '金华', 29 | }], 30 | }, { 31 | value: 'changxing', 32 | label: '长兴', 33 | children: [], 34 | }], 35 | }, { 36 | value: 'jiangsu', 37 | label: '江苏', 38 | children: [{ 39 | value: 'nanjing', 40 | label: '南京', 41 | children: [{ 42 | value: 'zhonghuamen', 43 | label: '中华门', 44 | }], 45 | }], 46 | }, { 47 | value: 'shandong', 48 | label: '山东', 49 | children: [{ 50 | value: 'jinan', 51 | label: '济南', 52 | children: [{ 53 | value: 'baotuquan', 54 | label: '趵突泉', 55 | }], 56 | }], 57 | }, { 58 | value: 'longname-0', 59 | label: '名称很长的选项展示效果0', 60 | children: [{ 61 | value: 'longname-0-0', 62 | label: '名称很长的选项展示效果0-0', 63 | children: [{ 64 | value: 'longname-0-0-0', 65 | label: '名称很长的选项展示效果0-0-0', 66 | }], 67 | }], 68 | }, 69 | ]; 70 | 71 | export const optionsWithDescription = [ 72 | { 73 | value: 'zhejiang', 74 | label: '浙江', 75 | description: '这是浙江', 76 | children: [{ 77 | value: 'hangzhou', 78 | label: '杭州', 79 | description: '这是杭州', 80 | children: [{ 81 | value: 'xihu', 82 | label: '西湖', 83 | description: '这是西湖', 84 | disabled: true, 85 | }, { 86 | value: 'bingjiang', 87 | label: '滨江', 88 | description: '这是滨江', 89 | }], 90 | }], 91 | }, { 92 | value: 'jiangsu', 93 | label: '江苏', 94 | description: '这是江苏', 95 | children: [{ 96 | value: 'nanjing', 97 | label: '南京', 98 | description: '这是南京', 99 | children: [{ 100 | value: 'zhonghuamen', 101 | label: '中华门', 102 | description: '这是中华门', 103 | }], 104 | }], 105 | } 106 | ]; 107 | 108 | // export const options2 = [ 109 | // { 110 | // "children": [ 111 | // { 112 | // "children": [ 113 | // { "disabled": false, "label": "d", "value": 287374 }, 114 | // { "disabled": false, "label": "c", "value": 287375 }, 115 | // { "disabled": false, "label": "b", "value": 287376 }, 116 | // { "disabled": false, "label": "a", "value": 287377 } 117 | // ], 118 | // "disabled": false, 119 | // "label": "cj_test1", 120 | // "value": 1018 121 | // }, 122 | // { 123 | // "children": [ 124 | // { "disabled": false, "label": "test3", "value": 287378 } 125 | // ], 126 | // "disabled": false, 127 | // "label": "cj_test_2", 128 | // "value": 1019 129 | // } 130 | // ], 131 | // "disabled": false, 132 | // "label": "存己_操作2", 133 | // "value": 736695 134 | // }, 135 | // { 136 | // "children": [ 137 | // { 138 | // "children": [ 139 | // { "disabled": false, "label": "d", "value": 287383 }, 140 | // { "disabled": false, "label": "c", "value": 287384 }, 141 | // { "disabled": false, "label": "b", "value": 287385 }, 142 | // { "disabled": false, "label": "a", "value": 287386 } 143 | // ], 144 | // "disabled": false, 145 | // "label": "cj_test1", 146 | // "value": 1018 147 | // }, { 148 | // "children": [ 149 | // { "disabled": false, "label": "test3", "value": 287387 } 150 | // ], 151 | // "disabled": false, 152 | // "label": "cj_test_2", 153 | // "value": 1019 154 | // } 155 | // ], "disabled": false, "label": "存己_操作一", "value": 736694 156 | // } 157 | // ]; 158 | 159 | export const options2 = [{ 160 | label: '常鸣—操作1', 161 | value: 1000, 162 | "disabled": true, 163 | children: [ 164 | { 165 | label: 'cj_test1', 166 | value: 2001, 167 | "disabled": true, 168 | children: [ 169 | { "disabled": true, "label": "d", "value": 287374 } 170 | ] 171 | } 172 | ] 173 | }, { 174 | label: '常鸣—操作2', 175 | value: 1001, 176 | children: [ 177 | { 178 | label: '222', 179 | value: 2001, 180 | "disabled": true, 181 | children: [ 182 | { "disabled": true, "label": "d2", "value": 287375 } 183 | ] 184 | } 185 | ] 186 | }]; 187 | 188 | // 四级联动数据 189 | export const options3 = [ 190 | { 191 | value: 'anhui', 192 | label: '安徽', 193 | children: [{ 194 | value: 'hefei', 195 | label: '合肥', 196 | children: [{ 197 | value: 'dashushan', 198 | label: '大蜀山', 199 | children: [{ 200 | value: 'shudingfengyun', 201 | label: '蜀顶风云', 202 | }, { 203 | value: 'shanjianyunhai', 204 | label: '山涧云海', 205 | }], 206 | }], 207 | }], 208 | }, { 209 | value: 'zhejiang', 210 | label: '浙江', 211 | children: [{ 212 | value: 'hangzhou', 213 | label: '杭州', 214 | children: [{ 215 | value: 'xihu', 216 | label: '西湖', 217 | children: [{ 218 | value: 'santanyingyue', 219 | label: '三潭印月', 220 | }, { 221 | value: 'duanqiaocanxue', 222 | label: '断桥残月', 223 | }, { 224 | value: 'leifengxizhao', 225 | label: '雷峰夕照', 226 | }, { 227 | value: 'pinghuqiuyue', 228 | label: '平湖秋月', 229 | }], 230 | }], 231 | }, { 232 | value: 'ningbo', 233 | label: '宁波', 234 | children: [], 235 | }], 236 | }, 237 | ]; 238 | -------------------------------------------------------------------------------- /tests/CascadeMultiSelect.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import React from 'react'; 3 | import Enzyme from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import deepcopy from 'lodash/cloneDeep'; 6 | import CascadeMultiSelect from '../src'; 7 | import { options, optionsWithDescription } from './const'; 8 | const { mount, shallow } = Enzyme; 9 | 10 | Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | describe('CascadeMultiSelect', () => { 13 | it('render displayValue', () => { 14 | const wrapper = shallow( 15 | 19 | ); 20 | expect(wrapper.state('displayValue')).to.be('西湖 , test'); 21 | }); 22 | 23 | it('define props.beforeRender', () => { 24 | const wrapper = shallow( 25 | { 28 | let back = ''; 29 | function recursion(list) { 30 | list.forEach(item => { 31 | if (item.checked) { 32 | back += `${item.label}, `; 33 | } else if (item.children && item.children.length) { 34 | recursion(item.children); 35 | } 36 | }); 37 | } 38 | recursion(opts); 39 | return back.substring(0, back.length - 2); 40 | }} 41 | value={['hangzhou']} 42 | /> 43 | ); 44 | expect(wrapper.state('displayValue')).to.be('杭州'); 45 | }); 46 | 47 | it('no response click event when CascadeMultiSelect is disabled', () => { 48 | const wrapper = mount( 49 | 54 | ); 55 | wrapper.find('.kuma-cascader-wrapper').simulate('click'); 56 | expect(wrapper.state('showSubMenu')).to.be(false); 57 | }); 58 | 59 | it('render Span only when CascadeMultiSelect is readOnly', () => { 60 | const wrapper = mount( 61 | 66 | ); 67 | expect(wrapper.find('span').text()).to.be('西湖'); 68 | }); 69 | 70 | it('clear All data', () => { 71 | const wrapper = mount( 72 | 76 | ); 77 | wrapper.find('.kuma-icon-error').simulate('click'); 78 | expect(wrapper.state('value')).to.be.empty(); 79 | }); 80 | 81 | it('value should change when onItemClick', () => { 82 | const wrapper = mount( 83 | 87 | ); 88 | 89 | wrapper.find('.kuma-cascader-wrapper').simulate('click'); 90 | const overlay = mount(wrapper.find('Dropdown').props().overlay); 91 | overlay.find('.kuma-cascade-multi-content').at(0) 92 | .find('.kuma-cascade-multi-list-item').at(0) 93 | .find('s').simulate('click'); 94 | expect(wrapper.state('value')).to.eql(['zhejiang']); 95 | overlay.find('.kuma-cascade-multi-content').at(0) 96 | .find('.kuma-cascade-multi-list-item').at(0) 97 | .find('s').simulate('click'); 98 | expect(wrapper.state('value')).to.eql(['xihu']); 99 | }); 100 | 101 | it('value should be empty when clean Btn is clicked ', () => { 102 | const wrapper = mount( 103 | 107 | ); 108 | wrapper.find('.kuma-cascader-wrapper').simulate('click'); 109 | const overlay = mount(wrapper.find('Dropdown').props().overlay); 110 | overlay.find('.kuma-cascade-multi-content').at(0) 111 | .find('.kuma-cascade-multi-list-item').at(0) 112 | .find('s').simulate('click'); 113 | overlay.find('.kuma-cascade-multi-result-clean').simulate('click'); 114 | expect(wrapper.state('value')).to.eql([]); 115 | }); 116 | 117 | it('change displayValue when onOk Btn is clicked', () => { 118 | class Demo extends React.Component { 119 | constructor(props) { 120 | super(props); 121 | this.state = { 122 | value: [''] 123 | }; 124 | } 125 | 126 | onOk = (valueList, labelList, leafList) => { 127 | this.setState({ value: leafList.map(item => item.value) }); 128 | } 129 | 130 | render() { 131 | return ( 132 | 137 | ); 138 | } 139 | } 140 | const wrapper = mount(); 141 | wrapper.find('.kuma-cascader-wrapper').simulate('click'); 142 | const overlay = mount(wrapper.find('Dropdown').props().overlay); 143 | overlay.find('.kuma-cascade-multi-content').at(0) 144 | .find('.kuma-cascade-multi-list-item').at(1) 145 | .find('s').simulate('click'); 146 | overlay.find('.kuma-cascade-multi-select-footer').find('button').simulate('click'); 147 | expect(wrapper.state('value')).to.eql(['zhonghuamen']); 148 | }); 149 | 150 | it('test「isCleanDisabledLabel」props', () => { 151 | const wrapper = mount( 152 | 157 | ); 158 | wrapper.find('.kuma-cascader-wrapper').simulate('click'); 159 | const overlay = mount(wrapper.find('Dropdown').props().overlay); 160 | overlay.find('.kuma-cascade-multi-result-clean').simulate('click'); 161 | expect(wrapper.state('value')).to.eql([]); 162 | }); 163 | 164 | it('test description', () => { 165 | const wrapper = mount( 166 | 170 | ); 171 | 172 | wrapper.find('.kuma-cascader-wrapper').simulate('click'); 173 | const overlay = mount(wrapper.find('Dropdown').props().overlay); 174 | overlay.find('.kuma-cascade-multi-content').at(0) 175 | .find('.kuma-cascade-multi-list-item').at(0).simulate('click'); 176 | console.log(wrapper.state('description')); 177 | expect(wrapper.state('description')).to.eql({ 178 | description: '这是浙江', 179 | value: 'zhejiang', 180 | label: '浙江' 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /src/CascadeMultiModal.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMultiSelect Component for uxcore 3 | * @author changming 4 | * 5 | * Copyright 2015-2017, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import classnames from 'classnames'; 11 | import deepcopy from 'lodash/cloneDeep'; 12 | import Button from 'uxcore-button'; 13 | import Dialog from 'uxcore-dialog'; 14 | import CascadeMultiPanel from './CascadeMultiPanel'; 15 | import i18n from './locale'; 16 | 17 | class CascadeMultiModal extends React.Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | value: props.value, 23 | options: props.options, 24 | visible: false, 25 | expand: true, 26 | result: {}, 27 | }; 28 | this.data = { 29 | value: props.value, 30 | options: props.options, 31 | result: {}, 32 | }; 33 | const { value, options } = props; 34 | this.initResult(value, options); 35 | } 36 | 37 | onOk() { 38 | const { value, options, result } = this.state; 39 | const { valueList, labelList, leafList } = result; 40 | this.data = { 41 | value, 42 | options, 43 | result, 44 | }; 45 | this.props.onOk(valueList, labelList, leafList); 46 | this.setState({ visible: false }); 47 | } 48 | 49 | onCancel() { 50 | const { value, options, result } = this.data; 51 | this.setState({ 52 | visible: false, 53 | value, 54 | options, 55 | result, 56 | }, () => { 57 | this.props.onCancel(); 58 | }); 59 | } 60 | 61 | onSelect(valueList, labelList, leafList) { 62 | this.setState({ 63 | value: valueList, 64 | result: { 65 | valueList, 66 | labelList, 67 | leafList, 68 | }, 69 | }, () => { 70 | this.props.onSelect(valueList, labelList, leafList); 71 | }); 72 | } 73 | 74 | onDelete(key) { 75 | const { options } = this.props; 76 | const { value } = this.state; 77 | const index = value.indexOf(key); 78 | if (index !== -1) { 79 | value.splice(index, 1); 80 | } 81 | this.initResult(value, options); 82 | this.setState({ value, options }); 83 | } 84 | 85 | onExpand(expand) { 86 | this.setState({ 87 | expand, 88 | }); 89 | } 90 | 91 | getSelectResult(value, dataList, keyArr, textArr) { 92 | if (dataList && dataList.length) { 93 | for (let i = 0; i < dataList.length; i++) { 94 | const item = dataList[i]; 95 | if (!value.length) { return; } 96 | if (value.indexOf(item.value) !== -1) { 97 | keyArr.push(item.value); 98 | textArr.push(item.label); 99 | value.splice(value.indexOf(item.value), 1); 100 | } 101 | if (item.children) { 102 | this.getSelectResult(value, item.children, keyArr, textArr); 103 | } 104 | } 105 | } 106 | } 107 | 108 | initResult(value, options) { 109 | const keyArr = []; 110 | const textArr = []; 111 | const valueList = deepcopy(value); 112 | this.getSelectResult(valueList, options, keyArr, textArr); 113 | this.data.value = keyArr; 114 | this.data.result = { 115 | valueList: keyArr, 116 | labelList: textArr, 117 | }; 118 | } 119 | 120 | renderDialog() { 121 | const { prefixCls, locale, title, cascadeSize, width } = this.props; 122 | const { visible } = this.state; 123 | if (!visible) { return null; } 124 | // 设置 dialog 默认宽度 125 | const defaultWidth = width || cascadeSize * 150 + 220 + 2; 126 | return ( 127 | { 134 | this.onOk(); 135 | }} 136 | onCancel={() => { 137 | this.onCancel(); 138 | }} 139 | > 140 | {this.renderContent()} 141 | 142 | ); 143 | } 144 | 145 | renderContent() { 146 | const { value, options } = this.state; 147 | return ( 148 |
149 | { 154 | this.onSelect(valueList, labelList, leafList); 155 | }} 156 | ref={(r) => { this.refCascadeMulti = r; }} 157 | mode="mix" 158 | /> 159 |
160 | ); 161 | } 162 | 163 | renderResult() { 164 | const { prefixCls } = this.props; 165 | return ( 166 |
169 | {this.renderResultList()} 170 | {this.renderExpand()} 171 |
172 | ); 173 | } 174 | 175 | renderExpand() { 176 | const { prefixCls, locale } = this.props; 177 | const { expand } = this.state; 178 | const { labelList } = this.data.result; 179 | if (!labelList || !labelList.length) { return null; } 180 | let arr = null; 181 | if (expand) { 182 | arr = ( 183 | { this.onExpand(false); }} 186 | > 187 | {i18n(locale).close} 188 | 189 | ); 190 | } else { 191 | arr = ( 192 | { this.onExpand(true); }} 195 | > 196 | {i18n(locale).expandAll} 197 | {labelList.length} 198 | {i18n(locale).item} 199 | 200 | ); 201 | } 202 | return arr; 203 | } 204 | 205 | renderResultList() { 206 | const { prefixCls } = this.props; 207 | const { expand } = this.state; 208 | const { valueList, labelList } = this.data.result; 209 | if (!labelList) { return null; } 210 | const arr = []; 211 | const style = {}; 212 | if (expand) { 213 | style.height = 'auto'; 214 | } else { 215 | style.maxHeight = 76; 216 | } 217 | labelList.forEach((item, index) => { 218 | arr.push( 219 |
  • 220 | {item} 221 | { this.onDelete(valueList[index]); }} 227 | > 228 |
  • 229 | ); 230 | }); 231 | return ( 232 |
      236 | {arr} 237 |
    238 | ); 239 | } 240 | 241 | render() { 242 | const { locale } = this.props; 243 | return ( 244 |
    245 | 253 | {this.renderResult()} 254 | {this.renderDialog()} 255 |
    256 | ); 257 | } 258 | 259 | } 260 | 261 | CascadeMultiModal.defaultProps = { 262 | className: '', 263 | prefixCls: 'kuma-cascade-multi', 264 | config: [], 265 | options: [], 266 | cascadeSize: 3, 267 | value: [], 268 | notFoundContent: '', 269 | allowClear: true, 270 | locale: 'zh-cn', 271 | onSelect: () => {}, 272 | 273 | title: '', 274 | width: 0, 275 | onOk: () => {}, 276 | onCancel: () => {}, 277 | }; 278 | 279 | CascadeMultiModal.propTypes = { 280 | className: PropTypes.string, 281 | prefixCls: PropTypes.string, 282 | config: PropTypes.array, 283 | options: PropTypes.array, 284 | cascadeSize: PropTypes.number, 285 | value: PropTypes.array, 286 | notFoundContent: PropTypes.string, 287 | allowClear: PropTypes.bool, 288 | locale: PropTypes.string, 289 | onSelect: PropTypes.func, 290 | 291 | title: PropTypes.string, 292 | width: PropTypes.number, 293 | onOk: PropTypes.func, 294 | onCancel: PropTypes.func, 295 | }; 296 | 297 | export default CascadeMultiModal; 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## uxcore-cascade-multi-select 2 | 3 | 级联多选组件,推荐所有层级的每一个候选 option 的 key 都是不重复的。 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![build status][travis-image]][travis-url] 7 | [![Test Coverage][coveralls-image]][coveralls-url] 8 | [![Dependency Status][dep-image]][dep-url] 9 | [![devDependency Status][devdep-image]][devdep-url] 10 | [![NPM downloads][downloads-image]][npm-url] 11 | 12 | [![Sauce Test Status][sauce-image]][sauce-url] 13 | 14 | [npm-image]: http://img.shields.io/npm/v/uxcore-cascade-multi-select.svg?style=flat-square 15 | [npm-url]: http://npmjs.org/package/uxcore-cascade-multi-select 16 | [travis-image]: https://img.shields.io/travis/uxcore/uxcore-cascade-multi-select.svg?style=flat-square 17 | [travis-url]: https://travis-ci.org/uxcore/uxcore-cascade-multi-select 18 | [coveralls-image]: https://img.shields.io/coveralls/uxcore/uxcore-cascade-multi-select.svg?style=flat-square 19 | [coveralls-url]: https://coveralls.io/r/uxcore/uxcore-cascade-multi-select?branch=master 20 | [dep-image]: http://img.shields.io/david/uxcore/uxcore-cascade-multi-select.svg?style=flat-square 21 | [dep-url]: https://david-dm.org/uxcore/uxcore-cascade-multi-select 22 | [devdep-image]: http://img.shields.io/david/dev/uxcore/uxcore-cascade-multi-select.svg?style=flat-square 23 | [devdep-url]: https://david-dm.org/uxcore/uxcore-cascade-multi-select#info=devDependencies 24 | [downloads-image]: https://img.shields.io/npm/dm/uxcore-cascade-multi-select.svg 25 | [sauce-image]: https://saucelabs.com/browser-matrix/uxcore-cascade-multi-select.svg 26 | [sauce-url]: https://saucelabs.com/u/uxcore-cascade-multi-select 27 | 28 | 29 | ### Development 30 | 31 | ```sh 32 | git clone https://github.com/uxcore/uxcore-cascade-multi-select 33 | cd uxcore-cascade-multi-select 34 | npm install 35 | npm run server 36 | ``` 37 | 38 | if you'd like to save your install time,you can use uxcore-tools globally. 39 | 40 | ```sh 41 | npm install uxcore-tools -g 42 | git clone https://github.com/uxcore/uxcore-cascade-multi-select 43 | cd uxcore-cascade-multi-select 44 | npm install 45 | npm run dep 46 | npm run start 47 | ``` 48 | 49 | ### Test Case 50 | 51 | ```sh 52 | npm run test 53 | ``` 54 | 55 | ### Coverage 56 | 57 | ```sh 58 | npm run coverage 59 | ``` 60 | 61 | ## Demo 62 | 63 | http://uxcore.github.io/components/cascade-multi-select 64 | 65 | ## Contribute 66 | 67 | Yes please! See the [CONTRIBUTING](https://github.com/uxcore/uxcore/blob/master/CONTRIBUTING.md) for details. 68 | 69 | ## CascadeMultiSelect 70 | 71 | ## API 72 | 73 | ## Props 74 | 75 | | 选项 | 描述 | 类型 | 必填 | 默认值 | 76 | |---|---|---|---|---| 77 | | prefixCls | 默认的类名前缀 | String | `false`| "kuma-cascade-multi" | 78 | | className | 自定义类名 | String | `false` | "" | 79 | | dropdownClassName | dropdown 部分的定制类名 | String | `false` | "" | 80 | | config | 每一级的特殊配置项,可参考[下方案例](#props.config) | Array | `false` | [] | 81 | | options | 横向级联的数据,可参考[下方案例](#props.options) | Array | `true` | [] | 82 | | value | 可由外部控制的值,可参考[下方案例](#props.value) | Array | `false` | [] | 83 | | defaultValue | 初始默认的值,格式同 value | Array | `false` | [] | 84 | | cascadeSize | 级联层级数 | number | `false` | 3 | 85 | | placeholder | placeholder | string | `false` | 'Please Select' 或 '请选择' | 86 | | notFoundContent | 没有子项级联数据时显示内容 | String | `false` | 'No Data' 或 '没有数据' | 87 | | allowClear | 是否允许清空 | bool | `false` | true | 88 | | disabled | 禁用模式,只能看到被禁掉的输入框 | bool | `false` | false | 89 | | readOnly | 只读模式,只能看到纯文本 | bool | `false` | false | 90 | | locale | 'zh-cn' or 'en-us' | String | `false` | 'zh-cn' | 91 | | onSelect | 选中选项的回调函数 | function | `false` | (valueList, labelList, leafList, cascadeSelected) => {} | 92 | | onItemClick | 点击选项事件,返回选项信息 | function | `false` | (item) => {} | 93 | | onOk | 点击确认按钮回调函数 | function | `false` | (valueList, labelList, leafList, cascadeSelected) => {} | 94 | | onCancel | 取消选择时回调函数,通常不点确定,直接隐藏下拉框也会触发这个函数 | function | `false` | () => {} | 95 | | beforeRender | 处理在input中预显示的内容,具体用法参考下方的案例 | function | `false` | (value, options) => {} | 96 | | keyCouldDuplicated | 是否允许除了第一级和最后一级以外的 id 重复 | bool | `false` | false | 97 | | isCleanDisabledLabel | 是否清除已禁用选项 | bool | `false` | false 98 | 99 | ### props.config 100 | 101 | ** 示例 ** 102 | ```javascript 103 | const config = [{ 104 | checkable: false, 105 | showSearch: true, // 显示过滤项,默认为 false 106 | }, { 107 | checkable: false, // 设置第二级不可选 108 | }, { 109 | checkable: false, 110 | }] 111 | ``` 112 | config 为一个数组,每一项的配置如下: 113 | 114 | * checkable: (boolean) 该级是否可选,默认为 true 115 | * showSearch: (boolean) 该级是否展示过滤搜索框,默认为 false 116 | 117 | ### props.options 118 | 119 | | 选项 | 描述 | 类型 | 必填 | 默认值 | 120 | |---|---|---|---|---| 121 | | value | 选项的值 | String | `true`| "" | 122 | | label | 选项的名称 | String | `true` | "" | 123 | | children | 选项的子项集 | Array | `false` | [] | 124 | | disabled | 是否禁止选中 | boolean | `false` | undefined | 125 | 126 | ** 示例 ** 127 | ```javascript 128 | const options = [{ 129 | value: 'zhejiang', 130 | label: '浙江', 131 | children: [{ 132 | value: 'hangzhou', 133 | label: '杭州', 134 | children: [{ 135 | value: 'xihu', 136 | label: '西湖', 137 | disabled: true, 138 | }], 139 | }], 140 | }, { 141 | value: 'jiangsu', 142 | label: '江苏', 143 | children: [{ 144 | value: 'nanjing', 145 | label: '南京', 146 | children: [{ 147 | value: 'zhonghuamen', 148 | label: '中华门', 149 | }], 150 | }], 151 | }]; 152 | ``` 153 | 154 | ### props.value 155 | 156 | props.value 传递的是 **key 构成的数组**,这里的 key 可以是任意级别,除非当 prop `keyCouldDuplicated` 为 true 时,必须传 **叶子节点的 key 数组**。 157 | 158 | ```javascript 159 | const value = ['xihu', 'bingjiang']; 160 | ``` 161 | 162 | ** 示例 ** 163 | ```javascript 164 | 168 | ``` 169 | 170 | ### props.beforeRender 171 | 172 | ```javascript 173 | props.beforeRender = (value, options) => { return '渲染你自己想要的字符串'; } 174 | ``` 175 | 176 | beforeRender 返回一个字符串,用来渲染进展开面板触发器的 input 内容。beforeRender 有两个参数,第一个 value 就是当前所有选中的 value 值数组,比较重要的是 options,options 对应的就是 props.options,并且带有每一个选项的选中状态。比如,对于 `[{ label: 'label1', children: [{ label: 'label1-1' }] }]`,当用户选中了 label1 时,options 的结构为: 177 | 178 | ```javascript 179 | [{ label: 'label1', checked: true, children: [{ label: 'label1-1', checked: true }] }] 180 | ``` 181 | 182 | 你在业务中通过获取 `checked` 的值,就可以知道用户选中了哪些选项,此外,当用户未全选某一级时,还会有 `halfChecked` 属性。 183 | 184 | ### props.onSelect 185 | 186 | ```javascript 187 | (valueList, labelList, leafList, cascadeList) => { 188 | valueList: 选中选项的value列表 189 | labelList: 选中选项的label列表 190 | leafList: 选中所有子项的{value, label}列表 191 | cascadeList: 所有级联结构,如果 item 被选中,则会有一个属性 `checked: true` 192 | } 193 | ``` 194 | > 注:如果选项的子集全部选中,则返回该选项值 195 | 196 | ## CascadeMultiPanel 197 | 198 | ## API 199 | 200 | ## Props 201 | 202 | | 选项 | 描述 | 类型 | 必填 | 默认值 | 203 | |---|---|---|---|---| 204 | | className | 自定义类名 | String | `false` | "" | 205 | | prefixCls | 默认的类名前缀 | String | `false`| "kuma-cascade-multi" | 206 | | config | 配置项 | Array | `false` | [] | 207 | | options | 横向级联的数据 | Array | `true` | [] | 208 | | value | 可由外部控制的值 | Array | `false` | [] | 209 | | cascadeSize | 级联层级数 | number | `false` | 3 | 210 | | notFoundContent | 没有子项级联数据时显示内容 | String | `false` | 'No Data' 或 '没有数据' | 211 | | allowClear | 是否允许清空 | bool | `false` | true | 212 | | locale | 'zh-cn' or 'en-us' | String | `false` | 'zh-cn' | 213 | | onSelect | 选中选项的回调函数 | function | `false` | (valueList, labelList, leafList) => {} | 214 | | onItemClick | 点击选项事件,返回选项信息 | function | `false` | (item) => {} | 215 | | keyCouldDuplicated | 是否允许除了第一级和最后一级以外的 id 重复 | bool | `false` | false | 216 | 217 | ## CascadeMultiModal 218 | 219 | ## API 220 | 221 | ## Props 222 | 223 | | 选项 | 描述 | 类型 | 必填 | 默认值 | 224 | |---|---|---|---|---| 225 | | prefixCls | 默认的类名前缀 | String | `false`| "kuma-cascade-multi" | 226 | | className | 自定义类名 | String | `false` | "" | 227 | | config | 配置项 | Array | `false` | [] | 228 | | options | 横向级联的数据 | Array | `true` | [] | 229 | | value | 可由外部控制的值 | Array | `false` | [] | 230 | | cascadeSize | 级联层级数 | number | `false` | 3 | 231 | | notFoundContent | 没有子项级联数据时显示内容 | String | `false` | 'No Data' 或 '没有数据' | 232 | | allowClear | 是否允许清空 | bool | `false` | true | 233 | | locale | 'zh-cn' or 'en-us' | String | `false` | 'zh-cn' | 234 | | onSelect | 选中选项的回调函数 | function | `false` | (valueList, labelList, leafList) => {} | 235 | | onItemClick | 点击选项事件,返回选项信息 | function | `false` | (item, level) => {} | 236 | | title | 标题 | String | `false` | '级联选择' | 237 | | width | dialog 宽度 | Number | `false` | 672 | 238 | | onOk | 成功按钮回调函数 | Function | `false` | (valueList, labelList, leafList) => {} | 239 | | onCancel | 取消的回调函数 | Function | `false` | () => {} | 240 | 241 | props 复用 uxcore-cascade-multi-select 的 props. 242 | 243 | 继承了部分Dialog的props, 244 | 245 | ### onOk 246 | 247 | ```javascript 248 | (valueList, labelList, leafList, cascadeList) => { 249 | valueList: 选中选项的value列表 250 | labelList: 选中选项的label列表 251 | leafList: 选中所有子项的{value, label}列表 252 | cascadeList: 所有级联结构,如果 item 被选中,则会有一个属性 `checked: true` 253 | } 254 | ``` 255 | 256 | ## 使用方法 257 | 258 | ```javascript 259 | import CascadeMultiSelect from 'uxcore-cascade-multi-select'; 260 | 261 | const { 262 | CascadeMultiPanel, 263 | CascadeMultiModal, 264 | } = CascadeMultiSelect; 265 | 266 | render() { 267 | return () { 268 |
    269 | 270 | 271 | 272 |
    273 | } 274 | } 275 | ``` 276 | -------------------------------------------------------------------------------- /src/CascadeMultiSelect.less: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMulti Component Style for uxcore 3 | * @author changming 4 | * 5 | * Copyright 2015-2017, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | @notificationPrefixCls: kuma-cascade-multi; 9 | @backgroundColor: @bg-disabled-color; 10 | @_border: rgba(31,56,88,0.20); 11 | @_border-light: #e8e8e8; 12 | @_text-0: #76889A; 13 | 14 | .@{notificationPrefixCls} { 15 | float: left; 16 | overflow: hidden; 17 | 18 | cursor: auto; 19 | -webkit-user-select: none; 20 | -moz-user-select: none; 21 | -ms-user-select: none; 22 | 23 | border: 1px solid @_border; 24 | background-color: #ffffff; 25 | 26 | .@{notificationPrefixCls}-content { 27 | float: left; 28 | overflow-y: auto; 29 | 30 | width: 150px; 31 | height: 310px; 32 | padding: 14px 0; 33 | 34 | border-left: 1px solid @_border-light; 35 | background-color: #fff; 36 | &:first-child { 37 | border-left: 0; 38 | } 39 | 40 | .@{notificationPrefixCls}-list-item { 41 | overflow: hidden; 42 | 43 | height: 28px; 44 | padding: 0 15px; 45 | 46 | white-space: nowrap; 47 | text-overflow: ellipsis; 48 | -ms-text-overflow: ellipsis; 49 | 50 | line-height: 28px; 51 | 52 | -o-text-overflow: ellipsis; 53 | &:hover { 54 | background-color: @bg-disabled-color; 55 | } 56 | s { 57 | margin-right: 5px; 58 | } 59 | 60 | .@{notificationPrefixCls}-item-label { 61 | span { 62 | vertical-align: middle; 63 | } 64 | } 65 | 66 | .@{notificationPrefixCls}-item-disabled { 67 | cursor: not-allowed; 68 | * { 69 | cursor: not-allowed; 70 | } 71 | } 72 | } 73 | 74 | .@{notificationPrefixCls}-list-item-active { 75 | background-color: @bg-disabled-color; 76 | } 77 | 78 | .@{notificationPrefixCls}-list-noData { 79 | display: block; 80 | 81 | margin-top: 7px; 82 | margin-left: 21px; 83 | } 84 | } 85 | 86 | .@{notificationPrefixCls}-result { 87 | float: left; 88 | 89 | width: 220px; 90 | height: 310px; 91 | padding: 0; 92 | 93 | border-left: 1px solid @_border-light; 94 | background-color: #fff; 95 | 96 | .@{notificationPrefixCls}-result-title { 97 | margin: 20px 15px 10px 15px; 98 | padding: 0; 99 | 100 | color: @_text-0; 101 | 102 | .@{notificationPrefixCls}-result-clean { 103 | float: right; 104 | 105 | margin-right: 6px; 106 | 107 | cursor: pointer; 108 | 109 | color: @link-color; 110 | } 111 | } 112 | 113 | .@{notificationPrefixCls}-result-tree { 114 | overflow-x: hidden; 115 | overflow-y: auto; 116 | 117 | height: 250px; 118 | margin: 0; 119 | & > ul { 120 | width: 220px; 121 | } 122 | ul { 123 | -webkit-user-select: none; 124 | -moz-user-select: none; 125 | -ms-user-select: none; 126 | li { 127 | white-space: nowrap; 128 | 129 | line-height: 25px; 130 | .kuma-icon-triangle-down, 131 | .kuma-icon-triangle-right { 132 | margin-right: 5px; 133 | 134 | vertical-align: middle; 135 | 136 | color: #999; 137 | } 138 | .tree-node-ul-li-div { 139 | &:hover { 140 | background-color: @backgroundColor; 141 | } 142 | .tree-node-ul-li-span { 143 | position: relative; 144 | 145 | display: inline-block; 146 | 147 | width: 100%; 148 | 149 | vertical-align: middle; 150 | background-color: transparent; 151 | &:hover { 152 | color: @text-primary-color; 153 | background-color: transparent; 154 | .tree-node-ul-li-del { 155 | -ms-transform: scale(1); 156 | transform: scale(1); 157 | 158 | opacity: 1; 159 | } 160 | } 161 | &-label { 162 | display: inline-block; 163 | overflow-x: hidden; 164 | 165 | vertical-align: middle; 166 | white-space: nowrap; 167 | text-overflow: ellipsis; 168 | -ms-text-overflow: ellipsis; 169 | 170 | -o-text-overflow: ellipsis; 171 | } 172 | .tree-node-ul-li-all { 173 | margin-left: 10px; 174 | 175 | vertical-align: middle; 176 | 177 | color: #999; 178 | } 179 | .tree-node-ul-li-del { 180 | position: absolute; 181 | right: 42px; 182 | 183 | cursor: pointer; 184 | // transition: opacity .3s, transform .3s; 185 | transition: none; 186 | -ms-transform: scale(0); 187 | transform: scale(0); 188 | 189 | color: @link-color; 190 | } 191 | } 192 | } 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | /** 200 | * CascadeMultiSelect Component Style for uxcore 201 | * @author guyunxiang 202 | * 203 | * Copyright 2015-2017, Uxcore Team, Alinw. 204 | * All rights reserved. 205 | */ 206 | .@{notificationPrefixCls}-select-panel-content { 207 | position: relative; 208 | box-shadow: @box-shadow-1; 209 | border-radius: @popup-border-radius; 210 | .@{notificationPrefixCls} { 211 | border-radius: @popup-border-radius @popup-border-radius 0 0; 212 | overflow: hidden; 213 | } 214 | .@{notificationPrefixCls}-select-footer { 215 | border-radius: 0 0 @popup-border-radius @popup-border-radius; 216 | overflow: hidden; 217 | clear: both; 218 | } 219 | .@{notificationPrefixCls}-select-footer-description { 220 | 221 | min-height: 52px; 222 | padding: 10px 20px; 223 | text-align: left; 224 | 225 | border-width: 0 0 1px 0; 226 | border-style: solid; 227 | 228 | border-color: @_border; 229 | background-color: #ffffff; 230 | 231 | line-height: 20px; 232 | } 233 | .@{notificationPrefixCls}-select-panel-wrap { 234 | border-radius: @popup-border-radius; 235 | box-shadow: @box-shadow-1; 236 | zoom: 1; 237 | overflow: hidden; 238 | } 239 | } 240 | 241 | .@{notificationPrefixCls}-input { 242 | background-color: #fff; 243 | &::-ms-clear { 244 | display: none; 245 | } 246 | } 247 | 248 | .@{notificationPrefixCls}-large { 249 | height: 38px; 250 | } 251 | .@{notificationPrefixCls}-middle { 252 | min-height: 32px; 253 | line-height: 32px; 254 | } 255 | .@{notificationPrefixCls}-small { 256 | min-height: 28px; 257 | line-height: 28px; 258 | } 259 | 260 | .@{notificationPrefixCls}-text-result { 261 | overflow-x: hidden; 262 | 263 | margin-right: 30px; 264 | 265 | white-space: nowrap; 266 | text-overflow: ellipsis; 267 | &-input { 268 | width: 100%; 269 | padding-right: 10px; 270 | 271 | border: 0; 272 | background-color: transparent; 273 | &::-ms-clear { 274 | display: none; 275 | } 276 | } 277 | } 278 | 279 | .@{notificationPrefixCls}-select-footer { 280 | float: left; 281 | 282 | text-align: center; 283 | 284 | border-width: 0 1px 1px 1px; 285 | border-style: solid; 286 | 287 | border-color: @_border; 288 | background-color: #ffffff; 289 | 290 | line-height: 52px; 291 | height: auto; 292 | } 293 | 294 | .@{notificationPrefixCls} { 295 | &.ucms-panel { 296 | border-radius: @popup-border-radius; 297 | } 298 | } 299 | 300 | 301 | /** 302 | * CascadeMultiModal Component Style for uxcore 303 | * @author guyunxiang 304 | * 305 | * Copyright 2015-2017, Uxcore Team, Alinw. 306 | * All rights reserved. 307 | */ 308 | .@{notificationPrefixCls}-model { 309 | .@{notificationPrefixCls} { 310 | border-left: none; 311 | border-right: none; 312 | } 313 | &-result { 314 | margin: 15px 0; 315 | &-ul { 316 | overflow: hidden; 317 | &-list { 318 | position: relative; 319 | 320 | float: left; 321 | 322 | margin-right: 8px; 323 | margin-bottom: 8px; 324 | padding: 6px 20px; 325 | 326 | border-radius: 2px; 327 | background-color: #f0f0f0; 328 | &:hover { 329 | // .@{notificationPrefixCls}-model-result-ul-list-remove { 330 | // -ms-transform: scale(1); 331 | // transform: scale(1); 332 | // } 333 | // .@{notificationPrefixCls}-model-result-ul-list-content { 334 | // margin-right: 10px; 335 | // margin-left: -10px; 336 | // } 337 | } 338 | &-content { 339 | transition: margin .3s cubic-bezier(.165,.84,.44,1); 340 | -ms-transform: scale(1); 341 | transform: scale(1); 342 | margin-right: 10px; 343 | margin-left: -10px; 344 | } 345 | &-remove { 346 | position: absolute; 347 | top: 3px; 348 | right: 5px; 349 | 350 | cursor: pointer; 351 | // transition: opacity .3s,transform .3s; 352 | -ms-transform: scale(1); 353 | transform: scale(1); 354 | vertical-align: middle; 355 | 356 | opacity: 1; 357 | color: @normal-alpha-4; 358 | &:before { 359 | content: '\e610'; 360 | } 361 | &:hover { 362 | color: @normal-alpha-3; 363 | } 364 | } 365 | } 366 | } 367 | } 368 | &-expand { 369 | cursor: pointer; 370 | 371 | color: @brand-primary; 372 | } 373 | .kuma-dlg-body { 374 | overflow: hidden; 375 | 376 | padding: 0; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /demo/CascadeMultiSelectDemo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMultiSelect Component Demo for uxcore 3 | * @author guyunxiang 4 | * 5 | * Copyright 2015-2016, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | import React from 'react'; 9 | import CascadeMultiSelect from '../src'; 10 | import { 11 | options, 12 | options2, 13 | options3, 14 | optionsWithDescription 15 | } from './const'; 16 | 17 | const dynamicData = [ 18 | { 19 | value: 1, 20 | label: 'one', 21 | children: [ 22 | { 23 | value: 2, 24 | label: 'two', 25 | children: [{ 26 | value: 3, 27 | label: 'three', 28 | children: [{ 29 | value: 4, 30 | label: 'four', 31 | }], 32 | }], 33 | }, 34 | ], 35 | }, 36 | ]; 37 | 38 | const { 39 | CascadeMultiPanel, 40 | CascadeMultiModal, 41 | } = CascadeMultiSelect; 42 | 43 | const size = ''; 44 | 45 | class Demo extends React.Component { 46 | 47 | constructor(props) { 48 | super(props); 49 | this.state = { 50 | demo1: ['zhejiang'], 51 | demo2: [], 52 | demo3: ['xihu'], 53 | demo4: ['bingjiang', 'ningbo', 'jiangsu'], 54 | demo5: ['bingjiang', 'ningbo', 'anhui', 'shandong'], 55 | demo6: ['xihu', 'bingjiang', 'shandong'], 56 | demo7: [], 57 | demo8: [], 58 | demo9: [287374], 59 | demo10: ['bingjiang', 'ningbo', 'anhui', 'shandong', 'jiangsu', 'longname-0'], 60 | asyncOptions6: options, 61 | dynamicData, 62 | size, 63 | }; 64 | } 65 | 66 | render() { 67 | return ( 68 |
    69 |
    70 |

    基本

    71 |
    72 |
    73 | { 79 | console.log(valueList, labelList, leafList, cascadeSelected); 80 | this.setState({ demo1: leafList.map(item => item.value) }); 81 | }} 82 | value={this.state.demo1} 83 | beforeRender={(value, opts) => { 84 | let back = ''; 85 | function recursion(list) { 86 | list.forEach(item => { 87 | if (item.checked) { 88 | back += `${item.label}, `; 89 | } else if (item.children && item.children.length) { 90 | recursion(item.children); 91 | } 92 | }); 93 | } 94 | recursion(opts); 95 | 96 | return back.substring(0, back.length - 2); 97 | }} 98 | /> 99 |
    100 | 101 |
    102 | 103 |
    104 |

    尺寸 large

    105 |
    106 |
    107 | 114 |
    115 | 116 |
    117 | 118 |
    119 |

    尺寸 middle

    120 |
    121 |
    122 | 129 |
    130 | 131 |
    132 | 133 |
    134 |

    尺寸 small

    135 |
    136 |
    137 | 144 |
    145 | 146 |
    147 | 148 |
    149 |

    显示description

    150 |
    151 |
    152 | 159 |
    160 | 161 |
    162 | 163 |
    164 |

    动态

    165 |
    166 |
    167 | console.log('onOk', params)} 170 | onChange={(...params) => console.log('onChange', params)} 171 | cascadeSize={4} 172 | size={'middle'} 173 | /> 174 |
    175 | 185 |
    186 | 187 |
    188 | 189 |
    190 |

    隐藏清空

    191 |
    192 |
    193 | { 199 | this.setState({ demo2: valueList }); 200 | }} 201 | /> 202 |
    203 | 204 |
    205 | 206 |
    207 |

    启用搜索

    208 |
    209 |
    210 | { 227 | this.setState({ demo2: valueList }); 228 | }} 229 | /> 230 |
    231 | 232 |
    233 | 234 |
    235 |

    禁用 (不可展开面板)

    236 |
    237 |
    238 |

    disabled

    239 | 245 |

    readOnly

    246 | 251 |
    252 | 253 |
    254 | 255 |
    256 |

    禁选前两级 (设置前两级 checkable: false)

    257 |
    258 |
    259 | { 271 | this.setState({ demo1: valueList }); 272 | }} 273 | /> 274 |
    275 | 276 |
    277 | 278 |
    279 |

    数据异步 (手动异步数据)

    280 |

    281 | 302 |

    303 |

    点击async更新options和value

    304 |

    点击西湖,更新选项

    305 |
    306 |
    307 | { 312 | if (item.value === 'xihu') { 313 | this.setState({ 314 | asyncOptions6: options2, 315 | demo6: [] 316 | }); 317 | } 318 | }} 319 | onOk={(valueList) => { 320 | this.setState({ demo6: valueList }); 321 | }} 322 | keyCouldDuplicated 323 | /> 324 |
    325 | 326 |
    327 | 328 |
    329 |

    不定级(四级)

    330 |
    331 |
    332 | { 338 | console.log(valueList); 339 | this.setState({ demo7: valueList }); 340 | }} 341 | /> 342 |
    343 | 344 |
    345 | 346 |
    347 |

    单选 (通过禁用所有级 + Props.onItemClick 实现)

    348 |
    349 |
    350 | { 365 | console.log(level, item); 366 | if (level === 4) { 367 | this.setState({ 368 | demo8: [item.value], 369 | }); 370 | } 371 | }} 372 | /> 373 |
    374 | 375 |
    376 | 377 |
    378 |

    只使用面板

    379 |
    380 |
    381 | { 386 | console.log(leafList); 387 | this.setState({ 388 | demo9: valueList, 389 | }); 390 | }} 391 | className={'ucms-panel'} 392 | keyCouldDuplicated 393 | isCleanDisabledLabel 394 | /> 395 |
    396 | 397 |
    398 | 399 |
    400 |

    弹框模式

    401 |
    402 |
    403 | { 410 | console.log(valueList, labelList, leafList); 411 | this.setState({ demo10: valueList }); 412 | }} 413 | /> 414 |
    415 |
    416 | ); 417 | } 418 | } 419 | 420 | export default Demo; 421 | -------------------------------------------------------------------------------- /src/CascadeMultiSelect.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMultiSelect Component for uxcore 3 | * @author changming 4 | * 5 | * Copyright 2015-2017, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import classnames from 'classnames'; 11 | import Dropdown from 'uxcore-dropdown'; 12 | import Button from 'uxcore-button'; 13 | import CascadeMultiPanel from './CascadeMultiPanel'; 14 | import CascadeMultiModal from './CascadeMultiModal'; 15 | import i18n from './locale'; 16 | import { getDisabledValueLabel, getWidthStyle } from './utils'; 17 | 18 | const makeOptionsChecked = (value = [], options) => { 19 | // 没有value则需要设置check为false 20 | const valueStr = value.map(i => `${i}`); 21 | for (let i = 0, l = options.length; i < l; i++) { 22 | const item = options[i]; 23 | const containIdx = valueStr.indexOf(`${item.value}`); 24 | if (containIdx > -1) { 25 | item.checked = true; 26 | valueStr.splice(containIdx, 1); 27 | } else { 28 | item.checked = false; 29 | } 30 | if (item.children && item.children.length) { 31 | makeOptionsChecked(valueStr, item.children); 32 | } 33 | } 34 | }; 35 | 36 | class CascadeMultiSelect extends React.Component { 37 | static separator = ' , '; 38 | 39 | static getDerivedStateFromProps(props, state) { 40 | const { value } = props; 41 | if (value === state.lastPropValue) { 42 | return null; 43 | } 44 | const displayValue = CascadeMultiSelect.getInputValue(props, value); 45 | return { displayValue, value, lastPropValue: value }; 46 | } 47 | 48 | static getInputValue(props, value) { 49 | const { options, beforeRender, locale } = props; 50 | if (beforeRender) { 51 | makeOptionsChecked(value, options); 52 | return beforeRender(value, options); 53 | } 54 | 55 | const arr = []; 56 | if (value && value.length) { 57 | for (let i = 0; i < value.length; i += 1) { 58 | arr.push(CascadeMultiSelect.getValueLabel(options, value[i], locale)); 59 | } 60 | } 61 | return arr.join(CascadeMultiSelect.separator); 62 | } 63 | 64 | static getValueLabel(dataList, key, locale) { 65 | let back = ''; 66 | if (dataList && dataList.length) { 67 | for (let i = 0; i < dataList.length; i += 1) { 68 | if (dataList[i].value === key) { 69 | return dataList[i].children && dataList[i].children.length ? 70 | `${dataList[i].label} (${i18n(locale).all})` : 71 | dataList[i].label; 72 | } 73 | if (dataList[i].children) { 74 | const res = CascadeMultiSelect.getValueLabel(dataList[i].children, key, locale); 75 | back = res || back; 76 | } 77 | } 78 | } 79 | return back; 80 | } 81 | 82 | constructor(props) { 83 | super(props); 84 | this.state = { 85 | description: { 86 | description: '', 87 | value: null, 88 | label: '', 89 | }, 90 | value: props.value || props.defaultValue, 91 | displayValue: '', 92 | showSubMenu: false, 93 | result: {}, 94 | }; 95 | this.hasChanged = false; 96 | this.onOk = this.onOk.bind(this); 97 | this.handleSelect = this.handleSelect.bind(this); 98 | this.handleItemClick = this.handleItemClick.bind(this); 99 | this.handleStopPropagation = this.handleStopPropagation.bind(this); 100 | this.updateDescription = this.updateDescription.bind(this); 101 | } 102 | 103 | componentDidMount() { 104 | const { value, defaultValue } = this.props; 105 | this.setInputValue(value || defaultValue); 106 | } 107 | 108 | onOk() { 109 | if (!this.hasChanged) { 110 | return; 111 | } 112 | const { value, result } = this.state; 113 | const displayValue = CascadeMultiSelect.getInputValue(this.props, value); 114 | const { valueList, labelList, leafList, cascadeSelected } = result; 115 | this.setState({ 116 | displayValue, 117 | value, 118 | }, () => { 119 | this.props.onOk(valueList, labelList, leafList, cascadeSelected); 120 | }); 121 | } 122 | 123 | onCancel() { 124 | const { value, result } = this.state; 125 | this.setState({ 126 | displayValue: CascadeMultiSelect.getInputValue(this.props, value), 127 | value, 128 | result, 129 | }, () => { 130 | this.props.onCancel(); 131 | }); 132 | } 133 | 134 | onCleanSelect() { 135 | const { isCleanDisabledLabel, options } = this.props; 136 | const prevValue = this.state.value; 137 | let displayValue = ''; 138 | const value = []; 139 | const labelList = []; 140 | let leafList = []; 141 | const result = {}; 142 | if (!isCleanDisabledLabel) { 143 | const data = getDisabledValueLabel(options, prevValue); 144 | leafList = data.leafNodes; 145 | data.disabledNodes.forEach((item) => { 146 | value.push(item.value); 147 | labelList.push(item.label); 148 | }); 149 | result.labelList = labelList; 150 | result.valueList = value; 151 | result.leafList = leafList; 152 | displayValue = CascadeMultiSelect.getInputValue(this.props, value); 153 | } 154 | this.setState({ 155 | value, 156 | displayValue, 157 | result, 158 | }, () => { 159 | this.props.onOk(value, labelList, leafList); 160 | this.props.onSelect(value, labelList, leafList); 161 | }); 162 | this.hasChanged = true; 163 | } 164 | 165 | onDropDownVisibleChange(visible) { 166 | const { disabled } = this.props; 167 | if (!disabled) { 168 | this.setState({ showSubMenu: visible }); 169 | } 170 | if (!visible) { 171 | this.onCancel(); 172 | } 173 | } 174 | 175 | getValueLabel(dataList, key) { 176 | let back = ''; 177 | if (dataList && dataList.length) { 178 | for (let i = 0; i < dataList.length; i += 1) { 179 | if (dataList[i].value === key) { 180 | return dataList[i].children && dataList[i].children.length ? 181 | `${dataList[i].label} (${i18n(this.props.locale).all})` : 182 | dataList[i].label; 183 | } 184 | if (dataList[i].children) { 185 | const res = this.getValueLabel(dataList[i].children, key); 186 | back = res || back; 187 | } 188 | } 189 | } 190 | return back; 191 | } 192 | 193 | setInputValue(value) { 194 | const displayValue = CascadeMultiSelect.getInputValue(this.props, value); 195 | this.setState({ displayValue, value }); 196 | } 197 | 198 | setPanelWidth() { 199 | const { cascadeSize } = this.props; 200 | const style = {}; 201 | const width = getWidthStyle(this.refUls, 150); 202 | const resultPanelWidth = getWidthStyle(this.refResultPanel, 220); 203 | style.width = 0; 204 | for (let i = 0; i < cascadeSize; i += 1) { 205 | style.width += parseInt(width, 0); 206 | } 207 | style.width += parseInt(resultPanelWidth, 0) + 2; 208 | this.resultPanelWidth = parseInt(resultPanelWidth, 0); 209 | return style; 210 | } 211 | 212 | handleSelect(valueList, labelList, leafList, cascadeSelected) { 213 | this.setState({ 214 | displayValue: CascadeMultiSelect.getInputValue(this.props), 215 | value: valueList, 216 | result: { 217 | valueList, 218 | labelList, 219 | leafList, 220 | cascadeSelected, 221 | }, 222 | }, () => { 223 | this.props.onSelect(valueList, labelList, leafList, cascadeSelected); 224 | }); 225 | this.hasChanged = true; 226 | } 227 | 228 | handleItemClick(...params) { 229 | this.props.onItemClick(...params); 230 | } 231 | 232 | handleStopPropagation(e) { 233 | const tagName = e.target.tagName; 234 | if (tagName === 'DIV') { 235 | e.stopPropagation(); 236 | } 237 | } 238 | 239 | updateDescription(description) { 240 | if (description.value !== this.state.description.value) { 241 | this.setState({ description }); 242 | } 243 | } 244 | 245 | renderInput() { 246 | const { prefixCls, placeholder, locale, readOnly, disabled } = this.props; 247 | const { displayValue } = this.state; 248 | if (readOnly) { 249 | return {displayValue}; 250 | } 251 | return ( 252 |
    253 | { 254 | !displayValue.length ? 255 |
    256 | {placeholder || i18n(locale).placeholder} 257 |
    : 258 |
    259 | {}} 266 | /> 267 |
    268 | } 269 |
    270 | ); 271 | } 272 | 273 | renderCloseIcon() { 274 | const { disabled } = this.props; 275 | if (disabled) { return null; } 276 | return ( 277 |
    278 | { 281 | this.onCleanSelect(); 282 | e.preventDefault(); 283 | e.stopPropagation(); 284 | }} 285 | /> 286 |
    287 | ); 288 | } 289 | 290 | renderContent() { 291 | const { className, prefixCls, size, allowClear, disabled } = this.props; 292 | const { displayValue, showSubMenu } = this.state; 293 | const prefixCls2 = 'kuma-cascader'; 294 | return ( 295 |
    0, 303 | })} 304 | > 305 |
    311 |
    312 | {this.renderInput()} 313 |
    314 |
    315 |
    321 | 322 |
    323 | {this.renderCloseIcon()} 324 |
    325 | ); 326 | } 327 | 328 | renderCascadeMultiPanel() { 329 | const { dropdownClassName, prefixCls } = this.props; 330 | const { value } = this.state; 331 | return ( 332 |
    333 |
    334 | { this.CascadeMulti = r; }} 339 | onSelect={this.handleSelect} 340 | onItemClick={this.handleItemClick} 341 | updateDescription={this.updateDescription} 342 | mode="mix" 343 | /> 344 | {this.renderFooter()} 345 |
    346 |
    347 | ); 348 | } 349 | 350 | renderDescription() { 351 | const { prefixCls } = this.props; 352 | const { description } = this.state; 353 | if (!description.value) { 354 | return null; 355 | } 356 | return ( 357 |
    360 | {`${description.label}: ${description.description}`} 361 |
    362 | ); 363 | } 364 | 365 | renderFooter() { 366 | const { prefixCls, locale } = this.props; 367 | return ( 368 |
    373 | {this.renderDescription()} 374 | 379 |
    380 | ); 381 | } 382 | 383 | render() { 384 | const { disabled, getPopupContainer, readOnly } = this.props; 385 | if (readOnly) { 386 | return this.renderInput(); 387 | } 388 | if (disabled) { 389 | return this.renderContent(); 390 | } 391 | const CascadeMultiComponent = this.renderCascadeMultiPanel(); 392 | return ( 393 | { 398 | this.onDropDownVisibleChange(visible); 399 | }} 400 | getPopupContainer={getPopupContainer} 401 | > 402 | {this.renderContent()} 403 | 404 | ); 405 | } 406 | } 407 | 408 | CascadeMultiSelect.defaultProps = { 409 | className: '', 410 | prefixCls: 'kuma-cascade-multi', 411 | config: [], 412 | options: [], 413 | cascadeSize: 3, 414 | value: [], 415 | notFoundContent: '', 416 | allowClear: true, 417 | locale: 'zh-cn', 418 | onSelect: () => {}, 419 | onItemClick: () => {}, 420 | 421 | size: 'large', 422 | placeholder: '', 423 | disabled: false, 424 | defaultValue: [], 425 | dropdownClassName: '', 426 | onOk: () => {}, 427 | onCancel: () => {}, 428 | getPopupContainer: null, 429 | 430 | beforeRender: null, 431 | readOnly: false, 432 | keyCouldDuplicated: false, 433 | isCleanDisabledLabel: false, 434 | }; 435 | 436 | CascadeMultiSelect.propTypes = { 437 | className: PropTypes.string, 438 | prefixCls: PropTypes.string, 439 | config: PropTypes.array, 440 | options: PropTypes.array, 441 | cascadeSize: PropTypes.number, 442 | value: PropTypes.array, 443 | notFoundContent: PropTypes.string, 444 | allowClear: PropTypes.bool, 445 | locale: PropTypes.string, 446 | onSelect: PropTypes.func, 447 | onItemClick: PropTypes.func, 448 | 449 | size: PropTypes.oneOf(['large', 'middle', 'small']), 450 | placeholder: PropTypes.string, 451 | disabled: PropTypes.bool, 452 | defaultValue: PropTypes.array, 453 | dropdownClassName: PropTypes.string, 454 | onOk: PropTypes.func, 455 | onCancel: PropTypes.func, 456 | getPopupContainer: PropTypes.func, 457 | 458 | beforeRender: PropTypes.func, 459 | readOnly: PropTypes.bool, 460 | keyCouldDuplicated: PropTypes.bool, 461 | isCleanDisabledLabel: PropTypes.bool, 462 | }; 463 | 464 | CascadeMultiSelect.displayName = 'CascadeMultiSelect'; 465 | 466 | CascadeMultiSelect.CascadeMultiPanel = CascadeMultiPanel; 467 | CascadeMultiSelect.CascadeMultiModal = CascadeMultiModal; 468 | 469 | module.exports = CascadeMultiSelect; 470 | -------------------------------------------------------------------------------- /src/CascadeMultiPanel.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * CascadeMultiSelect Component for uxcore 3 | * @author changming 4 | * 5 | * Copyright 2015-2017, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | import React from 'react'; 9 | import classnames from 'classnames'; 10 | import deepcopy from 'lodash/cloneDeep'; 11 | import PropTypes from 'prop-types'; 12 | import i18n from './locale'; 13 | import { getDisabledValueLabel, getCascadeSelected, getWidthStyle } from './utils'; 14 | 15 | class CascadeMulti extends React.Component { 16 | static getDerivedStateFromProps(props, state) { 17 | const { value, options, keyCouldDuplicated } = props; 18 | if (value === state.lastValue && options === state.lastOptions) { 19 | return null; 20 | } 21 | const { dataList, allItemDisabled } = 22 | CascadeMulti.setData(value, options, keyCouldDuplicated); 23 | return { dataList, allItemDisabled, lastValue: value, lastOptions: options }; 24 | } 25 | 26 | /** 27 | * 外部设置组件的 value 28 | * @param value 设置的结果 29 | * @param options 选项列表 30 | * @param from 从哪里调用 31 | */ 32 | static setData(value, options, keyCouldDuplicated) { 33 | let dataList = deepcopy(options); 34 | let allItemDisabled = true; 35 | if (dataList && dataList.length) { 36 | const res = CascadeMulti.setCleanResult(dataList, true, keyCouldDuplicated); 37 | dataList = res.listArray; 38 | allItemDisabled = res.allItemDisabled; 39 | for (let i = 0, len = value.length; i < len; i += 1) { 40 | const $id = value[i]; 41 | const treeNodeObj = 42 | CascadeMulti.getTreeNodeData(dataList, $id, null, keyCouldDuplicated); 43 | if (treeNodeObj) { 44 | const { parentNode, itemNode } = treeNodeObj; 45 | itemNode.checked = true; 46 | if (itemNode.children) { 47 | itemNode.children = CascadeMulti.setChildrenChecked( 48 | itemNode.children, 49 | true, 50 | true, 51 | ); 52 | } 53 | if (parentNode) { 54 | CascadeMulti.setFatherCheckState(itemNode, true, dataList, keyCouldDuplicated); 55 | } 56 | } 57 | } 58 | } 59 | return { dataList, allItemDisabled }; 60 | } 61 | 62 | /** 63 | * 清空 64 | */ 65 | static setCleanResult(dataList, isCleanDisabledLabel, keyCouldDuplicated) { 66 | const listArray = deepcopy(dataList); 67 | let allItemDisabled = true; 68 | const recursion = (list, rootNum = -1, ancestorNodes = [], isChildrenCheck = false) => { 69 | if (list && list.length) { 70 | // 处理 dataList 添加根节点标识,因为除了第一级、根级以外其余级别可能会重复 71 | for (let i = 0; i < list.length; i++) { 72 | let newAncestorNodes = []; 73 | const item = list[i]; 74 | item.halfChecked = false; 75 | item.rootNum = rootNum === -1 ? i : rootNum; 76 | item.$id = keyCouldDuplicated ? 77 | `${item.rootNum}/${item.value}` : 78 | `${item.value}`; // 如果每一级的 value 有可能会重复时,则使用 rootNum + value 作为 id 79 | if (!isChildrenCheck) { 80 | // 当isCleanDisabledLabel=false,被选中且disabled节点需处理父级的halfChecked 81 | if ((!isCleanDisabledLabel && item.disabled) && item.checked) { 82 | newAncestorNodes.forEach(ii => { 83 | ii.halfChecked = true; // eslint-disable-line 84 | }); 85 | isChildrenCheck = true; // eslint-disable-line 86 | } else { 87 | item.checked = false; 88 | } 89 | } 90 | newAncestorNodes = ancestorNodes.concat(item); 91 | if (item.children) { 92 | recursion(item.children, item.rootNum, newAncestorNodes, isChildrenCheck); 93 | } 94 | if (item.disabled !== true) { 95 | allItemDisabled = false; 96 | } 97 | } 98 | } 99 | }; 100 | 101 | recursion(listArray); 102 | 103 | return { 104 | listArray, 105 | allItemDisabled, 106 | }; 107 | } 108 | 109 | /** 110 | * 根据传入的 key 获取其节点,父节点 111 | * @param dataList 组件的 options 112 | * @param key 要查询的 item.$id 也有可能是 $item.value 113 | * @param parentNode 父节点(方法自用) 114 | */ 115 | static getTreeNodeData(dataList, key, parentNode = null, keyCouldDuplicated) { 116 | let back = null; 117 | if (!key) { return null; } 118 | if (dataList && dataList.length) { 119 | for (let i = 0, len = dataList.length; i < len; i += 1) { 120 | let theKey = `${key}`; 121 | if (keyCouldDuplicated && theKey.indexOf('/') === -1) { 122 | theKey = `${dataList[i].rootNum}/${key}`; 123 | } 124 | if (dataList[i].$id === theKey) { 125 | return { 126 | parentNode, 127 | itemNode: dataList[i], 128 | }; 129 | } 130 | if (dataList[i].children) { 131 | const item = 132 | CascadeMulti.getTreeNodeData( 133 | dataList[i].children, theKey, dataList[i], keyCouldDuplicated 134 | ); 135 | back = item || back; 136 | } 137 | } 138 | } 139 | return back; 140 | } 141 | 142 | /** 143 | * 设置children选中/取消状态 144 | * @param childrenList 子集 145 | * @param checked 设置的状态 146 | */ 147 | static setChildrenChecked( 148 | dataList, 149 | checked, 150 | isCleanDisabledLabel, 151 | itemDisabledNodes = [], 152 | ) { 153 | const childrenList = deepcopy(dataList); 154 | if (childrenList && childrenList.length) { 155 | for (let i = 0; i < childrenList.length; i++) { 156 | const item = childrenList[i]; 157 | if (!isCleanDisabledLabel && item.disabled) { 158 | if (checked !== item.checked) { 159 | itemDisabledNodes.push(item); 160 | } 161 | } else { 162 | item.checked = checked; 163 | item.halfChecked = false; 164 | if (item.children) { 165 | item.children = this.setChildrenChecked( 166 | item.children, 167 | checked, 168 | isCleanDisabledLabel, 169 | itemDisabledNodes 170 | ); 171 | } 172 | } 173 | } 174 | } 175 | return childrenList; 176 | } 177 | 178 | /** 179 | * 设置父亲节点的选中/半选中状态 180 | * @param item 当前节点 181 | * @param checked 设置状态 182 | */ 183 | static setFatherCheckState(item, checked, dataList, keyCouldDuplicated) { 184 | const treeNodeObj = 185 | CascadeMulti.getTreeNodeData(dataList, item.$id, null, keyCouldDuplicated); 186 | const { parentNode } = treeNodeObj; 187 | if (parentNode) { 188 | const halfChecked = CascadeMulti.getBotherCheckedState(parentNode.children, !checked); 189 | if (halfChecked) { 190 | parentNode.checked = !halfChecked; 191 | parentNode.halfChecked = halfChecked; 192 | } else { 193 | parentNode.checked = checked; 194 | parentNode.halfChecked = false; 195 | } 196 | CascadeMulti.setFatherCheckState(parentNode, checked, dataList, keyCouldDuplicated); 197 | } 198 | } 199 | 200 | /** 201 | * 获取兄弟节点指定选中状态 202 | * @param botherList 兄弟节点列表 203 | * @param state 查询的选中状态 204 | * @return 兄弟节点中包含对应状态结果 boolean 205 | */ 206 | static getBotherCheckedState(botherList, state) { 207 | let handleCheckedState = false; 208 | if (botherList && botherList.length) { 209 | for (let i = 0, len = botherList.length; i < len; i += 1) { 210 | // 查询是否存在选中 211 | if (state) { 212 | if (botherList[i].checked || botherList[i].halfChecked) { 213 | handleCheckedState = true; 214 | break; 215 | } 216 | } else { 217 | // 查询是否存在未选中 218 | // 要么未选中,要么半选中状态 219 | if ( 220 | !botherList[i].checked && !botherList[i].halfChecked || 221 | !botherList[i].checked && botherList[i].halfChecked 222 | ) { 223 | handleCheckedState = true; 224 | break; 225 | } 226 | } 227 | } 228 | } 229 | return handleCheckedState; 230 | } 231 | 232 | constructor(props) { 233 | super(props); 234 | this.state = { 235 | dataList: [], 236 | selectArray: [], 237 | allItemDisabled: true, // 所有选项都被禁用 238 | }; 239 | const { value, options, keyCouldDuplicated } = this.props; 240 | if (value) { 241 | const { dataList, allItemDisabled } = 242 | CascadeMulti.setData(value, options, keyCouldDuplicated); 243 | this.state.dataList = dataList; 244 | this.state.allItemDisabled = allItemDisabled; 245 | } 246 | } 247 | 248 | /** 249 | * 选项列表点击事件 250 | */ 251 | onItemClick(data, level) { 252 | const { selectArray } = this.state; 253 | if (data.$id !== selectArray[level]) { 254 | selectArray.splice(level + 1); 255 | } 256 | selectArray[level] = data.$id; 257 | if (this.props.onItemClick) { 258 | this.props.onItemClick({ 259 | value: data.value, 260 | label: data.label, 261 | children: data.children, 262 | }, level + 1, selectArray); 263 | } 264 | this.setState({ selectArray }, () => { 265 | if (data.description) { 266 | this.props.updateDescription({ 267 | description: data.description, 268 | value: data.value, 269 | label: data.label, 270 | }); 271 | } 272 | }); 273 | } 274 | 275 | /** 276 | * 选中/取消选项事件 277 | */ 278 | onItemChecked(item, level) { 279 | const { isCleanDisabledLabel, keyCouldDuplicated } = this.props; 280 | const { dataList } = this.state; 281 | const treeNodeObj = 282 | CascadeMulti.getTreeNodeData(dataList, item.$id, null, keyCouldDuplicated); 283 | const { itemNode } = treeNodeObj; 284 | itemNode.checked = !itemNode.checked; 285 | itemNode.halfChecked = false; 286 | this.itemDisabledNodes = []; 287 | // 设置子集全部选中 288 | if (itemNode.children) { 289 | itemNode.children = CascadeMulti.setChildrenChecked( 290 | itemNode.children, 291 | itemNode.checked, 292 | isCleanDisabledLabel, 293 | this.itemDisabledNodes 294 | ); 295 | } 296 | if (!isCleanDisabledLabel && this.itemDisabledNodes.length > 0) { 297 | itemNode.checked = false; 298 | itemNode.halfChecked = true; 299 | let itemDisabledNode = this.itemDisabledNodes.pop(); 300 | while (itemDisabledNode) { 301 | CascadeMulti.setFatherCheckState( 302 | itemDisabledNode, itemDisabledNode.checked, dataList, keyCouldDuplicated 303 | ); 304 | itemDisabledNode = this.itemDisabledNodes.pop(); 305 | } 306 | } else if (level) { 307 | // 设置父级选中状态 308 | CascadeMulti.setFatherCheckState(itemNode, itemNode.checked, dataList, keyCouldDuplicated); 309 | } 310 | this.setState({ dataList }, () => { 311 | this.setSelectResult(); 312 | }); 313 | } 314 | 315 | /** 316 | * 清空结果事件 317 | */ 318 | onCleanSelect() { 319 | const { dataList } = this.state; 320 | const { value, isCleanDisabledLabel, keyCouldDuplicated, onSelect } = this.props; 321 | this.setState({ 322 | dataList: CascadeMulti.setCleanResult( 323 | dataList, isCleanDisabledLabel, keyCouldDuplicated 324 | ).listArray, 325 | }, function afterClean() { 326 | if (isCleanDisabledLabel) { 327 | onSelect([], [], []); 328 | } else { 329 | const { disabledNodes, leafNodes: leafList } = getDisabledValueLabel(dataList, value); 330 | const valueList = []; 331 | const labelList = []; 332 | disabledNodes.forEach((item) => { 333 | valueList.push(item.value); 334 | labelList.push(item.label); 335 | }); 336 | onSelect( 337 | valueList, labelList, leafList, getCascadeSelected(this.state.dataList, valueList) 338 | ); 339 | } 340 | }); 341 | } 342 | 343 | /** 344 | * 展开/收起结果列 345 | */ 346 | onTriggerNode(item) { 347 | const { dataList } = this.state; 348 | const { keyCouldDuplicated } = this.props; 349 | const treeNodeObj = 350 | CascadeMulti.getTreeNodeData(dataList, item.$id, null, keyCouldDuplicated); 351 | const { itemNode } = treeNodeObj; 352 | itemNode.expand = !itemNode.expand; 353 | this.setState({ dataList }); 354 | } 355 | 356 | /** 357 | * 删除选项事件 358 | */ 359 | onDeleteItem(item, level) { 360 | const { dataList } = this.state; 361 | const { keyCouldDuplicated, isCleanDisabledLabel } = this.props; 362 | const treeNodeObj = 363 | CascadeMulti.getTreeNodeData(dataList, item.$id, null, keyCouldDuplicated); 364 | const { itemNode } = treeNodeObj; 365 | itemNode.checked = false; 366 | itemNode.halfChecked = false; 367 | if (itemNode.children) { 368 | itemNode.children = CascadeMulti.setChildrenChecked( 369 | itemNode.children, 370 | false, 371 | isCleanDisabledLabel, 372 | this.itemDisabledNodes 373 | ); 374 | } 375 | if (level) { 376 | CascadeMulti.setFatherCheckState(itemNode, false, dataList, keyCouldDuplicated); 377 | } 378 | this.setSelectResult(); 379 | } 380 | 381 | /** 382 | * 获取选中的结果 383 | * @param dataList 组件选项列表 384 | * @param arr 存放结果 value 的数组 385 | * @param textArr 存放结果 label 的数组 386 | * @param back 存放选中的级联结构 387 | */ 388 | getSelectResult(dataList, arr, textArr, back = []) { 389 | if (dataList && dataList.length) { 390 | dataList.forEach((item) => { 391 | if (item.checked) { 392 | arr.push(item.value); 393 | textArr.push(item.label); 394 | back.push(item); 395 | } 396 | if (item.halfChecked) { 397 | const backItem = { 398 | label: item.label, 399 | value: item.value, 400 | children: [], 401 | }; 402 | backItem.children = this.getSelectResult(item.children, arr, textArr, backItem.children); 403 | back.push(backItem); 404 | } 405 | }); 406 | } 407 | return back; 408 | } 409 | 410 | getAllLeafNode(dataList = []) { 411 | let back = []; 412 | dataList.forEach(item => { 413 | if ((item.checked || item.halfChecked) && item.children && item.children.length) { 414 | back = back.concat(this.getAllLeafNode(item.children)); 415 | } else if (item.checked) { 416 | back.push({ 417 | value: item.value, 418 | label: item.label, 419 | }); 420 | } 421 | }); 422 | return back; 423 | } 424 | 425 | /** 426 | * 获取选中的数量 427 | */ 428 | getNums(dataList) { 429 | if (dataList && dataList.length) { 430 | dataList.forEach((item) => { 431 | if (item.checked || item.halfChecked) { 432 | this.selectNums += 1; 433 | if (item.children) { 434 | this.getNums(item.children); 435 | } else { 436 | this.handleSelectNums += 1; 437 | } 438 | } 439 | }); 440 | } 441 | } 442 | 443 | /** 444 | * 设置选中的结果 445 | */ 446 | setSelectResult() { 447 | const arr = []; 448 | this.textArr = []; 449 | this.leafArr = this.getAllLeafNode(this.state.dataList); 450 | const cascadeSelected = this.getSelectResult(this.state.dataList, arr, this.textArr); 451 | this.props.onSelect(arr, this.textArr, this.leafArr, cascadeSelected); 452 | } 453 | 454 | /** 455 | * 设置组件宽度样式,兼容名称过长时显示效果等 456 | */ 457 | setPanelWidth() { 458 | const resultPanelWidth = getWidthStyle(this.refResultPanel, 220); 459 | this.resultPanelWidth = parseInt(resultPanelWidth, 0); 460 | } 461 | 462 | /** 463 | * 渲染对应级的选项面板 464 | */ 465 | renderUlList(level) { 466 | const t = this; 467 | const { prefixCls, notFoundContent, config, locale, keyCouldDuplicated } = this.props; 468 | const { dataList, selectArray } = this.state; 469 | if (!dataList.length) { 470 | return ( 471 |
      { this.refUls = r; }} 478 | /> 479 | ); 480 | } 481 | const treeNodeObj = CascadeMulti.getTreeNodeData( 482 | dataList, 483 | selectArray[level - 1], 484 | null, 485 | keyCouldDuplicated 486 | ); 487 | const childrenList = ( 488 | treeNodeObj && 489 | treeNodeObj.itemNode && 490 | treeNodeObj.itemNode.children && 491 | treeNodeObj.itemNode.children.length 492 | ) ? treeNodeObj.itemNode.children : []; 493 | const listArray = level ? childrenList : dataList; 494 | const noDataText = notFoundContent || i18n(locale).noData; 495 | return ( 496 |
      497 | { 498 | config[level] && config[level].showSearch ? 499 |
      500 | { 504 | const val = e.target.value; 505 | const keywords = this.keywords || []; 506 | keywords[level] = val; 507 | clearTimeout(this.showSearchKeywordsTiming); 508 | this.showSearchKeywordsTiming = setTimeout(() => { 509 | this.setState({ showSearchKeywords: keywords }); 510 | }, 200); 511 | }} 512 | /> 513 |
      514 | : null 515 | } 516 |
        { this.refUls = r; }} 519 | > 520 | { 521 | selectArray[level - 1] && !listArray.length ? 522 | {noDataText} : 523 | t.renderListItems(listArray, level) 524 | } 525 |
      526 |
      527 | ); 528 | } 529 | 530 | /** 531 | * 渲染对应级的 ListItem 532 | */ 533 | renderListItems(dataList, level) { 534 | const { prefixCls, config, mode } = this.props; 535 | const { selectArray } = this.state; 536 | const arr = []; 537 | // 设置当前级是否开启 checkbox 538 | const checkable = !(config[level] && config[level].checkable === false); 539 | dataList.forEach((item) => { 540 | // 如果只是用面板,则默认选择第一项 541 | if (mode === 'independent' && !selectArray[level]) { 542 | selectArray[level] = item.$id; 543 | } 544 | 545 | const { showSearchKeywords } = this.state; 546 | if (showSearchKeywords && item.label && item.label.indexOf(showSearchKeywords) === -1) { 547 | return; 548 | } 549 | 550 | arr.push( 551 |
    • { this.onItemClick(item, level); }} 560 | > 561 | 586 |
    • 587 | ); 588 | }); 589 | return arr; 590 | } 591 | 592 | /** 593 | * 渲染结果面板 594 | */ 595 | renderResult() { 596 | const { prefixCls, allowClear, locale } = this.props; 597 | return ( 598 |
      { this.refResultPanel = r; }} 601 | > 602 |
      603 | {i18n(locale).selected} {this.renderResultNums()} 604 | { 605 | (allowClear && this.state.allItemDisabled === false) ? 606 | { this.onCleanSelect(); }} 609 | > 610 | {i18n(locale).clean} 611 | : 612 | null 613 | } 614 |
      615 | {this.renderResultTree()} 616 |
      617 | ); 618 | } 619 | 620 | /** 621 | * 渲染已选中节点数量 622 | */ 623 | renderResultNums() { 624 | const { dataList } = this.state; 625 | // 记录所有选中的叶子节点 626 | this.handleSelectNums = 0; 627 | // 记录所有选中的节点 628 | this.selectNums = 0; 629 | this.getNums(dataList); 630 | return ( 631 | ({this.handleSelectNums}) 632 | ); 633 | } 634 | 635 | /** 636 | * 渲染已选择结果 TreeList 637 | */ 638 | renderResultTree() { 639 | const { prefixCls } = this.props; 640 | const { dataList } = this.state; 641 | return ( 642 |
      645 | {this.renderTreeListNode(dataList, 0)} 646 |
      647 | ); 648 | } 649 | 650 | /** 651 | * 渲染已选择结果 TreeListNode 652 | */ 653 | renderTreeListNode(dataList, level) { 654 | const { cascadeSize, isCleanDisabledLabel } = this.props; 655 | const arr = []; 656 | if (dataList && dataList.length) { 657 | dataList.forEach((item) => { 658 | if (item.checked || item.halfChecked) { 659 | // 设置 label 的宽度 660 | const style = { maxWidth: 0 }; 661 | // 86 = marginLeft(15) + 箭头icon占位宽度(21) + "删除"按钮的宽度(30) + marginRight(20) 662 | style.maxWidth = this.resultPanelWidth - 86 - (level * 15); 663 | // 56 = "已选择"文字宽度 664 | style.maxWidth -= level < cascadeSize - 1 && item.checked ? 56 : 0; 665 | let isDelete = !item.disabled; 666 | if (!isCleanDisabledLabel) { 667 | isDelete = item.checked && !item.disabled; 668 | } 669 | arr.push( 670 |
    • { this.refResultUl = r; }} 676 | title={item.label} 677 | key={item.$id} 678 | onClick={(e) => { 679 | e.stopPropagation(); 680 | this.onTriggerNode(item); 681 | }} 682 | > 683 |
      687 | { 688 | this.renderExpand(item) 689 | } 690 | 691 | { 692 | 696 | {item.label} 697 | 698 | } 699 | { 700 | level < cascadeSize - 1 && 701 | item.checked && item.children && item.children.length ? 702 | 703 | {i18n(this.props.locale).haveAll} 704 | : 705 | null 706 | } 707 | { 708 | !isDelete ? null : 709 | { 712 | e.stopPropagation(); 713 | this.onDeleteItem(item, level); 714 | }} 715 | > 716 | {i18n(this.props.locale).delete} 717 | 718 | } 719 | 720 |
      721 | { 722 | item.children && !item.expand ? 723 | this.renderTreeListNode(item.children, level + 1) : 724 | null 725 | } 726 |
    • 727 | ); 728 | } 729 | }); 730 | } 731 | return ( 732 |
        735 | {arr} 736 |
      737 | ); 738 | } 739 | 740 | /** 741 | * 渲染结果列表展开/收缩按钮 742 | */ 743 | renderExpand(item) { 744 | let arr = []; 745 | if (item.children && item.children.length) { 746 | arr = !item.expand ? : 747 | ; 748 | } else { 749 | // 21 = kuma-icon的占位宽度 750 | arr = ; 751 | } 752 | return arr; 753 | } 754 | 755 | render() { 756 | const { className, prefixCls, cascadeSize, mode } = this.props; 757 | const arr = []; 758 | let minWidth = 0; 759 | for (let i = 0; i < cascadeSize; i += 1) { 760 | arr.push(this.renderUlList(i)); 761 | minWidth = 150 * cascadeSize + 222; 762 | } 763 | this.setPanelWidth(); 764 | const back = ( 765 |
      { 770 | e.stopPropagation(); 771 | }} 772 | style={{ minWidth }} 773 | > 774 | {arr} 775 | {this.renderResult()} 776 |
      777 | ); 778 | 779 | if (mode === 'independent') { 780 | return
      {back}
      ; 781 | } 782 | 783 | return back; 784 | } 785 | 786 | } 787 | 788 | CascadeMulti.defaultProps = { 789 | className: '', 790 | prefixCls: 'kuma-cascade-multi', 791 | config: [], 792 | options: [], 793 | cascadeSize: 3, 794 | value: [], 795 | notFoundContent: '', 796 | allowClear: true, 797 | locale: 'zh-cn', 798 | onSelect: () => { }, 799 | onItemClick: () => { }, 800 | mode: 'independent', 801 | keyCouldDuplicated: false, 802 | isCleanDisabledLabel: false, 803 | }; 804 | 805 | CascadeMulti.propTypes = { 806 | className: PropTypes.string, 807 | prefixCls: PropTypes.string, 808 | config: PropTypes.array, 809 | options: PropTypes.array, 810 | cascadeSize: PropTypes.number, 811 | value: PropTypes.array, 812 | notFoundContent: PropTypes.string, 813 | allowClear: PropTypes.bool, 814 | locale: PropTypes.string, 815 | onSelect: PropTypes.func, 816 | onItemClick: PropTypes.func, 817 | mode: PropTypes.oneOf(['independent', 'mix']), 818 | keyCouldDuplicated: PropTypes.bool, 819 | isCleanDisabledLabel: PropTypes.bool, 820 | updateDescription: PropTypes.func, 821 | }; 822 | 823 | CascadeMulti.displayName = 'CascadeMulti'; 824 | 825 | module.exports = CascadeMulti; 826 | --------------------------------------------------------------------------------