From 48bdfcd4b2731913839b903200824f8aa1b8e96e Mon Sep 17 00:00:00 2001 From: wuduoyi Date: Fri, 17 Apr 2020 16:39:40 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20gitee=20=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9c963d01..45c2a04e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 目前在百度大量用于内部平台的前端开发,已有 100+ 部门使用,创建了 2.5w+ 页面。 通过 amis 搭建自己的后台系统,可以参考这: https://github.com/fex-team/amis-admin -包含:fis3版本、webpack版本和 jssdk 版本。 +包含:fis3 版本、webpack 版本和 jssdk 版本。 ## 快速开始 @@ -13,6 +13,8 @@ ## 开发指南 +> 如果 github 下载慢可以使用 [gitee](https://gitee.com/baidu/amis) 上的镜像。 + 推荐使用 node 10,node 6 和 node 8 也可以。node 12 未测试,可能 fis3 还有些插件不支持。 ```bash @@ -46,9 +48,9 @@ npm run coverage ## 维护者 -- [2betop](https://github.com/2betop) -- [RickCole21](https://github.com/RickCole21) -- [catchonme](https://github.com/catchonme) +- [2betop](https://github.com/2betop) +- [RickCole21](https://github.com/RickCole21) +- [catchonme](https://github.com/catchonme) ## 讨论 From db21de1c54f1722d308abd278677b642a8f9a2b4 Mon Sep 17 00:00:00 2001 From: rickcole Date: Mon, 20 Apr 2020 11:33:06 +0800 Subject: [PATCH 2/8] =?UTF-8?q?nested-select=E6=94=AF=E6=8C=81=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=EF=BC=8C=E5=B9=B6=E4=BC=98=E5=8C=96=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/renderers/Form/NestedSelect.md | 27 +- scss/components/form/_nested-select.scss | 40 ++- src/renderers/Form/NestedSelect.tsx | 353 ++++++++++++++++------- src/renderers/Page.tsx | 2 +- src/utils/helper.ts | 30 ++ 5 files changed, 323 insertions(+), 129 deletions(-) diff --git a/docs/renderers/Form/NestedSelect.md b/docs/renderers/Form/NestedSelect.md index aaebdc65..17a09c9f 100644 --- a/docs/renderers/Form/NestedSelect.md +++ b/docs/renderers/Form/NestedSelect.md @@ -2,18 +2,21 @@ 树形结构选择框。 -- `type` 请设置成 `nested-select` -- `options` 类似于 [select](./Select.md) 中 `options`, 并且支持通过 `children` 无限嵌套。 -- `source` Api 地址,如果选项不固定,可以通过配置 `source` 动态拉取。 -- `multiple` 默认为 `false`, 设置成 `true` 表示可多选。 -- `joinValues` 默认为 `true` -- 单选模式:当用户选中某个选项时,选项中的 value 将被作为该表单项的值提交,否则,整个选项对象都会作为该表单项的值提交。 -- 多选模式:选中的多个选项的 `value` 会通过 `delimiter` 连接起来,否则直接将以数组的形式提交值。 -- `extractValue` 默认为 `false`, `joinValues`设置为`false`时生效, 开启后将选中的选项 value 的值封装为数组,作为当前表单项的值。 -- `delimiter` 默认为 `,` -- `autoFill` 将当前已选中的选项的某个字段的值自动填充到表单中某个表单项中,只在单选时有效 - - `autoFill`的格式为`{address: "${label}"}`,表示将选中项中的`label`的值,自动填充到当前表单项中`name` 为`address` 中 -- **还有更多通用配置请参考** [FormItem](./FormItem.md) +- `type` 请设置成 `nested-select` +- `options` 类似于 [select](./Select.md) 中 `options`, 并且支持通过 `children` 无限嵌套。 +- `source` Api 地址,如果选项不固定,可以通过配置 `source` 动态拉取。 +- `multiple` 默认为 `false`, 设置成 `true` 表示可多选。 +- `searchable` 默认为 `false`, 是否可搜索 +- `joinValues` 默认为 `true` +- 单选模式:当用户选中某个选项时,选项中的 value 将被作为该表单项的值提交,否则,整个选项对象都会作为该表单项的值提交。 +- 多选模式:选中的多个选项的 `value` 会通过 `delimiter` 连接起来,否则直接将以数组的形式提交值。 +- `extractValue` 默认为 `false`, `joinValues`设置为`false`时生效, 开启后将选中的选项 value 的值封装为数组,作为当前表单项的值。 +- `delimiter` 默认为 `,` +- `autoFill` 将当前已选中的选项的某个字段的值自动填充到表单中某个表单项中,只在单选时有效 + - `autoFill`的格式为`{address: "${label}"}`,表示将选中项中的`label`的值,自动填充到当前表单项中`name` 为`address` 中 +- `cascade` 设置成 `true` 时当选中父节点时不自动选择子节点。 +- `withChildren` 是指成 `true`,选中父节点时,值里面将包含子节点的值,否则只会保留父节点的值。 +- **还有更多通用配置请参考** [FormItem](./FormItem.md) ```schema:height="300" scope="form-item" { diff --git a/scss/components/form/_nested-select.scss b/scss/components/form/_nested-select.scss index 6b8230ae..49d0574b 100644 --- a/scss/components/form/_nested-select.scss +++ b/scss/components/form/_nested-select.scss @@ -52,6 +52,12 @@ flex-grow: 1; } + .#{$ns}Select-arrow { + position: absolute; + right: 0; + top: px2rem(10px); + } + &-clear { padding: px2rem(3px); cursor: pointer; @@ -84,15 +90,25 @@ } } - &-menuOuter, - &-childrenOuter { - z-index: 10; - position: absolute; + &-menuOuter { + display: flex; + } + + &-menu { + width: 160px; + max-width: 160px; + max-height: px2rem(300px); background: $Form-select-menu-bg; color: $Form-select-menu-color; border: $Form-select-outer-borderWidth solid $Form-input-onFocused-borderColor; box-shadow: $Form-select-outer-boxShadow; + overflow-y: auto; + overflow-x: hidden; + + &:not(:first-child) { + border-left: 0; + } .#{$ns}NestedSelect-option { position: relative; @@ -100,6 +116,12 @@ height: $Form-input-height; line-height: $Form-input-height; cursor: pointer; + display: flex; + + > label { + flex: 1; + cursor: pointer; + } &.is-active { color: $Form-select-menu-onActive-color; @@ -125,14 +147,4 @@ } } } - - &-childrenOuter { - display: none; - position: relative; - width: 100%; - left: 100%; - transform: translateY(-$Form-input-height); - padding: 0; - margin: 0; - } } diff --git a/src/renderers/Form/NestedSelect.tsx b/src/renderers/Form/NestedSelect.tsx index 07e38f3d..115aaee7 100644 --- a/src/renderers/Form/NestedSelect.tsx +++ b/src/renderers/Form/NestedSelect.tsx @@ -6,10 +6,17 @@ import Checkbox from '../../components/Checkbox'; import PopOver from '../../components/PopOver'; import {RootCloseWrapper} from 'react-overlays'; import {Icon} from '../../components/icons'; -import {autobind, flattenTree, isEmpty} from '../../utils/helper'; +import { + autobind, + flattenTree, + isEmpty, + filterTreeWithChildren +} from '../../utils/helper'; import {dataMapping} from '../../utils/tpl-builtin'; - import {OptionsControl, OptionsControlProps, Option} from '../Form/Options'; +import Input from '../../components/Input'; +import {findDOMNode} from 'react-dom'; +import {cloneDeep} from 'lodash'; export interface NestedSelectProps extends OptionsControlProps { cascade?: boolean; @@ -18,6 +25,9 @@ export interface NestedSelectProps extends OptionsControlProps { export interface NestedSelectState { isOpened?: boolean; + isFocused?: boolean; + inputValue?: string; + stack?: Array; } export default class NestedSelectControl extends React.Component< @@ -26,12 +36,17 @@ export default class NestedSelectControl extends React.Component< > { static defaultProps: Partial = { cascade: false, - withChildren: false + withChildren: false, + searchPromptText: '输入内容进行检索' }; target: any; + input: HTMLInputElement; alteredOptions: any; state = { - isOpened: false + isOpened: false, + isFocused: false, + inputValue: '', + stack: [] }; @autobind @@ -41,9 +56,11 @@ export default class NestedSelectControl extends React.Component< @autobind open() { - if (!this.props.disabled) { + const {options, disabled} = this.props; + if (!disabled) { this.setState({ - isOpened: true + isOpened: true, + stack: [options] }); } } @@ -51,7 +68,8 @@ export default class NestedSelectControl extends React.Component< @autobind close() { this.setState({ - isOpened: false + isOpened: false, + stack: [] }); } @@ -101,6 +119,10 @@ export default class NestedSelectControl extends React.Component< onBulkChange } = this.props; + if (multiple) { + return; + } + e.stopPropagation(); const sendTo = @@ -120,7 +142,7 @@ export default class NestedSelectControl extends React.Component< !multiple && this.close(); } - handleCheck(option: any | Array) { + handleCheck(option: any | Array, index?: number) { const { onChange, selectedOptions, @@ -129,8 +151,27 @@ export default class NestedSelectControl extends React.Component< delimiter, extractValue, withChildren, - cascade + cascade, + multiple } = this.props; + const {stack} = this.state; + + if ( + option.children && + option.children.length && + typeof index === 'number' + ) { + const checked = selectedOptions.some(o => o.value == option.value); + const uncheckable = cascade + ? false + : option.uncheckable || (multiple && !checked); + const children = option.children.map(c => ({...c, uncheckable})); + if (stack[index]) { + stack.splice(index + 1, 1, children); + } else { + stack.push(children); + } + } const items = selectedOptions.concat(); let newValue; @@ -148,13 +189,18 @@ export default class NestedSelectControl extends React.Component< newValue = xorBy(items, [option], valueField || 'value'); } else if (withChildren) { option = flattenTree([option]); - const fn = option.every((opt: any) => !!~items.indexOf(opt)) + const fn = option.every( + (opt: any) => !!~items.findIndex(item => item.value === opt.value) + ) ? xorBy : unionBy; newValue = fn(items, option, valueField || 'value'); } else { - newValue = items.filter(item => !~flattenTree([option]).indexOf(item)); - !~items.indexOf(option) && newValue.push(option); + newValue = items.filter( + item => !~flattenTree([option], i => i.value).indexOf(item.value) + ); + !~items.map(item => item.value).indexOf(option.value) && + newValue.push(option); } } else { newValue = xorBy(items, [option], valueField || 'value'); @@ -172,22 +218,24 @@ export default class NestedSelectControl extends React.Component< } allChecked(options: Array): boolean { + const {selectedOptions, withChildren} = this.props; return options.every((option: any) => { - if (option.children) { + if (withChildren && option.children) { return this.allChecked(option.children); } - return this.props.selectedOptions.some( + return selectedOptions.some( selectedOption => selectedOption.value == option.value ); }); } partialChecked(options: Array): boolean { + const {selectedOptions, withChildren} = this.props; return options.some((option: any) => { - if (option.children) { + if (withChildren && option.children) { return this.partialChecked(option.children); } - return this.props.selectedOptions.some( + return selectedOptions.some( selectedOption => selectedOption.value == option.value ); }); @@ -198,11 +246,79 @@ export default class NestedSelectControl extends React.Component< reload && reload(); } - renderOptions( - newOptions: Array, - isChildren: boolean, - uncheckable: boolean - ): any { + @autobind + onFocus(e: any) { + this.props.disabled || + this.state.isOpened || + this.setState( + { + isFocused: true + }, + this.focus + ); + + this.props.onFocus && this.props.onFocus(e); + } + + @autobind + onBlur(e: any) { + this.setState({ + isFocused: false + }); + + this.props.onBlur && this.props.onBlur(e); + } + + @autobind + focus() { + this.input + ? this.input.focus() + : this.getTarget() && this.getTarget().focus(); + } + + @autobind + blur() { + this.input + ? this.input.blur() + : this.getTarget() && this.getTarget().blur(); + } + + @autobind + getTarget() { + if (!this.target) { + this.target = findDOMNode(this) as HTMLElement; + } + return this.target as HTMLElement; + } + + @autobind + inputRef(ref: HTMLInputElement) { + this.input = ref; + } + + @autobind + handleInputChange(evt: React.ChangeEvent) { + const inputValue = evt.currentTarget.value; + const {options, labelField, valueField} = this.props; + + let filtedOptions = + inputValue && this.state.isOpened + ? filterTreeWithChildren(cloneDeep(options), option => { + const reg = new RegExp(`${inputValue}`, 'i'); + return ( + reg.test(option[labelField || 'label']) || + reg.test(option[valueField || 'value']) + ); + }) + : options.concat(); + + this.setState({ + inputValue, + stack: [filtedOptions] + }); + } + + renderOptions(): any { const { multiple, selectedOptions, @@ -210,87 +326,119 @@ export default class NestedSelectControl extends React.Component< value, options, disabled, - cascade + searchable, + searchPromptText } = this.props; - if (multiple) { - let partialChecked = this.partialChecked(options); - let allChecked = this.allChecked(options); + const stack = this.state.stack; - return ( -
- {!isChildren ? ( -
- - 全选 - -
- ) : null} - {newOptions.map((option, idx) => { - const checked = selectedOptions.some(o => o.value == option.value); - const selfChecked = !!uncheckable || checked; - let nodeDisabled = !!uncheckable || !!disabled; + const searchInput = searchable ? ( +
+ + +
+ ) : null; - return ( -
- - {option.label} - - {option.children ? ( -
- -
- ) : null} - {option.children && option.children.length - ? this.renderOptions( - option.children, - true, - cascade ? false : uncheckable || (multiple && checked) - ) - : null} -
- ); - })} -
- ); - } + let partialChecked = this.partialChecked(options); + let allChecked = this.allChecked(options); return ( -
- {newOptions.map((option, idx) => ( -
- {option.label} - {option.children ? ( -
- + <> + {stack.map((options, index) => ( +
+ {index === 0 ? searchInput : null} + {multiple && index === 0 ? ( +
+ + 全选 +
) : null} - {option.children && option.children.length - ? this.renderOptions(option.children, true, false) - : null} + + {options.map((option, idx) => { + const checked = selectedOptions.some( + o => o.value == option.value + ); + const selfChecked = !!option.uncheckable || checked; + let nodeDisabled = !!option.uncheckable || !!disabled; + + return ( +
+ {multiple ? ( + + {option.label} + + ) : ( + {option.label} + )} + + {option.children && option.children.length ? ( +
+ +
+ ) : null} +
+ ); + })}
))} -
+ ); } + onMouseEnter(option: Option, index: number, e: MouseEvent) { + let {stack} = this.state; + let {cascade, multiple, selectedOptions} = this.props; + index = index + 1; + + if (option.children && option.children.length) { + const checked = selectedOptions.some(o => o.value == option.value); + const uncheckable = cascade + ? false + : option.uncheckable || (multiple && checked); + const children = option.children.map(c => ({...c, uncheckable})); + if (stack[index]) { + stack.splice(index, 1, children); + } else { + stack.push(children); + } + } else { + stack[index] && stack.splice(index, 1); + } + + this.setState({stack}); + } + renderOuter() { - const {popOverContainer, options, classnames: cx} = this.props; + const {popOverContainer, classnames: cx} = this.props; let body = ( - {this.renderOptions(options, false, false)} + {this.renderOptions()}
); - if (popOverContainer) { - return ( - this.target} show> - - {body} - - - ); - } - return body; + return ( + + + {body} + + + ); } render() { diff --git a/src/renderers/Page.tsx b/src/renderers/Page.tsx index e41fbfa4..6adf0d98 100644 --- a/src/renderers/Page.tsx +++ b/src/renderers/Page.tsx @@ -66,7 +66,7 @@ export default class Page extends React.Component { static propsList: Array = [ 'title', - 'subtitle', + 'subTitle', 'initApi', 'initFetchOn', 'initFetch', diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 3d0db117..df9c3b07 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -848,6 +848,36 @@ export function filterTree( }); } +/** + * 这个和 filterTree 的区别是,这个会保留 children 也符合匹配的节点 + * + * @param tree + * @param iterator + * @param level + */ +export function filterTreeWithChildren( + tree: Array, + iterator: (item: T, key: number, level: number) => boolean, + level: number = 1 +): Array { + return tree.filter((item, index) => { + if (iterator(item, index, level)) { + return true; + } + + if (item.children && item.children.splice) { + item.children = filterTreeWithChildren( + item.children, + iterator, + level + 1 + ); + return item.children?.length; + } + + return false; + }); +} + /** * 判断树中每个节点是否满足某个条件。 * @param tree From 89c0f87daab9ba74b0a4668ea90988cea824c843 Mon Sep 17 00:00:00 2001 From: rickcole Date: Mon, 20 Apr 2020 13:27:03 +0800 Subject: [PATCH 3/8] fix filterMapWithChildren --- docs/renderers/Form/NestedSelect.md | 4 ++-- src/renderers/Form/NestedSelect.tsx | 3 +-- src/utils/helper.ts | 37 +++++++++++++---------------- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/docs/renderers/Form/NestedSelect.md b/docs/renderers/Form/NestedSelect.md index 17a09c9f..6a71bf25 100644 --- a/docs/renderers/Form/NestedSelect.md +++ b/docs/renderers/Form/NestedSelect.md @@ -1,12 +1,12 @@ ### NestedSelect -树形结构选择框。 +嵌套选择框。 - `type` 请设置成 `nested-select` - `options` 类似于 [select](./Select.md) 中 `options`, 并且支持通过 `children` 无限嵌套。 - `source` Api 地址,如果选项不固定,可以通过配置 `source` 动态拉取。 - `multiple` 默认为 `false`, 设置成 `true` 表示可多选。 -- `searchable` 默认为 `false`, 是否可搜索 +- `searchable` 默认为 `false`, 表示是否可搜索 - `joinValues` 默认为 `true` - 单选模式:当用户选中某个选项时,选项中的 value 将被作为该表单项的值提交,否则,整个选项对象都会作为该表单项的值提交。 - 多选模式:选中的多个选项的 `value` 会通过 `delimiter` 连接起来,否则直接将以数组的形式提交值。 diff --git a/src/renderers/Form/NestedSelect.tsx b/src/renderers/Form/NestedSelect.tsx index 115aaee7..daf153c4 100644 --- a/src/renderers/Form/NestedSelect.tsx +++ b/src/renderers/Form/NestedSelect.tsx @@ -16,7 +16,6 @@ import {dataMapping} from '../../utils/tpl-builtin'; import {OptionsControl, OptionsControlProps, Option} from '../Form/Options'; import Input from '../../components/Input'; import {findDOMNode} from 'react-dom'; -import {cloneDeep} from 'lodash'; export interface NestedSelectProps extends OptionsControlProps { cascade?: boolean; @@ -303,7 +302,7 @@ export default class NestedSelectControl extends React.Component< let filtedOptions = inputValue && this.state.isOpened - ? filterTreeWithChildren(cloneDeep(options), option => { + ? filterTreeWithChildren(options, option => { const reg = new RegExp(`${inputValue}`, 'i'); return ( reg.test(option[labelField || 'label']) || diff --git a/src/utils/helper.ts b/src/utils/helper.ts index df9c3b07..024046a5 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -850,32 +850,27 @@ export function filterTree( /** * 这个和 filterTree 的区别是,这个会保留 children 也符合匹配的节点 - * - * @param tree - * @param iterator - * @param level + * + * @param tree + * @param iterator + * @param level */ export function filterTreeWithChildren( tree: Array, iterator: (item: T, key: number, level: number) => boolean, level: number = 1 -): Array { - return tree.filter((item, index) => { - if (iterator(item, index, level)) { - return true; - } - - if (item.children && item.children.splice) { - item.children = filterTreeWithChildren( - item.children, - iterator, - level + 1 - ); - return item.children?.length; - } - - return false; - }); +) { + return tree + .filter((item, index) => iterator(item, index, level) || item.children) + .map(item => { + if (item.children && item.children.splice) { + item = { + ...item, + children: filterTreeWithChildren(item.children, iterator, level + 1) + }; + } + return item; + }); } /** From b29b54cb1afb922bd846adfc134cffd105d3bafb Mon Sep 17 00:00:00 2001 From: rickcole Date: Mon, 20 Apr 2020 13:35:08 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E5=85=88=E6=8A=8A=E6=96=87=E6=A1=A3autofll?= =?UTF-8?q?l=E5=88=A0=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/renderers/Form/NestedSelect.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/renderers/Form/NestedSelect.md b/docs/renderers/Form/NestedSelect.md index 6a71bf25..c53393da 100644 --- a/docs/renderers/Form/NestedSelect.md +++ b/docs/renderers/Form/NestedSelect.md @@ -12,8 +12,6 @@ - 多选模式:选中的多个选项的 `value` 会通过 `delimiter` 连接起来,否则直接将以数组的形式提交值。 - `extractValue` 默认为 `false`, `joinValues`设置为`false`时生效, 开启后将选中的选项 value 的值封装为数组,作为当前表单项的值。 - `delimiter` 默认为 `,` -- `autoFill` 将当前已选中的选项的某个字段的值自动填充到表单中某个表单项中,只在单选时有效 - - `autoFill`的格式为`{address: "${label}"}`,表示将选中项中的`label`的值,自动填充到当前表单项中`name` 为`address` 中 - `cascade` 设置成 `true` 时当选中父节点时不自动选择子节点。 - `withChildren` 是指成 `true`,选中父节点时,值里面将包含子节点的值,否则只会保留父节点的值。 - **还有更多通用配置请参考** [FormItem](./FormItem.md) From 57427f0c47affd43cf062fc2be0d09cc39033fd3 Mon Sep 17 00:00:00 2001 From: rickcole Date: Mon, 20 Apr 2020 14:10:40 +0800 Subject: [PATCH 5/8] fix type --- src/renderers/Form/NestedSelect.tsx | 56 +++++++++++++++-------------- src/utils/helper.ts | 29 ++------------- 2 files changed, 31 insertions(+), 54 deletions(-) diff --git a/src/renderers/Form/NestedSelect.tsx b/src/renderers/Form/NestedSelect.tsx index daf153c4..6d0c569a 100644 --- a/src/renderers/Form/NestedSelect.tsx +++ b/src/renderers/Form/NestedSelect.tsx @@ -6,14 +6,10 @@ import Checkbox from '../../components/Checkbox'; import PopOver from '../../components/PopOver'; import {RootCloseWrapper} from 'react-overlays'; import {Icon} from '../../components/icons'; -import { - autobind, - flattenTree, - isEmpty, - filterTreeWithChildren -} from '../../utils/helper'; +import {autobind, flattenTree, isEmpty, filterTree} from '../../utils/helper'; import {dataMapping} from '../../utils/tpl-builtin'; -import {OptionsControl, OptionsControlProps, Option} from '../Form/Options'; +import {OptionsControl, OptionsControlProps} from '../Form/Options'; +import {Option, Options} from '../../components/Select'; import Input from '../../components/Input'; import {findDOMNode} from 'react-dom'; @@ -26,7 +22,7 @@ export interface NestedSelectState { isOpened?: boolean; isFocused?: boolean; inputValue?: string; - stack?: Array; + stack: Array>; } export default class NestedSelectControl extends React.Component< @@ -40,8 +36,7 @@ export default class NestedSelectControl extends React.Component< }; target: any; input: HTMLInputElement; - alteredOptions: any; - state = { + state: NestedSelectState = { isOpened: false, isFocused: false, inputValue: '', @@ -141,7 +136,7 @@ export default class NestedSelectControl extends React.Component< !multiple && this.close(); } - handleCheck(option: any | Array, index?: number) { + handleCheck(option: Option | Options, index?: number) { const { onChange, selectedOptions, @@ -156,11 +151,14 @@ export default class NestedSelectControl extends React.Component< const {stack} = this.state; if ( + !Array.isArray(option) && option.children && option.children.length && typeof index === 'number' ) { - const checked = selectedOptions.some(o => o.value == option.value); + const checked = selectedOptions.some( + o => o.value == (option as Option).value + ); const uncheckable = cascade ? false : option.uncheckable || (multiple && !checked); @@ -173,7 +171,7 @@ export default class NestedSelectControl extends React.Component< } const items = selectedOptions.concat(); - let newValue; + let newValue: Option | Options | string; // 三种情况: // 1.全选,option为数组 @@ -189,14 +187,17 @@ export default class NestedSelectControl extends React.Component< } else if (withChildren) { option = flattenTree([option]); const fn = option.every( - (opt: any) => !!~items.findIndex(item => item.value === opt.value) + (opt: Option) => !!~items.findIndex(item => item.value === opt.value) ) ? xorBy : unionBy; - newValue = fn(items, option, valueField || 'value'); + newValue = fn(items, [option], valueField || 'value'); } else { newValue = items.filter( - item => !~flattenTree([option], i => i.value).indexOf(item.value) + item => + !~flattenTree([option], i => (i as Option).value).indexOf( + item.value + ) ); !~items.map(item => item.value).indexOf(option.value) && newValue.push(option); @@ -206,19 +207,19 @@ export default class NestedSelectControl extends React.Component< } if (joinValues) { - newValue = newValue - .map((item: any) => item[valueField || 'value']) + newValue = (newValue as Options) + .map(item => item[valueField || 'value']) .join(delimiter || ','); } else if (extractValue) { - newValue = newValue.map((item: any) => item[valueField || 'value']); + newValue = (newValue as Options).map(item => item[valueField || 'value']); } onChange(newValue); } - allChecked(options: Array): boolean { + allChecked(options: Options): boolean { const {selectedOptions, withChildren} = this.props; - return options.every((option: any) => { + return options.every(option => { if (withChildren && option.children) { return this.allChecked(option.children); } @@ -228,9 +229,9 @@ export default class NestedSelectControl extends React.Component< }); } - partialChecked(options: Array): boolean { + partialChecked(options: Options): boolean { const {selectedOptions, withChildren} = this.props; - return options.some((option: any) => { + return options.some(option => { if (withChildren && option.children) { return this.partialChecked(option.children); } @@ -302,11 +303,12 @@ export default class NestedSelectControl extends React.Component< let filtedOptions = inputValue && this.state.isOpened - ? filterTreeWithChildren(options, option => { + ? filterTree(options, option => { const reg = new RegExp(`${inputValue}`, 'i'); return ( reg.test(option[labelField || 'label']) || - reg.test(option[valueField || 'value']) + reg.test(option[valueField || 'value']) || + option.children ); }) : options.concat(); @@ -317,7 +319,7 @@ export default class NestedSelectControl extends React.Component< }); } - renderOptions(): any { + renderOptions() { const { multiple, selectedOptions, @@ -370,7 +372,7 @@ export default class NestedSelectControl extends React.Component<
) : null} - {options.map((option, idx) => { + {options.map((option: Option, idx: number) => { const checked = selectedOptions.some( o => o.value == option.value ); diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 024046a5..4e7adea3 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -834,39 +834,14 @@ export function filterTree( tree: Array, iterator: (item: T, key: number, level: number) => boolean, level: number = 1 -) { - return tree.filter((item, index) => { - if (!iterator(item, index, level)) { - return false; - } - - if (item.children && item.children.splice) { - item.children = filterTree(item.children, iterator, level + 1); - } - - return true; - }); -} - -/** - * 这个和 filterTree 的区别是,这个会保留 children 也符合匹配的节点 - * - * @param tree - * @param iterator - * @param level - */ -export function filterTreeWithChildren( - tree: Array, - iterator: (item: T, key: number, level: number) => boolean, - level: number = 1 ) { return tree - .filter((item, index) => iterator(item, index, level) || item.children) + .filter((item, index) => iterator(item, index, level)) .map(item => { if (item.children && item.children.splice) { item = { ...item, - children: filterTreeWithChildren(item.children, iterator, level + 1) + children: filterTree(item.children, iterator, level + 1) }; } return item; From fa9bb7d60cd9f2b24b4ec13c6267a34e2453eda4 Mon Sep 17 00:00:00 2001 From: rickcole Date: Mon, 20 Apr 2020 14:12:32 +0800 Subject: [PATCH 6/8] bugfix --- src/renderers/Form/NestedSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/Form/NestedSelect.tsx b/src/renderers/Form/NestedSelect.tsx index 6d0c569a..692fecd4 100644 --- a/src/renderers/Form/NestedSelect.tsx +++ b/src/renderers/Form/NestedSelect.tsx @@ -191,7 +191,7 @@ export default class NestedSelectControl extends React.Component< ) ? xorBy : unionBy; - newValue = fn(items, [option], valueField || 'value'); + newValue = fn(items, option, valueField || 'value'); } else { newValue = items.filter( item => From a1618cabde668b1b5523dacc9615adcce1cf8fca Mon Sep 17 00:00:00 2001 From: rickcole Date: Mon, 20 Apr 2020 16:27:51 +0800 Subject: [PATCH 7/8] fix nested-select iter --- src/renderers/Form/NestedSelect.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/renderers/Form/NestedSelect.tsx b/src/renderers/Form/NestedSelect.tsx index 692fecd4..978e98e9 100644 --- a/src/renderers/Form/NestedSelect.tsx +++ b/src/renderers/Form/NestedSelect.tsx @@ -301,16 +301,15 @@ export default class NestedSelectControl extends React.Component< const inputValue = evt.currentTarget.value; const {options, labelField, valueField} = this.props; + const regexp = new RegExp(`${inputValue}`, 'i'); + const iterator = (option: Option) => + regexp.test(option[labelField || 'label']) || + regexp.test(option[valueField || 'value']) || + (!!option.children && option.children.some(iterator)); + let filtedOptions = inputValue && this.state.isOpened - ? filterTree(options, option => { - const reg = new RegExp(`${inputValue}`, 'i'); - return ( - reg.test(option[labelField || 'label']) || - reg.test(option[valueField || 'value']) || - option.children - ); - }) + ? filterTree(options, iterator) : options.concat(); this.setState({ From 79b9e37539eaa90167667adfe823ef559df3b8b6 Mon Sep 17 00:00:00 2001 From: rickcole Date: Mon, 20 Apr 2020 17:19:26 +0800 Subject: [PATCH 8/8] =?UTF-8?q?filterTree=20=E6=89=A9=E5=85=85=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=E4=BC=98=E5=85=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderers/Form/NestedSelect.tsx | 14 +++++++++----- src/utils/helper.ts | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/renderers/Form/NestedSelect.tsx b/src/renderers/Form/NestedSelect.tsx index 978e98e9..77d0ad4d 100644 --- a/src/renderers/Form/NestedSelect.tsx +++ b/src/renderers/Form/NestedSelect.tsx @@ -302,14 +302,18 @@ export default class NestedSelectControl extends React.Component< const {options, labelField, valueField} = this.props; const regexp = new RegExp(`${inputValue}`, 'i'); - const iterator = (option: Option) => - regexp.test(option[labelField || 'label']) || - regexp.test(option[valueField || 'value']) || - (!!option.children && option.children.some(iterator)); let filtedOptions = inputValue && this.state.isOpened - ? filterTree(options, iterator) + ? filterTree( + options, + option => + regexp.test(option[labelField || 'label']) || + regexp.test(option[valueField || 'value']) || + !!(option.children && option.children.length), + 1, + true + ) : options.concat(); this.setState({ diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 4e7adea3..ed2c85c7 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -833,15 +833,28 @@ export function getTree( export function filterTree( tree: Array, iterator: (item: T, key: number, level: number) => boolean, - level: number = 1 + level: number = 1, + depthFirst: boolean = false ) { + if (depthFirst) { + return tree + .map(item => { + let children: TreeArray | undefined = item.children + ? filterTree(item.children, iterator, level + 1, depthFirst) + : undefined; + children && (item = {...item, children: children}); + return item; + }) + .filter((item, index) => iterator(item, index, level)); + } + return tree .filter((item, index) => iterator(item, index, level)) .map(item => { if (item.children && item.children.splice) { item = { ...item, - children: filterTree(item.children, iterator, level + 1) + children: filterTree(item.children, iterator, level + 1, depthFirst) }; } return item;