merge: feat-virtual-list into master (#5829)

* feat: select transfer 除树形模式外,增加虚拟列表能力 (#5612)

* feat: select transfer 除树形模式外,增加虚拟列表能力

* 修改分组模式问题

* 更新快照

* transfer 虚拟列表单测补充

* select 虚拟列表单测补充

* 更新 select 快照

* chore: merge master into feat-virtual-list branch (#5716)

* chore: combo组件items扩充编辑器的拖拽点位 (#5693)

* feat: combo组件items支持编辑器的拖拽

* feat: combo组件items支持编辑器的拖拽

* feat: combo组件items支持编辑器的拖拽

* feat: combo组件items支持编辑器的拖拽

* fix: 数字输入框-属性配置-placeholder置灰+边框与form-input统一 amis-saas-4938 (#5666)

Co-authored-by: limengyang03 <limengyang03@baidu.com>

* feat:Remark组件支持自定义图标 (#5694)

* feat:Remark组件支持自定义图标

* 去掉多余内容

Co-authored-by: xujiahao01 <xujiahao01@baidu.com>

* fix: 修复 amis本地跑不起来 问题

* fix: https://github.com/baidu/amis/issues/4681

* feat: 使用cross-env 设置NODE_ENV 变量

* perf: 删除重复声明项

* revert: 恢复删除的SchemaRedirect

Co-authored-by: 吴多益 <wuduoyi@baidu.com>

* feat: ui 组件 form 支持 autoSubmit (#5695)

* feat: ui 组件 form 支持 autoSubmit

* 下发 onSubmit 否则只能用 handleSubmit 这个会绕过 form 的检测,无法通知到上层

* chore: sdk embed 方法添加 callback 在 callback 中可确保 scoped 方法是可用的 (#5698)

* publish beta

* chore: sdk embed 方法添加 callback 在 callback 中可确保 scoped 方法是可用的

* fix: filter 过滤器 isTrue/isFalse 问题修复 (#5676)

* fix: filter 过滤器 isTrue 问题修复

* 修改

* 修改

* 修改

* chore: InputArray新增默认值示例, 调整Combo组件scaffold逻辑 (#5701)

* chore: InputNumber严格判断大数模式,避免错误抛出string类型值 (#5703)

* fix: 修复查看代码报 require is not defined 错误 (#5704)

* publish beta

* fix: 修复查看代码报 require is not defined 错误

* feat: 补充 ConfirmBox ui 控件, 并将 PickerContainer 改成 ConfirmBox 实现 (#5708)

* publish beta

* feat: 添加 ui ConfirmBox

* feat: 补充 confirmBox ui 控件, 并将 pickerContainer 改成 confirmBox 实现

* PickerContainer title 逻辑不变动

* 暴露 InputTableColumnProps

* 调整 ts 定义

* 升级 react-hook-form

* inputTable 补充数组本身的验证

* Combo 也支持内部数组的验证

* 调整内部验证

* 调整目录

* chore: 优化 locale, theme hoc, 存在 context 直接复用 (#5702)

* publish beta

* chore: 优化 locale, theme hoc, 存在 context 直接复用

* chore: 日期范围类组件单元测试补充 (#5705)

* dateRange

* datetimeRange

* timeRange

* monthRange

* quarter and year

* 修改

* 修改

* fix: 评分组件 count 支持变量获取 (#5681)

* fix: 评分组件 count 支持变量获取

* tokenize 修改为 filter

Co-authored-by: zhou999 <zhousq809@163.com>
Co-authored-by: PE_Sicca <46698676+swjtulmy@users.noreply.github.com>
Co-authored-by: limengyang03 <limengyang03@baidu.com>
Co-authored-by: 徐佳豪 <1440054388@qq.com>
Co-authored-by: xujiahao01 <xujiahao01@baidu.com>
Co-authored-by: h7ml <h7ml@qq.com>
Co-authored-by: 吴多益 <wuduoyi@baidu.com>
Co-authored-by: liaoxuezhi <2betop.cn@gmail.com>
Co-authored-by: sansiro <sansiro@sansiro.me>

* feat: tree 增加虚拟列表 (#5696)

* feat: Tree 使用 VirtualList 渲染, 提升大数据时的性能

* fix: 适配编辑和新建

* chore: 增加 Tree 测试用例

* chore: 更新 Tree 测试用例

* feat: Tree虚拟列表,调整props名称

* chore: 清理 Tree 相关的无用代码

* feat: select transfer 树形模式增加虚拟滚动支持, treeSelect 虚拟滚动参数 (#5799)

* 其他 transfer 支持虚拟列表属性

* feat: select transfer 树形模式增加虚拟滚动支持, treeSelect 虚拟滚动参数

* add

* fix: tree 多级时,通过递归parent来判断展开收起,递归partial判断父元素的Partial状态 (#5807)

* fix: 调整 tree 组件中,判断父子关系的方法 (#5814)

* Revert "chore: merge master into feat-virtual-list branch (#5716)" (#5825)

This reverts commit c9895812989c31d3471d50c2adaf7110478ad0a0.

* chore: 更新 tree 虚拟滚动相关快照 和一些优化

Co-authored-by: RUNZE LU <36724300+lurunze1226@users.noreply.github.com>
Co-authored-by: zhou999 <zhousq809@163.com>
Co-authored-by: PE_Sicca <46698676+swjtulmy@users.noreply.github.com>
Co-authored-by: limengyang03 <limengyang03@baidu.com>
Co-authored-by: 徐佳豪 <1440054388@qq.com>
Co-authored-by: xujiahao01 <xujiahao01@baidu.com>
Co-authored-by: h7ml <h7ml@qq.com>
Co-authored-by: 吴多益 <wuduoyi@baidu.com>
Co-authored-by: liaoxuezhi <2betop.cn@gmail.com>
Co-authored-by: meerkat <kit_hack@outlook.com>
This commit is contained in:
sansiro 2022-11-28 18:07:45 +08:00 committed by GitHub
parent cb5452d429
commit 5520004dec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 8839 additions and 1667 deletions

View File

@ -991,6 +991,8 @@ true false true [{label: 'A/B/C', value: 'a/b/c'},{label: 'A
| enableNodePath | `boolean` | `false` | 是否开启节点路径模式 |
| pathSeparator | `string` | `/` | 节点路径的分隔符,`enableNodePath`为`true`时生效 |
| highlightTxt | `string` | | 标签中需要高亮的字符,支持变量 |
| itemHeight | `number` | `32` | 每个选项的高度,用于虚拟渲染 |
| virtualThreshold | `number` | `100` | 在选项数量超过多少时开启虚拟渲染 |
## 事件表

View File

@ -846,6 +846,8 @@ icon:
| resultSearchPlaceholder | `string` | | 右侧列表搜索框提示 |
| menuTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义选项展示 |
| valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 |
| itemHeight | `number` | `32` | 每个选项的高度,用于虚拟渲染 |
| virtualThreshold | `number` | `100` | 在选项数量超过多少时开启虚拟渲染 |
## 事件表

View File

@ -1,31 +1,58 @@
const options = [...new Array(1000)].map((item, index) => {
const renderOptions = (n, callback) => {
return [...new Array(n)].map((item, index) => callback(index));
};
const options = renderOptions(1000, index => {
const value = `${10000 + index * 3}`;
return {
value: value,
label: value
label: value,
children:
index % 10 === 0
? renderOptions(20, i => {
return {
value: `value-${value}-${i}`,
label: `label-${value}-${i}`
};
})
: undefined
};
});
export default {
type: 'form',
body: {
type: 'tree-select',
// "type": "nested-select",
name: 'output_fields',
label: '输出字段',
description: '输出字段中的制表符会转换为"\\t",换行符会转换为"\
body: [
{
// type: 'input-tree',
type: 'tree-select',
// "type": "nested-select",
name: 'output_fields',
label: '输出字段',
description: '输出字段中的制表符会转换为"\\t",换行符会转换为"\
"',
mode: 'horizontal',
multiple: true,
required: true,
clearable: true,
withChildren: true,
extractValue: true,
joinValues: true,
cascade: true,
borderMode: 'full',
searchable: true,
options: options,
initiallyOpen: false
}
mode: 'horizontal',
multiple: true,
required: true,
clearable: true,
withChildren: true,
extractValue: true,
joinValues: true,
cascade: true,
borderMode: 'full',
searchable: true,
options: options,
initiallyOpen: false,
draggable: true
},
{
label: '选项',
type: 'select',
name: 'select',
searchable: true,
multiple: true,
menuTpl: '<div>${label} 值:${value}, 当前是否选中: ${checked}</div>',
options: options
}
]
};

View File

@ -495,12 +495,16 @@ export interface FormItemConfig extends FormItemBasicConfig {
const getItemLabelClassName = (props: FormItemProps) => {
const {staticLabelClassName, labelClassName} = props;
return props.static && staticLabelClassName ? staticLabelClassName : labelClassName;
return props.static && staticLabelClassName
? staticLabelClassName
: labelClassName;
};
const getItemInputClassName = (props: FormItemProps) => {
const {staticInputClassName, inputClassName} = props;
return props.static && staticInputClassName ? staticInputClassName : inputClassName;
return props.static && staticInputClassName
? staticInputClassName
: inputClassName;
};
export class FormItemWrap extends React.Component<FormItemProps> {
@ -524,6 +528,12 @@ export class FormItemWrap extends React.Component<FormItemProps> {
() => this.forceUpdate()
)
);
this.reaction.push(
reaction(
() => model?.filteredOptions,
() => this.forceUpdate()
)
);
this.reaction.push(
reaction(
() => JSON.stringify(model.tmpValue),
@ -1001,12 +1011,11 @@ export class FormItemWrap extends React.Component<FormItemProps> {
) : null}
<div
className={cx(`Form-value`,{
// [`Form-itemColumn--offset${getWidthRate(horizontal.offset)}`]: !label && label !== false,
[`Form-itemColumn--${right}`]:
!horizontal.leftFixed && !!right && right !== 12 - left
}
)}
className={cx(`Form-value`, {
// [`Form-itemColumn--offset${getWidthRate(horizontal.offset)}`]: !label && label !== false,
[`Form-itemColumn--${right}`]:
!horizontal.leftFixed && !!right && right !== 12 - left
})}
>
{renderControl()}

View File

@ -392,6 +392,8 @@ export function registerOptionsControl(config: OptionsConfig) {
return true;
} else if (nextProps.formItem?.expressionsInOptions) {
return true;
} else if (nextProps.formItem?.filteredOptions) {
return true;
}
if (anyChanged(detectProps, this.props, nextProps)) {

View File

@ -746,7 +746,9 @@ export function wrapControl<
setValue: this.setValue,
getValue: this.getValue,
prinstine: model ? model.prinstine : undefined,
setPrinstineValue: this.setPrinstineValue
setPrinstineValue: this.setPrinstineValue,
// !没了这个, tree 里的 options 渲染会出问题
_filteredOptions: this.model?.filteredOptions
};
return (

View File

@ -294,7 +294,16 @@ export function isArrayChildrenModified(
}
for (let i: number = prev.length - 1; i >= 0; i--) {
if (strictMode ? prev[i] !== next[i] : prev[i] != next[i]) {
if (
strictMode
? prev[i] !== next[i]
: prev[i] != next[i] ||
isArrayChildrenModified(
prev[i].children,
next[i].children,
strictMode
)
) {
return true;
}
}
@ -1015,15 +1024,15 @@ export function someTree<T extends TreeItem>(
export function flattenTree<T extends TreeItem>(tree: Array<T>): Array<T>;
export function flattenTree<T extends TreeItem, U>(
tree: Array<T>,
mapper: (value: T, index: number) => U
mapper: (value: T, index: number, level: number, paths?: Array<T>) => U
): Array<U>;
export function flattenTree<T extends TreeItem, U>(
tree: Array<T>,
mapper?: (value: T, index: number) => U
mapper?: (value: T, index: number, level: number, paths?: Array<T>) => U
): Array<U> {
let flattened: Array<any> = [];
eachTree(tree, (item, index) =>
flattened.push(mapper ? mapper(item, index) : item)
eachTree(tree, (item, index, level, paths) =>
flattened.push(mapper ? mapper(item, index, level, paths) : item)
);
return flattened;
}

View File

@ -42,6 +42,7 @@
.#{$ns}GroupedSelection {
max-height: px2rem(300px);
height: 100%;
overflow: auto;
user-select: none;
padding: var(--gap-xs) 0;
@ -117,8 +118,22 @@
}
.#{$ns}TableSelection {
position: relative;
height: 100%;
.#{$ns}Table-content {
border-top: var(--Table-borderWidth) solid var(--Table-borderColor);
&.is-virtual {
height: 100%;
display: flex;
flex-direction: column;
.#{$ns}Table-content-virtual {
flex: 1;
position: relative;
}
}
}
.#{$ns}Table-table > tbody > tr {
@ -238,6 +253,7 @@
min-height: 100%;
&-col {
position: relative;
flex-grow: 1;
min-width: 150px;
}
@ -311,11 +327,17 @@
&-left,
&-right {
position: relative;
flex-grow: 1;
width: 0;
min-height: px2rem(200px);
max-height: px2rem(400px);
overflow: auto;
> .#{$ns}GroupedSelection {
padding: 0;
max-height: 100%;
}
}
&-left {

View File

@ -385,6 +385,10 @@
}
}
.#{$ns}Tree.#{$ns}Transfer-checkboxes {
width: 100%;
}
&-search {
margin: var(--gap-sm) var(--gap-sm);
.#{$ns}InputBox {

View File

@ -63,7 +63,7 @@
position: relative;
}
&--outline &-sublist &-item--isLeaf {
&--outline &-item--isLeaf {
&:before {
position: absolute;
top: -8px;

View File

@ -120,7 +120,9 @@ export class AssociatedSelection extends BaseSelection<
cellRender,
multiple,
itemRender,
labelField
labelField,
virtualThreshold,
itemHeight
} = this.props;
const selectdOption = BaseSelection.resolveSelected(
@ -141,6 +143,8 @@ export class AssociatedSelection extends BaseSelection<
options={leftOptions}
onChange={this.handleLeftSelect}
onDeferLoad={this.handleLeftDeferLoad}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
) : (
<GroupedSelecton
@ -151,6 +155,8 @@ export class AssociatedSelection extends BaseSelection<
onChange={this.handleLeftSelect}
multiple={false}
clearable={false}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
)}
</div>
@ -189,6 +195,8 @@ export class AssociatedSelection extends BaseSelection<
option2value={option2value}
cellRender={cellRender}
multiple={multiple}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
) : rightMode === 'tree' ? (
<Tree
@ -198,6 +206,8 @@ export class AssociatedSelection extends BaseSelection<
onChange={onChange!}
multiple={multiple}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
) : rightMode === 'chained' ? (
<ChainedSelection
@ -209,6 +219,8 @@ export class AssociatedSelection extends BaseSelection<
multiple={multiple}
itemRender={itemRender}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
) : (
<GroupedSelection
@ -220,6 +232,8 @@ export class AssociatedSelection extends BaseSelection<
multiple={multiple}
itemRender={itemRender}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
)
) : (

View File

@ -11,6 +11,7 @@ import {getTreeDepth} from 'amis-core';
import times from 'lodash/times';
import Spinner from './Spinner';
import {localeable} from 'amis-core';
import VirtualList, {AutoSizer} from './virtual-list';
export interface ChainedSelectionProps extends BaseSelectionProps {
defaultSelectedIndex?: string;
@ -54,7 +55,13 @@ export class ChainedSelection extends BaseSelection<
);
}
renderItem(option: Option, index: number, depth: number, id: string) {
renderItem(
option: Option,
index: number,
depth: number,
id: string,
styles: object = {}
) {
const {
labelClassName,
disabled,
@ -68,6 +75,7 @@ export class ChainedSelection extends BaseSelection<
return (
<div
style={styles}
key={index}
className={cx(
'ChainedSelection-item',
@ -102,7 +110,13 @@ export class ChainedSelection extends BaseSelection<
);
}
renderOption(option: Option, index: number, depth: number, id: string) {
renderOption(
option: Option,
index: number,
depth: number,
id: string,
styles: object = {}
) {
const {
labelClassName,
disabled,
@ -117,6 +131,7 @@ export class ChainedSelection extends BaseSelection<
if (Array.isArray(option.children) || option.defer) {
return (
<div
style={styles}
key={index}
className={cx(
'ChainedSelection-item',
@ -143,7 +158,7 @@ export class ChainedSelection extends BaseSelection<
);
}
return this.renderItem(option, index, depth, id);
return this.renderItem(option, index, depth, id, styles);
}
render() {
@ -155,7 +170,10 @@ export class ChainedSelection extends BaseSelection<
classnames: cx,
option2value,
itemRender,
translate: __
translate: __,
virtualThreshold = 1000,
itemHeight = 32,
virtualListHeight
} = this.props;
this.valueArray = BaseSelection.value2array(value, options, option2value);
@ -189,33 +207,103 @@ export class ChainedSelection extends BaseSelection<
let nextPlaceholder: string = '';
let nextIndexes = indexes;
body.push(
<div key={depth} className={cx('ChainedSelection-col')}>
{subTitle ? (
<div className={cx('ChainedSelection-subTitle')}>
{subTitle}
</div>
) : null}
{Array.isArray(options) && options.length ? (
options.map((option, index) => {
const id = indexes.concat(index).join('-');
if (Array.isArray(options) && options.length > virtualThreshold) {
options.forEach((option, index) => {
const id = indexes.concat(index).join('-');
if (id === selected) {
nextSubTitle = option.subTitle;
nextOptions = option.children!;
nextIndexes = indexes.concat(index);
nextPlaceholder = option.placeholder;
}
});
if (id === selected) {
nextSubTitle = option.subTitle;
nextOptions = option.children!;
nextIndexes = indexes.concat(index);
nextPlaceholder = option.placeholder;
}
const finalOptions = options.concat();
if (subTitle) {
finalOptions.unshift({
type: 'chainedSelection-subTitle',
value: subTitle
});
}
return this.renderOption(option, index, depth, id);
})
) : (
<div className={cx('ChainedSelection-placeholder')}>
{__(placeholder)}
</div>
)}
</div>
);
body.push(
<div key={depth} className={cx('ChainedSelection-col')}>
<AutoSizer minHeight={virtualListHeight}>
{({height}: {height: number}) => (
<VirtualList
height={height}
itemCount={finalOptions.length}
itemSize={itemHeight}
renderItem={({
index,
style
}: {
index: number;
style?: object;
}) => {
const option = finalOptions[index];
if (!option) {
return null;
}
if (option?.type === 'chainedSelection-subTitle') {
return (
<div
style={{
...style,
width: '100%'
}}
key={indexes.join('-') + 'subTitle'}
className={cx('ChainedSelection-subTitle')}
>
{option.value}
</div>
);
}
index = subTitle ? index - 1 : index;
const id = indexes.concat(index).join('-');
return this.renderOption(option, index, depth, id, {
...style,
width: '100%'
});
}}
/>
)}
</AutoSizer>
</div>
);
} else {
body.push(
<div key={depth} className={cx('ChainedSelection-col')}>
{subTitle ? (
<div className={cx('ChainedSelection-subTitle')}>
{subTitle}
</div>
) : null}
{Array.isArray(options) && options.length ? (
options.map((option, index) => {
const id = indexes.concat(index).join('-');
if (id === selected) {
nextSubTitle = option.subTitle;
nextOptions = option.children!;
nextIndexes = indexes.concat(index);
nextPlaceholder = option.placeholder;
}
return this.renderOption(option, index, depth, id);
})
) : (
<div className={cx('ChainedSelection-placeholder')}>
{__(placeholder)}
</div>
)}
</div>
);
}
return {
options: nextOptions,

View File

@ -1,11 +1,12 @@
import React from 'react';
import {uncontrollable} from 'amis-core';
import {uncontrollable, flattenTree} from 'amis-core';
import {BaseSelection, BaseSelectionProps} from './Selection';
import {themeable} from 'amis-core';
import Checkbox from './Checkbox';
import {Option} from './Select';
import {localeable} from 'amis-core';
import VirtualList, {AutoSizer} from './virtual-list';
export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
valueArray: Array<Option>;
@ -13,20 +14,17 @@ export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
renderOption(
option: Option,
index: number,
key: string = `${index}`
key: string = `${index}`,
styles: object = {}
): JSX.Element {
const {
labelClassName,
disabled,
classnames: cx,
itemClassName,
itemRender,
multiple,
labelField
} = this.props;
const valueArray = this.valueArray;
if (Array.isArray(option.children)) {
if (!option.label) {
return (
@ -63,9 +61,81 @@ export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
);
}
return this.renderPureOption(option, index, key, styles);
}
renderOptionOrLabel(
option: Option,
index: number,
hasParent: boolean = false,
styles: object = {}
): JSX.Element {
const {
disabled,
classnames: cx,
itemRender,
multiple,
labelField
} = this.props;
if (option.children) {
return (
<div
key={index}
style={styles}
className={cx('GroupedSelection-group', option.className)}
>
<div className={cx('GroupedSelection-itemLabel')}>
{itemRender(option, {
index: index,
multiple: multiple,
checked: false,
onChange: () => undefined,
disabled: disabled || option.disabled,
labelField
})}
</div>
</div>
);
}
return hasParent ? (
<div
key={'group' + index}
style={styles}
className={cx('GroupedSelection-group', option.className)}
>
<div className={cx('GroupedSelection-items', option.className)}>
{this.renderPureOption(option, index)}
</div>
</div>
) : (
this.renderPureOption(option, index, undefined, styles)
);
}
renderPureOption(
option: Option,
index: number,
key: string = `${index}`,
styles: object = {}
): JSX.Element {
const {
labelClassName,
disabled,
classnames: cx,
itemClassName,
itemRender,
multiple,
labelField
} = this.props;
const valueArray = this.valueArray;
return (
<div
key={index}
style={styles}
className={cx(
'GroupedSelection-item',
itemClassName,
@ -107,20 +177,63 @@ export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
classnames: cx,
option2value,
onClick,
placeholderRender
placeholderRender,
virtualThreshold = 1000,
itemHeight = 32,
virtualListHeight
} = this.props;
const __ = this.props.translate;
this.valueArray = BaseSelection.value2array(value, options, option2value);
let body: Array<React.ReactNode> = [];
let body: Array<React.ReactNode> | React.ReactNode | null = null;
if (Array.isArray(options) && options.length) {
body = options.map((option, key) => this.renderOption(option, key));
const flattendOptions: Option[] = flattenTree(
options,
(item, index, level) => {
return {
option: item,
hasParent: level > 1
};
}
);
body =
flattendOptions.length > virtualThreshold ? (
<AutoSizer minHeight={virtualListHeight}>
{({height}: {height: number}) => (
<VirtualList
height={height}
itemCount={flattendOptions.length}
itemSize={itemHeight}
renderItem={({
index,
style
}: {
index: number;
style?: object;
}) => {
const {option, hasParent} = flattendOptions[index] || {};
if (!option) {
return null;
}
return this.renderOptionOrLabel(option, index, hasParent, {
...style,
width: '100%'
});
}}
/>
)}
</AutoSizer>
) : (
options.map((option, key) => this.renderOption(option, key))
);
}
return (
<div className={cx('GroupedSelection', className)} onClick={onClick}>
{body && body.length ? (
{body ? (
body
) : (
<div className={cx('GroupedSelection-placeholder')}>

View File

@ -13,6 +13,7 @@ import {autobind, guid} from 'amis-core';
import {LocaleProps, localeable} from 'amis-core';
import {BaseSelection, BaseSelectionProps} from './Selection';
import TransferSearch from './TransferSearch';
import VirtualList, {AutoSizer} from './virtual-list';
export interface ResultListProps extends ThemeProps, LocaleProps {
className?: string;
@ -29,6 +30,8 @@ export interface ResultListProps extends ThemeProps, LocaleProps {
onSearch?: Function;
valueField?: string;
labelField?: string;
itemHeight?: number; // 每个选项的高度,主要用于虚拟渲染
virtualThreshold?: number; // 数据量多大的时候开启虚拟渲染
}
export interface ItemRenderStates {
@ -56,11 +59,17 @@ export class ResultList extends React.Component<
static defaultProps: Pick<
ResultListProps,
'placeholder' | 'itemRender' | 'searchPlaceholder'
| 'placeholder'
| 'itemRender'
| 'searchPlaceholder'
| 'virtualThreshold'
| 'itemHeight'
> = {
placeholder: 'placeholder.selectData',
itemRender: this.itemRender,
searchPlaceholder: ''
searchPlaceholder: '',
virtualThreshold: 100,
itemHeight: 32
};
state: ResultListState = {
@ -223,7 +232,12 @@ export class ResultList extends React.Component<
}
}
renderNormalList(value?: Options) {
renderOption(
option: any,
index: number,
value: Options,
styles: object = {}
) {
const {
classnames: cx,
itemRender,
@ -231,56 +245,92 @@ export class ResultList extends React.Component<
itemClassName,
sortable,
labelField,
translate: __
} = this.props;
return (
<div
style={styles}
className={cx('Selections-item', itemClassName, option?.className)}
key={index}
>
{sortable && !disabled && value!.length > 1 ? (
<Icon className={cx('Selections-dragbar icon')} icon="drag-bar" />
) : null}
<label
className={cx('Selections-label', {
'is-invalid': option?.__unmatched
})}
>
{itemRender(option, {
index,
disabled,
onChange: this.handleValueChange.bind(this, index),
labelField
})}
</label>
{!disabled ? (
<a
className={cx('Selections-delBtn')}
data-index={index}
onClick={(e: React.MouseEvent<HTMLElement>) =>
this.handleCloseItem(e, option)
}
>
<Icon icon="close" className="icon" />
</a>
) : null}
</div>
);
}
renderNormalList(value?: Options) {
const {
classnames: cx,
translate: __,
placeholder
placeholder,
virtualThreshold = 1000,
itemHeight = 30
} = this.props;
return (
<>
{Array.isArray(value) && value.length ? (
<div className={cx('Selections-items')}>
{value.map((option, index) => (
<div
className={cx(
'Selections-item',
itemClassName,
option?.className
)}
key={index}
>
{sortable && !disabled && value.length > 1 ? (
<Icon
className={cx('Selections-dragbar icon')}
icon="drag-bar"
{value.length > virtualThreshold ? (
<AutoSizer>
{({height}: {height: number}) => (
<VirtualList
height={height}
itemCount={value.length}
itemSize={itemHeight}
renderItem={({
index,
style
}: {
index: number;
style?: object;
}) => {
const option = value[index];
if (!option) {
return null;
}
return this.renderOption(option, index, value, {
...style,
width: '100%'
});
}}
/>
) : null}
<label
className={cx('Selections-label', {
'is-invalid': option?.__unmatched
})}
>
{itemRender(option, {
index,
disabled,
onChange: this.handleValueChange.bind(this, index),
labelField
})}
</label>
{!disabled ? (
<a
className={cx('Selections-delBtn')}
data-index={index}
onClick={(e: React.MouseEvent<HTMLElement>) =>
this.handleCloseItem(e, option)
}
>
<Icon icon="close" className="icon" />
</a>
) : null}
</div>
))}
)}
</AutoSizer>
) : (
value.map((option, index) =>
this.renderOption(option, index, value)
)
)}
</div>
) : (
<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>

View File

@ -146,7 +146,9 @@ export class BaseResultTableSelection extends BaseSelection<
option2value,
onChange,
translate: __,
placeholder
placeholder,
virtualThreshold,
itemHeight
} = this.props;
const {searching, tableOptions, searchTableOptions} = this.state;
@ -163,6 +165,8 @@ export class BaseResultTableSelection extends BaseSelection<
onChange={onChange}
multiple={false}
resultMode={true}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
cellRender={(
column: {
name: string;

View File

@ -177,7 +177,8 @@ export class BaseResultTreeList extends React.Component<
const {searching, treeOptions} = this.state;
let temNode: Options = [];
const cb = (node: Option) => {
if (isEqual(node, option)) {
// 对比时去掉 parent因为其无限嵌套
if (isEqual(omit(node, 'parent'), omit(option, 'parent'))) {
temNode = [node];
}
};
@ -194,7 +195,11 @@ export class BaseResultTreeList extends React.Component<
value.filter(
item =>
!arr.find(arrItem =>
isEqual(omit(arrItem, ['isChecked', 'childrens']), item)
// 对比时去掉 parent因为其无限嵌套且不相等
isEqual(
omit(arrItem, ['isChecked', 'childrens', 'parent']),
omit(item, 'parent')
)
)
)
);
@ -258,7 +263,9 @@ export class BaseResultTreeList extends React.Component<
valueField,
itemRender,
translate: __,
placeholder
placeholder,
virtualThreshold,
itemHeight
} = this.props;
const {treeOptions, searching, searchTreeOptions} = this.state;
@ -276,6 +283,8 @@ export class BaseResultTreeList extends React.Component<
itemRender={itemRender}
removable
onDelete={(option: Option) => this.deleteTreeChecked(option)}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
) : (
<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>

View File

@ -34,6 +34,9 @@ export interface BaseSelectionProps extends ThemeProps, LocaleProps {
labelClassName?: string;
option2value?: (option: Option) => any;
itemClassName?: string;
itemHeight?: number; // 每个选项的高度,主要用于虚拟渲染
virtualThreshold?: number; // 数据量多大的时候开启虚拟渲染
virtualListHeight?: number; // 虚拟渲染时,列表高度
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
disabled?: boolean;
onClick?: (e: React.MouseEvent) => void;
@ -66,7 +69,9 @@ export class BaseSelection<
placeholder: 'placeholder.noOption',
itemRender: this.itemRender,
multiple: true,
clearable: false
clearable: false,
virtualThreshold: 1000,
itemHeight: 32
};
static value2array(

View File

@ -1,11 +1,13 @@
import {BaseSelection, BaseSelectionProps} from './Selection';
import {noop, themeable} from 'amis-core';
import {noop, offset, themeable} from 'amis-core';
import React from 'react';
import {uncontrollable} from 'amis-core';
import Checkbox from './Checkbox';
import {Option} from './Select';
import {resolveVariable} from 'amis-core';
import {localeable} from 'amis-core';
import VirtualList, {AutoSizer, RenderedRows} from './virtual-list';
import {isEqual, forEach} from 'lodash';
export interface TableSelectionProps extends BaseSelectionProps {
/** 是否为结果渲染列表 */
@ -27,7 +29,13 @@ export interface TableSelectionProps extends BaseSelectionProps {
) => JSX.Element;
}
export class TableSelection extends BaseSelection<TableSelectionProps> {
export interface TableSelectionState {
colsWidth: number[];
tableWidth: number;
rowRenderScope: null | RenderedRows;
}
export class TableSelection extends BaseSelection<TableSelectionProps, any> {
static defaultProps = {
...BaseSelection.defaultProps,
cellRender: (
@ -42,6 +50,16 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
) => <span>{resolveVariable(column.name, option)}</span>
};
constructor(props: TableSelectionProps) {
super(props);
this.state = {
rowRenderScope: null,
colsWidth: [],
tableWidth: 0
};
}
getColumns() {
let columns = this.props.columns;
@ -77,25 +95,90 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
});
return (
<thead>
<tr>
{multiple && Array.isArray(options) && options.length ? (
<th className={cx('Table-checkCell')}>
<Checkbox
key="checkbox"
size="sm"
disabled={disabled}
onChange={this.toggleAll}
checked={partialChecked}
partial={partialChecked && !allChecked}
/>
</th>
) : null}
{columns.map((column, index) => (
<th key={index}>{column.label}</th>
))}
</tr>
</thead>
<>
<thead>
<tr>
{multiple && Array.isArray(options) && options.length ? (
<th className={cx('Table-checkCell')}>
<Checkbox
key="checkbox"
size="sm"
disabled={disabled}
onChange={this.toggleAll}
checked={partialChecked}
partial={partialChecked && !allChecked}
/>
</th>
) : null}
{columns.map((column, index) => (
<th key={index}>{column.label}</th>
))}
</tr>
</thead>
</>
);
}
renderTr({
option,
rowIndex,
valueArray,
columns,
styles
}: {
option: any;
rowIndex: number;
valueArray: any[];
columns: any[];
styles?: object;
}) {
const {
classnames: cx,
cellRender,
disabled,
multiple,
translate: __,
itemClassName,
resultMode
} = this.props;
const checked = valueArray.indexOf(option) !== -1;
return (
<tr
style={styles ?? {}}
key={rowIndex}
/** 被ResultTableList引用如果设置click事件会导致错误删除结果列表的内容先加一个开关判断 */
onClick={
resultMode
? noop
: e => e.defaultPrevented || this.toggleOption(option)
}
className={cx(
itemClassName,
option.className,
disabled || option.disabled ? 'is-disabled' : '',
!!~valueArray.indexOf(option) ? 'is-active' : ''
)}
>
{multiple ? (
<td
className={cx('Table-checkCell')}
key="checkbox"
onClick={e => {
e.stopPropagation();
this.toggleOption(option);
}}
>
<Checkbox size="sm" checked={checked} disabled={disabled} />
</td>
) : null}
{columns.map((column, colIndex) => (
<td key={colIndex}>
{cellRender(column, option, colIndex, rowIndex)}
</td>
))}
</tr>
);
}
@ -103,15 +186,9 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
const {
options,
placeholder,
classnames: cx,
cellRender,
value,
disabled,
multiple,
option2value,
translate: __,
itemClassName,
resultMode
translate: __
} = this.props;
const columns = this.getColumns();
let valueArray = BaseSelection.value2array(value, options, option2value);
@ -119,45 +196,9 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
return (
<tbody>
{Array.isArray(options) && options.length ? (
options.map((option, rowIndex) => {
const checked = valueArray.indexOf(option) !== -1;
return (
<tr
key={rowIndex}
/** 被ResultTableList引用如果设置click事件会导致错误删除结果列表的内容先加一个开关判断 */
onClick={
resultMode
? noop
: e => e.defaultPrevented || this.toggleOption(option)
}
className={cx(
itemClassName,
option.className,
disabled || option.disabled ? 'is-disabled' : '',
!!~valueArray.indexOf(option) ? 'is-active' : ''
)}
>
{multiple ? (
<td
className={cx('Table-checkCell')}
key="checkbox"
onClick={e => {
e.stopPropagation();
this.toggleOption(option);
}}
>
<Checkbox size="sm" checked={checked} disabled={disabled} />
</td>
) : null}
{columns.map((column, colIndex) => (
<td key={colIndex}>
{cellRender(column, option, colIndex, rowIndex)}
</td>
))}
</tr>
);
})
options.map((option, rowIndex) =>
this.renderTr({option, rowIndex, valueArray, columns})
)
) : (
<tr>
<td colSpan={columns.length}>{__(placeholder)}</td>
@ -167,19 +208,159 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
);
}
render() {
const {className, classnames: cx} = this.props;
ref: any;
tableHeadRef(ref: any) {
ref && (this.ref = ref);
}
handleVirtualTableResize({width}: {width: number}) {
if (width && width === this.state.width) {
return;
}
const widths: any = {};
this.ref &&
forEach(
this.ref.querySelectorAll('thead>tr:last-child>th'),
(item: HTMLElement, index: number) => {
widths[index] = item.getBoundingClientRect().width;
}
);
const colsWidth: number[] = [];
Object.keys(widths)
.filter(key => !isNaN(Number(key)))
.sort()
.forEach(key => {
colsWidth.push(widths[key]);
});
this.setState({colsWidth, tableWidth: width});
}
renderVirtualTable() {
const {
options,
value,
classnames: cx,
option2value,
translate: __,
itemHeight = 30,
virtualListHeight
} = this.props;
const columns = this.getColumns();
let valueArray = BaseSelection.value2array(value, options, option2value);
const {startIndex = 0, stopIndex = 10} = this.state.rowRenderScope || {};
let tableList: React.ReactNode | null = null;
if (startIndex !== undefined && stopIndex !== undefined) {
const trs = [];
for (let index = startIndex; index <= stopIndex; index++) {
const option = options[index];
if (!option) {
break;
}
trs.push(
this.renderTr({
option,
rowIndex: index,
valueArray,
columns,
styles: {
height: `${itemHeight}px`
}
})
);
}
tableList = (
<table
className={cx('Table-table')}
style={{
marginTop: (startIndex || 0) * itemHeight + 'px'
}}
>
{this.state.colsWidth.length ? (
<colgroup>
{this.state.colsWidth.map((colWidth: number, index: number) => (
<col style={{width: `${colWidth}px`}} key={`col-${index}`} />
))}
</colgroup>
) : null}
<tbody>{trs}</tbody>
</table>
);
}
return (
<div className={cx('TableSelection', className)}>
<div className={cx('Table-content', 'is-virtual')}>
<table className={cx('Table-table')} ref={this.tableHeadRef.bind(this)}>
{this.renderTHead()}
</table>
<div className={cx('Table-content-virtual')}>
<AutoSizer
minHeight={virtualListHeight}
onResize={this.handleVirtualTableResize.bind(this)}
>
{({height}: {height: number}) => (
<VirtualList
onItemsRendered={res => {
if (!isEqual(this.state.rowRenderScope, res)) {
// 需要延后执行,否则报 warning
setTimeout(() => {
this.setState({
rowRenderScope: res
});
});
}
}}
height={height}
itemCount={options.length}
itemSize={itemHeight}
WrapperComponent="div"
InnerComponent="div"
prefix={tableList}
innerStyleFilter={(styles: object) => ({
...styles,
position: 'absolute',
top: 0,
minWidth: undefined,
width: '1px',
visibility: 'hidden'
})}
renderItem={() => null}
/>
)}
</AutoSizer>
</div>
</div>
);
}
render() {
const {
className,
classnames: cx,
options,
virtualThreshold = 1000
} = this.props;
const table =
Array.isArray(options) && options.length > virtualThreshold ? (
this.renderVirtualTable()
) : (
<div className={cx('Table-content')}>
<table className={cx('Table-table')}>
{this.renderTHead()}
{this.renderTBody()}
</table>
</div>
</div>
);
);
return <div className={cx('TableSelection', className)}>{table}</div>;
}
}

View File

@ -4,6 +4,7 @@ import Tabs, {Tab} from './Tabs';
import InputBox from './InputBox';
import TableCheckboxes from './TableSelection';
import TreeCheckboxes from './TreeSelection';
import Tree from './Tree';
import ChainedCheckboxes from './ChainedSelection';
import ListCheckboxes from './GroupedSelection';
import {Options, Option} from './Select';
@ -58,6 +59,10 @@ export class TabsTransfer extends React.Component<
TabsTransferProps,
TabsTransferState
> {
static defaultProps = {
multiple: true
};
state = {
inputValue: '',
searchResult: null
@ -153,7 +158,9 @@ export class TabsTransfer extends React.Component<
onChange,
option2value,
cellRender,
optionItemRender
optionItemRender,
itemHeight,
virtualThreshold
} = this.props;
const options = searchResult || [];
const mode = searchResultMode;
@ -169,16 +176,21 @@ export class TabsTransfer extends React.Component<
onChange={onChange}
option2value={option2value}
cellRender={cellRender}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
/>
) : mode === 'tree' ? (
<TreeCheckboxes
<Tree
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
options={options}
value={value}
disabled={disabled}
onChange={onChange}
option2value={option2value}
onChange={onChange!}
joinValues={false}
showIcon={false}
multiple={true}
cascade={true}
itemRender={
optionItemRender
? (item: Option, states: ItemRenderStates) =>
@ -205,6 +217,8 @@ export class TabsTransfer extends React.Component<
})
: undefined
}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
/>
) : (
<ListCheckboxes
@ -223,6 +237,8 @@ export class TabsTransfer extends React.Component<
})
: undefined
}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
/>
);
}
@ -301,7 +317,9 @@ export class TabsTransfer extends React.Component<
onLeftDeferLoad,
cellRender,
translate: __,
optionItemRender
optionItemRender,
itemHeight,
virtualThreshold
} = this.props;
return option.selectMode === 'table' ? (
@ -316,16 +334,20 @@ export class TabsTransfer extends React.Component<
option2value={option2value}
onDeferLoad={onDeferLoad}
cellRender={cellRender}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
/>
) : option.selectMode === 'tree' ? (
<TreeCheckboxes
<Tree
className={cx('Transfer-checkboxes')}
options={option.children || []}
value={value}
multiple={multiple}
disabled={disabled}
onChange={onChange}
option2value={option2value}
onChange={onChange!}
joinValues={false}
showIcon={false}
cascade={true}
onDeferLoad={onDeferLoad}
itemRender={
optionItemRender
@ -336,6 +358,8 @@ export class TabsTransfer extends React.Component<
})
: undefined
}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
/>
) : option.selectMode === 'chained' ? (
<ChainedCheckboxes
@ -357,6 +381,8 @@ export class TabsTransfer extends React.Component<
})
: undefined
}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
/>
) : option.selectMode === 'associated' ? (
<AssociatedCheckboxes
@ -381,6 +407,8 @@ export class TabsTransfer extends React.Component<
})
: undefined
}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
/>
) : (
<ListCheckboxes
@ -401,6 +429,8 @@ export class TabsTransfer extends React.Component<
})
: undefined
}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
/>
);
}

View File

@ -105,6 +105,9 @@ export interface TransferProps
sortable?: boolean;
onRef?: (ref: Transfer) => void;
onSelectAll?: (options: Options) => void;
itemHeight?: number; // 每个选项的高度,主要用于虚拟渲染
virtualThreshold?: number; // 数据量多大的时候开启虚拟渲染`
virtualListHeight?: number; // 虚拟渲染时,列表高度
}
export interface TransferState {
@ -119,12 +122,17 @@ export class Transfer<
> extends React.Component<T, TransferState> {
static defaultProps: Pick<
TransferProps,
'multiple' | 'resultListModeFollowSelect' | 'selectMode' | 'statistics'
| 'multiple'
| 'resultListModeFollowSelect'
| 'selectMode'
| 'statistics'
| 'virtualThreshold'
> = {
multiple: true,
resultListModeFollowSelect: false,
selectMode: 'list',
statistics: true
statistics: true,
virtualThreshold: 100
};
state: TransferState = {
@ -459,7 +467,10 @@ export class Transfer<
optionItemRender,
cellRender,
multiple,
labelField
labelField,
virtualThreshold,
itemHeight,
virtualListHeight
} = props;
const {isTreeDeferLoad, searchResult} = this.state;
const options = searchResult ?? [];
@ -479,6 +490,9 @@ export class Transfer<
cellRender={cellRender}
itemRender={optionItemRender}
multiple={multiple}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
virtualListHeight={virtualListHeight}
/>
) : mode === 'tree' ? (
<Tree
@ -498,6 +512,8 @@ export class Transfer<
onlyChildren={!isTreeDeferLoad}
itemRender={optionItemRender}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
) : mode === 'chained' ? (
<ChainedSelection
@ -511,6 +527,9 @@ export class Transfer<
itemRender={optionItemRender}
multiple={multiple}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
virtualListHeight={virtualListHeight}
/>
) : (
<GroupedSelection
@ -524,6 +543,9 @@ export class Transfer<
itemRender={optionItemRender}
multiple={multiple}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
virtualListHeight={virtualListHeight}
/>
);
}
@ -547,7 +569,10 @@ export class Transfer<
optionItemRender,
multiple,
noResultsText,
labelField
labelField,
virtualThreshold,
itemHeight,
virtualListHeight
} = props;
return selectMode === 'table' ? (
@ -562,6 +587,9 @@ export class Transfer<
onDeferLoad={onDeferLoad}
cellRender={cellRender}
multiple={multiple}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
virtualListHeight={virtualListHeight}
/>
) : selectMode === 'tree' ? (
<Tree
@ -579,6 +607,8 @@ export class Transfer<
multiple={multiple}
cascade={true}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
) : selectMode === 'chained' ? (
<ChainedSelection
@ -592,6 +622,9 @@ export class Transfer<
itemRender={optionItemRender}
multiple={multiple}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
virtualListHeight={virtualListHeight}
/>
) : selectMode === 'associated' ? (
<AssociatedSelection
@ -610,6 +643,9 @@ export class Transfer<
itemRender={optionItemRender}
multiple={multiple}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
virtualListHeight={virtualListHeight}
/>
) : (
<GroupedSelection
@ -623,6 +659,9 @@ export class Transfer<
itemRender={optionItemRender}
multiple={multiple}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
virtualListHeight={virtualListHeight}
/>
);
}
@ -644,7 +683,9 @@ export class Transfer<
sortable,
labelField,
translate: __,
placeholder = __('Transfer.selectFromLeft')
placeholder = __('Transfer.selectFromLeft'),
virtualThreshold,
itemHeight
} = this.props;
const {resultSelectMode, isTreeDeferLoad} = this.state;
@ -667,6 +708,8 @@ export class Transfer<
placeholder={placeholder}
searchPlaceholder={resultSearchPlaceholder}
onSearch={onResultSearch}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
);
case 'tree':
@ -684,6 +727,8 @@ export class Transfer<
searchPlaceholder={resultSearchPlaceholder}
onSearch={onResultSearch}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
);
default:
@ -700,6 +745,8 @@ export class Transfer<
searchable={searchable}
onSearch={onResultSearch}
labelField={labelField}
virtualThreshold={virtualThreshold}
itemHeight={itemHeight}
/>
);
}

View File

@ -45,7 +45,9 @@ export class TransferDropDown extends Transfer<TransferDropDownProps> {
popOverContainer,
placeholder,
maxTagCount,
overflowTagPopover
overflowTagPopover,
itemHeight,
virtualThreshold
} = this.props;
const {inputValue, searchResult} = this.state;
const mobileUI = useMobileUI && isMobile();

View File

@ -1,6 +1,19 @@
/**
* @file Tree
* @description
*
*
* 1. 连带选中子节点 : autoChildren = true
* 1.1
* 1.1.1 cascade = false,
* 1.1.2 cascade = true, withChildren
* 1.2 state.value
* 1.2.1 cascade = false
* 1.2.2 onlyChildren = true
* 1.2.3 withChildren = true || cascade = true
*
* 2.
*
* @author fex
*/
@ -11,18 +24,17 @@ import {
autobind,
findTreeIndex,
hasAbility,
createObject,
getTreeParent,
getTreeAncestors,
cloneObject
getTreeAncestors
} from 'amis-core';
import {Option, Options, value2array} from './Select';
import {ClassNamesFn, themeable, ThemeProps, highlight} from 'amis-core';
import {themeable, ThemeProps, highlight} from 'amis-core';
import {Icon, getIcon} from './icons';
import Checkbox from './Checkbox';
import {LocaleProps, localeable} from 'amis-core';
import Spinner from './Spinner';
import {ItemRenderStates} from './Selection';
import VirtualList from './virtual-list';
interface IDropIndicator {
left: number;
@ -93,13 +105,16 @@ interface TreeSelectorProps extends ThemeProps, LocaleProps {
/*
* autoCheckChildren为true时生效false
* 1.casacde为falseui行为为级联选中子节点
* 1.cascade falseui行为为级联选中子节点
* 2.cascade为falsewithChildren为trueui行为为级联选中子节点
* 3.cascade为trueui行为级联选中子节点withChildren属性失效
* 4.cascade不论为true还是falseonlyChildren为trueui行为级联选中子节点
*/
cascade?: boolean;
/**
* 使 disable
*/
selfDisabledAffectChildren?: boolean;
minLength?: number;
maxLength?: number;
@ -109,6 +124,9 @@ interface TreeSelectorProps extends ThemeProps, LocaleProps {
rootCreateTip?: string;
creatable?: boolean;
createTip?: string;
// 是否开启虚拟滚动
virtualThreshold?: number;
itemHeight?: number;
onAdd?: (
idx?: number | Array<number>,
value?: any,
@ -135,6 +153,9 @@ interface TreeSelectorState {
isEditing: boolean;
editingItem: Option | null;
// 拍平的 Option list
flattenedOptions: Option[];
// 拖拽指示器
dropIndicator?: IDropIndicator;
}
@ -174,10 +195,15 @@ export class TreeSelector extends React.Component<
removeTip: 'Tree.removeNode',
enableNodePath: false,
pathSeparator: '/',
nodePath: []
nodePath: [],
virtualThreshold: 100,
itemHeight: 32
};
// 展开的节点
unfolded: WeakMap<Object, boolean> = new WeakMap();
// key: child option, value: parent option;
relations: WeakMap<Option, Option> = new WeakMap();
dragNode: Option | null;
dropInfo: IDropInfo | null;
startPoint: {
@ -204,7 +230,7 @@ export class TreeSelector extends React.Component<
},
props.enableNodePath
),
flattenedOptions: [],
inputValue: '',
addingParent: null,
isAdding: false,
@ -221,6 +247,8 @@ export class TreeSelector extends React.Component<
// onRef只有渲染器的情况才会使用
this.props.onRef?.(this);
// 初始化
this.flattenOptions();
enableNodePath && this.expandLazyLoadNodes();
}
@ -298,6 +326,7 @@ export class TreeSelector extends React.Component<
}
});
this.flattenOptions();
initFoldedLevel && this.forceUpdate();
return unfolded;
@ -314,12 +343,17 @@ export class TreeSelector extends React.Component<
}
unfolded.set(node, !unfolded.get(node));
this.flattenOptions();
this.forceUpdate();
}
isUnfolded(node: any) {
isUnfolded(node: any): boolean {
const unfolded = this.unfolded;
return unfolded.get(node);
const parent = this.relations.get(node);
if (parent) {
return !!unfolded.get(node) && this.isUnfolded(parent);
}
return !!unfolded.get(node);
}
@autobind
@ -419,6 +453,7 @@ export class TreeSelector extends React.Component<
@autobind
handleCheck(item: any, checked: boolean) {
// TODO: 重新梳理这里的逻辑
const props = this.props;
const value = this.state.value.concat();
const idx = value.indexOf(item);
@ -542,11 +577,28 @@ export class TreeSelector extends React.Component<
const idxes = findTreeIndex(options, item => item === parent) || [];
return onAdd && onAdd(idxes.concat(0));
} else {
this.setState({
isEditing: false,
isAdding: true,
addingParent: parent
});
this.setState(
{
isEditing: false,
isAdding: true,
addingParent: parent
},
() => {
if (!parent) {
return;
}
const result = [] as Option[];
for (let option of this.state.flattenedOptions) {
result.push(option);
if (option === parent) {
result.push({...option, isAdding: true});
}
}
this.setState({flattenedOptions: result});
}
);
}
}
@ -593,7 +645,6 @@ export class TreeSelector extends React.Component<
if (!value) {
return;
}
const {labelField, onAdd, options, onEdit} = this.props;
this.setState(
{
@ -710,7 +761,7 @@ export class TreeSelector extends React.Component<
@autobind
updateDropIndicator(e: React.DragEvent<Element>, node: Option) {
const gap = node?.children?.length ? 0 : 16;
// const gap = node?.children?.length ? 0 : 16;
this.dropInfo = this.getDropInfo(e, node);
let {dragNode, indicator} = this.dropInfo;
if (node === dragNode) {
@ -738,6 +789,7 @@ export class TreeSelector extends React.Component<
if (node?.children?.length) {
this.unfolded.set(node, false);
this.flattenOptions();
this.forceUpdate();
}
} else {
@ -776,31 +828,196 @@ export class TreeSelector extends React.Component<
};
}
/**
* TODO: this.unfolded => reaction
*/
flattenOptions(props?: TreeSelectorProps): void | Option[] {
let flattenedOptions: Option[] = [];
eachTree(
props?.options || this.props.options,
(item, key, level, paths: Option[]) => {
const parent = paths[paths.length - 2];
if (!isVisible(item)) {
return;
}
if (paths.length === 1) {
// 父节点
item.key = item.key || key;
flattenedOptions.push(item);
} else if (this.isUnfolded(parent)) {
this.relations.set(item, parent);
// 父节点是展开的状态
item.level = level;
item.key = item.key || `${parent.key}-${key}`;
flattenedOptions.push(item);
}
}
);
if (!this.state.flattenedOptions) {
// 初始化
this.state = {...this.state, flattenedOptions};
} else {
this.setState({
flattenedOptions: flattenedOptions
});
}
}
/**
*
* TODO: 递归可能需要优化
*/
isParentChecked(item?: Option): boolean {
if (!item || !this.relations.get(item)) {
return false;
}
const itemParent = this.relations.get(item);
const {value} = this.state;
const checked = !!~value.indexOf(itemParent);
return checked || this.isParentChecked(itemParent);
}
/**
*
*/
isItemChildrenChecked(item: Option) {
if (!item || !item.children) {
return true;
}
return !item.children.some(child => !this.isItemChecked(child));
}
/**
*
*/
isItemChildrenPartialChecked(item: Option, checked: boolean): boolean {
if (!item || !item.children || checked) {
return false;
}
let checkedLength = 0;
let partialChildrenLength = 0;
for (const child of item.children) {
if (this.isItemChecked(child)) {
checkedLength++;
} else if (this.isItemChildrenPartialChecked(child, false)) {
partialChildrenLength++;
}
}
return checkedLength !== 0 || partialChildrenLength !== 0;
}
/**
* checked
*/
isItemChecked(item?: Option): boolean {
if (!item) {
return false;
}
const {autoCheckChildren, onlyChildren, multiple, withChildren, cascade} =
this.props;
const {value} = this.state;
const checked = !!~value.indexOf(item);
if (checked) {
return true;
}
if (item.children?.length) {
if (
onlyChildren &&
autoCheckChildren &&
this.isItemChildrenChecked(item) // TODO: 优化这个逻辑
) {
// 当前元素没有在 value 中,但是子组件全部勾选了
return true;
}
}
const itemParent = this.relations.get(item);
if (itemParent && multiple && autoCheckChildren) {
// 当前节点为子节点
if (withChildren) {
return false;
}
if (cascade) {
return false;
}
return this.isParentChecked(item);
}
// 判断父组件是否勾选
return false;
}
/**
* item disabled
* props.disabled === true return;
*
*/
isItemDisabled(item: Option, checked: boolean) {
const {
disabledField,
disabled,
autoCheckChildren,
valueField,
multiple,
maxLength,
minLength,
cascade,
onlyChildren
} = this.props;
const {value} = this.state;
const selfDisabled = item[disabledField];
const nodeDisabled =
!!disabled ||
selfDisabled ||
(multiple && !autoCheckChildren && !item[valueField]);
if (nodeDisabled) {
return true;
}
if (
(maxLength && !checked && value.length >= maxLength) ||
(minLength && checked && value.length <= minLength)
) {
return true;
}
const itemParent = this.relations.get(item);
if (
autoCheckChildren &&
multiple &&
checked &&
itemParent &&
this.isItemChecked(itemParent)
) {
// 子节点
if (onlyChildren) {
return false;
}
return !cascade;
}
return false;
}
@autobind
renderList(
list: Options,
value: Option[],
uncheckable: boolean
): {dom: Array<JSX.Element | null>; childrenChecked: number} {
renderItem({index, style}: {index: number; style?: Record<string, any>}) {
const {
itemClassName,
showIcon,
showRadio,
multiple,
disabled,
labelField,
valueField,
iconField,
disabledField,
autoCheckChildren,
cascade,
selfDisabledAffectChildren,
onlyChildren,
classnames: cx,
highlightTxt,
options,
maxLength,
minLength,
creatable,
editable,
removable,
@ -811,262 +1028,217 @@ export class TreeSelector extends React.Component<
itemRender,
draggable
} = this.props;
const {
value: stateValue,
isAdding,
addingParent,
editingItem,
isEditing
} = this.state;
let childrenChecked = 0;
let ret = list.map((item, key) => {
if (!isVisible(item as any, options)) {
return null;
}
const checked = !!~value.indexOf(item);
const selfDisabled = item[disabledField];
let selfChecked = !!uncheckable || checked;
let childrenItems = null;
let selfChildrenChecked = false;
if (item.children && item.children.length) {
childrenItems = this.renderList(
item.children,
value,
!autoCheckChildren || cascade
? false
: uncheckable ||
(selfDisabledAffectChildren ? selfDisabled : false) ||
(multiple && checked)
);
selfChildrenChecked = !!childrenItems.childrenChecked;
if (
!selfChecked &&
onlyChildren &&
autoCheckChildren &&
item.children.length === childrenItems.childrenChecked
) {
selfChecked = true;
}
childrenItems = childrenItems.dom;
}
const item = this.state.flattenedOptions[index];
if ((onlyChildren ? selfChecked : selfChildrenChecked) || checked) {
childrenChecked++;
}
if (!item) {
return null;
}
let nodeDisabled =
!!uncheckable ||
!!disabled ||
selfDisabled ||
(multiple && !autoCheckChildren && !item[valueField]);
const {isAdding, editingItem, isEditing} = this.state;
if (
!nodeDisabled &&
((maxLength && !selfChecked && stateValue.length >= maxLength) ||
(minLength && selfChecked && stateValue.length <= minLength))
) {
nodeDisabled = true;
}
const checkbox: JSX.Element | null = multiple ? (
<Checkbox
size="sm"
disabled={nodeDisabled}
checked={selfChecked || (autoCheckChildren && selfChildrenChecked)}
partial={!selfChecked}
onChange={this.handleCheck.bind(this, item, !selfChecked)}
/>
) : showRadio ? (
<Checkbox
size="sm"
disabled={nodeDisabled}
checked={checked}
onChange={this.handleSelect.bind(this, item)}
/>
) : null;
const checked = this.isItemChecked(item);
const disabled = this.isItemDisabled(item, checked);
const partial = this.isItemChildrenPartialChecked(item, checked);
const checkedInValue = !!~this.state.value.indexOf(item);
const isLeaf =
(!item.children || !item.children.length) && !item.placeholder;
const checkbox: JSX.Element | null = multiple ? (
<Checkbox
size="sm"
disabled={disabled}
checked={checked || partial}
partial={partial}
onChange={this.handleCheck.bind(this, item, !checked)}
/>
) : showRadio ? (
<Checkbox
size="sm"
disabled={disabled}
checked={checked}
onChange={this.handleSelect.bind(this, item)}
/>
) : null;
const iconValue = item[iconField] || (childrenItems ? 'folder' : 'file');
const isLeaf =
(!item.children || !item.children.length) && !item.placeholder;
return (
<li
key={key}
className={cx(`Tree-item ${itemClassName || ''}`, {
'Tree-item--isLeaf': isLeaf
const iconValue = item[iconField] || (item.children ? 'folder' : 'file');
const level = item.level ? item.level - 1 : 0;
let body = null;
if (isEditing && editingItem === item) {
body = this.renderInput(checkbox);
} else if (item.isAdding) {
body = this.renderInput(checkbox);
} else {
body = (
<div
className={cx('Tree-itemLabel', {
'is-children-checked':
multiple &&
!cascade &&
this.isItemChildrenChecked(item) &&
!disabled,
'is-checked': checkedInValue,
'is-disabled': disabled
})}
draggable={draggable}
onDragStart={this.onDragStart(item)}
onDragOver={this.onDragOver(item)}
onDragEnd={this.onDragEnd(item)}
>
{isEditing && editingItem === item ? (
this.renderInput(checkbox)
) : (
{draggable && (
<a className={cx('Tree-itemDrager drag-bar')}>
<Icon icon="drag-bar" className="icon" />
</a>
)}
{item.loading ? (
<Spinner
size="sm"
show
icon="reload"
spinnerClassName={cx('Tree-spinner')}
/>
) : !isLeaf || (item.defer && !item.loaded) ? (
<div
className={cx('Tree-itemLabel', {
'is-children-checked':
multiple && !cascade && selfChildrenChecked && !nodeDisabled,
'is-checked': checked,
'is-disabled': nodeDisabled
onClick={() => this.toggleUnfolded(item)}
className={cx('Tree-itemArrow', {
'is-folded': !this.isUnfolded(item)
})}
draggable={draggable}
onDragStart={this.onDragStart(item)}
onDragOver={this.onDragOver(item)}
onDragEnd={this.onDragEnd(item)}
>
{draggable && (
<a className={cx('Tree-itemDrager drag-bar')}>
<Icon icon="drag-bar" className="icon" />
</a>
)}
<Icon icon="down-arrow-bold" className="icon" />
</div>
) : (
<span className={cx('Tree-itemArrowPlaceholder')} />
)}
{item.loading ? (
<Spinner
size="sm"
show
icon="reload"
spinnerClassName={cx('Tree-spinner')}
/>
) : !isLeaf || (item.defer && !item.loaded) ? (
<div
onClick={() => this.toggleUnfolded(item)}
className={cx('Tree-itemArrow', {
'is-folded': !this.isUnfolded(item)
})}
>
<Icon icon="down-arrow-bold" className="icon" />
</div>
) : (
<span className={cx('Tree-itemArrowPlaceholder')} />
)}
{checkbox}
{checkbox}
<div className={cx('Tree-itemLabel-item')}>
{showIcon ? (
<i
className={cx(
`Tree-itemIcon ${
item.children ? 'Tree-folderIcon' : 'Tree-leafIcon'
}`
)}
onClick={() =>
!disabled &&
(multiple
? this.handleCheck(item, !checked)
: this.handleSelect(item))
}
>
{getIcon(iconValue) ? (
<Icon icon={iconValue} className="icon" />
) : React.isValidElement(iconValue) ? (
iconValue
) : (
<i className={iconValue}></i>
)}
</i>
) : null}
<div className={cx(
'Tree-itemLabel-item'
)}>
{showIcon ? (
<i
className={cx(
`Tree-itemIcon ${
childrenItems ? 'Tree-folderIcon' : 'Tree-leafIcon'
}`
)}
onClick={() =>
!nodeDisabled &&
(multiple
? this.handleCheck(item, !selfChecked)
: this.handleSelect(item))
}
<span
className={cx('Tree-itemText')}
onClick={() =>
!disabled &&
(multiple
? this.handleCheck(item, !checked)
: this.handleSelect(item))
}
title={item[labelField]}
>
{highlightTxt
? highlight(`${item[labelField]}`, highlightTxt)
: itemRender
? itemRender(item, {
index: item.key,
multiple: multiple,
checked: checked,
onChange: () => this.handleCheck(item, !checked),
disabled: disabled || item.disabled
})
: `${item[labelField]}`}
</span>
{!disabled &&
!isAdding &&
!isEditing &&
!(item.defer && !item.loaded) ? (
<div className={cx('Tree-item-icons')}>
{creatable && hasAbility(item, 'creatable') ? (
<a
onClick={this.handleAdd.bind(this, item)}
data-tooltip={__(createTip)}
data-position="left"
>
{getIcon(iconValue) ? (
<Icon icon={iconValue} className="icon" />
) : React.isValidElement(iconValue) ? (
iconValue
) : (
<i className={iconValue}></i>
)}
</i>
<Icon icon="plus" className="icon" />
</a>
) : null}
<span
className={cx('Tree-itemText')}
onClick={() =>
!nodeDisabled &&
(multiple
? this.handleCheck(item, !selfChecked)
: this.handleSelect(item))
}
title={item[labelField]}
>
{highlightTxt
? highlight(`${item[labelField]}`, highlightTxt)
: itemRender
? itemRender(item, {
index: key,
multiple: multiple,
checked: checked,
onChange: () => this.handleCheck(item, !selfChecked),
disabled: disabled || item.disabled,
labelField
})
: `${item[labelField]}`}
</span>
{removable && hasAbility(item, 'removable') ? (
<a
onClick={this.handleRemove.bind(this, item)}
data-tooltip={__(removeTip)}
data-position="left"
>
<Icon icon="minus" className="icon" />
</a>
) : null}
{!nodeDisabled &&
!isAdding &&
!isEditing &&
!(item.defer && !item.loaded) ? (
<div className={cx('Tree-item-icons')}>
{creatable && hasAbility(item, 'creatable') ? (
<a
onClick={this.handleAdd.bind(this, item)}
data-tooltip={__(createTip)}
data-position="left"
>
<Icon icon="plus" className="icon" />
</a>
) : null}
{removable && hasAbility(item, 'removable') ? (
<a
onClick={this.handleRemove.bind(this, item)}
data-tooltip={__(removeTip)}
data-position="left"
>
<Icon icon="minus" className="icon" />
</a>
) : null}
{editable && hasAbility(item, 'editable') ? (
<a
onClick={this.handleEdit.bind(this, item)}
data-tooltip={__(editTip)}
data-position="left"
>
<Icon icon="new-edit" className="icon" />
</a>
) : null}
</div>
{editable && hasAbility(item, 'editable') ? (
<a
onClick={this.handleEdit.bind(this, item)}
data-tooltip={__(editTip)}
data-position="left"
>
<Icon icon="new-edit" className="icon" />
</a>
) : null}
</div>
</div>
)}
{/* 有children而且为展开状态 或者 添加child时 */}
{(childrenItems && this.isUnfolded(item)) ||
(isAdding && addingParent === item) ? (
<ul className={cx('Tree-sublist')}>
{isAdding && addingParent === item ? (
<li className={cx('Tree-item')}>
{this.renderInput(
checkbox
? React.cloneElement(checkbox, {
checked: false,
disabled: true
})
: null
)}
</li>
) : null}
{childrenItems}
</ul>
) : !childrenItems && item.placeholder && this.isUnfolded(item) ? (
<ul className={cx('Tree-sublist')}>
<li className={cx('Tree-item')}>
<div className={cx('Tree-placeholder')}>{item.placeholder}</div>
</li>
</ul>
) : null}
</li>
) : null}
</div>
</div>
);
});
}
return {
dom: ret,
childrenChecked
};
return (
<li
key={item.key}
className={cx(`Tree-item ${itemClassName || ''}`, {
'Tree-item--isLeaf': isLeaf
})}
style={{
...style,
left: `calc(${level} * var(--Tree-indent))`,
width: `calc(100% - ${level} * var(--Tree-indent))`
}}
>
{body}
</li>
);
}
@autobind
renderList(list: Options, value: any[]) {
const {virtualThreshold, itemHeight = 32} = this.props;
if (virtualThreshold && list.length > virtualThreshold) {
return (
<VirtualList
height={list.length > 8 ? 266 : list.length * itemHeight}
itemCount={list.length}
itemSize={itemHeight}
//! hack: 让 VirtualList 重新渲染
renderItem={this.renderItem.bind(this)}
/>
);
}
return list.map((item, index) => this.renderItem({index}));
}
render() {
@ -1085,14 +1257,13 @@ export class TreeSelector extends React.Component<
draggable,
translate: __
} = this.props;
let options = this.props.options;
const {
value,
isAdding,
addingParent,
isEditing,
inputValue,
dropIndicator
dropIndicator,
flattenedOptions
} = this.state;
let addBtn = null;
@ -1120,7 +1291,9 @@ export class TreeSelector extends React.Component<
})}
ref={this.root}
>
{(options && options.length) || addBtn || hideRoot === false ? (
{(flattenedOptions && flattenedOptions.length) ||
addBtn ||
hideRoot === false ? (
<ul className={cx('Tree-list')}>
{hideRoot ? (
<>
@ -1128,7 +1301,8 @@ export class TreeSelector extends React.Component<
{isAdding && !addingParent ? (
<li className={cx('Tree-item')}>{this.renderInput()}</li>
) : null}
{this.renderList(options, value, false).dom}
{this.renderList(flattenedOptions, value)}
</>
) : (
<li
@ -1170,7 +1344,7 @@ export class TreeSelector extends React.Component<
{isAdding && !addingParent ? (
<li className={cx('Tree-item')}>{this.renderInput()}</li>
) : null}
{this.renderList(options, value, false).dom}
{this.renderList(flattenedOptions, value)}
</ul>
</li>
)}

View File

@ -102,7 +102,7 @@ import Card from './Card';
import GridNav, {GridNavItem} from './GridNav';
import type {GridNavDirection} from './GridNav';
import Link from './Link';
import VirtualList from './virtual-list';
import VirtualList, {AutoSizer} from './virtual-list';
import {withStore} from './WithStore';
import PopOverContainer from './PopOverContainer';
import Pagination, {MODE_TYPE} from './Pagination';
@ -224,6 +224,7 @@ export {
GridNavItem,
Link,
VirtualList,
AutoSizer,
withStore,
PopOverContainer,
Pagination,

View File

@ -0,0 +1,75 @@
import * as React from 'react';
import {findDOMNode} from 'react-dom';
import {resizeSensor} from 'amis-core';
export type Size = {
height: number;
width: number;
};
export type AutoSizerProps = {
WrapperComponent?: React.ElementType;
minHeight?: number;
children: (props: Size) => React.ReactNode;
onResize?: (props: Size) => void;
};
export default class AutoSizer extends React.PureComponent<
AutoSizerProps,
Size
> {
unSensor: Function;
constructor(props: AutoSizerProps) {
super(props);
this.state = {width: 0, height: 0};
}
componentDidMount() {
const dom = findDOMNode(this) as HTMLElement;
this.unSensor = resizeSensor(dom, () => this.sizer(dom));
this.sizer(dom);
}
componentWillUnmount() {
this.unSensor && this.unSensor();
}
sizer(dom: HTMLElement) {
const width = dom.offsetWidth;
const height = dom.offsetHeight;
this.props?.onResize?.({width, height});
this.setState({
width,
height
});
}
render() {
const {children, WrapperComponent, minHeight} = this.props;
const {width, height} = this.state;
const WrapperCmpt = WrapperComponent ?? 'div';
return (
<WrapperCmpt
style={{
display: 'block',
position: 'relative',
width: '100%',
height: '100%',
...(minHeight ? {minHeight} : {})
}}
>
{children({
width,
height
})}
</WrapperCmpt>
);
}
}

View File

@ -62,6 +62,10 @@ export interface Props {
stickyIndices?: number[];
style?: React.CSSProperties;
width?: number | string;
WrapperComponent?: React.ElementType;
InnerComponent?: React.ElementType;
innerStyleFilter?: (styles: object) => object;
prefix?: React.ReactNode | null;
onItemsRendered?({startIndex, stopIndex}: RenderedRows): void;
onScroll?(offset: number, event: UIEvent): void;
renderItem(itemInfo: ItemInfo): React.ReactNode;
@ -259,6 +263,11 @@ export default class VirtualList extends React.PureComponent<Props, State> {
) {
this.scrollTo(offset);
}
if (props.itemCount !== itemCount) {
// 长度发生变化时重新渲染
this.forceUpdate();
}
}
componentWillUnmount() {
@ -312,6 +321,10 @@ export default class VirtualList extends React.PureComponent<Props, State> {
stickyIndices,
style,
width,
WrapperComponent,
InnerComponent,
prefix,
innerStyleFilter,
...props
} = this.props;
const {offset} = this.state;
@ -364,15 +377,23 @@ export default class VirtualList extends React.PureComponent<Props, State> {
}
}
const WrapperCmpt = WrapperComponent || 'div';
const InnerCmpt = InnerComponent || 'div';
return (
<div ref={this.getRef} {...props} style={wrapperStyle}>
<div style={innerStyle}>{items}</div>
</div>
<WrapperCmpt ref={this.getRef} {...props} style={wrapperStyle}>
{prefix ?? null}
<InnerCmpt
style={innerStyleFilter ? innerStyleFilter(innerStyle) : innerStyle}
>
{items}
</InnerCmpt>
</WrapperCmpt>
);
}
private getRef = (node: HTMLDivElement): void => {
this.rootNode = node;
private getRef = (node: HTMLElement | null): void => {
node && (this.rootNode = node);
};
private handleScroll = (event: UIEvent) => {
@ -445,3 +466,5 @@ export default class VirtualList extends React.PureComponent<Props, State> {
});
}
}
export {default as AutoSizer} from './AutoSizer';

View File

@ -57,3 +57,26 @@ export const createMockMediaMatcher =
return true;
}
});
export function formatStyleObject(style: string | null, px2number = true) {
if (!style) {
return {};
}
// 去除注释 /* xx */
style = style.replace(/\/\*[^(\*\/)]*\*\//g, '');
const res: any = {};
style.split(';').forEach((item: string) => {
if (!item || !String(item).includes(':')) return;
const [key, value] = item.split(':');
res[String(key).trim()] =
px2number && value.endsWith('px')
? Number(String(value).replace(/px$/, ''))
: String(value).trim();
});
return res;
}

View File

@ -174,296 +174,378 @@ exports[`Renderer:tabsTransfer 1`] = `
</div>
</div>
<div
class="cxd-TreeSelection cxd-Transfer-checkboxes"
class="cxd-Tree cxd-Transfer-checkboxes"
>
<div
class="cxd-TreeSelection-item is-expanded"
<ul
class="cxd-Tree-list"
>
<div
class="cxd-TreeSelection-itemInner"
>
<a
class="cxd-Table-expandBtn is-active"
>
<icon-mock
classname="icon icon-right-arrow-bold"
icon="right-arrow-bold"
/>
</a>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-TreeSelection-itemLabel"
>
<span
class=""
>
法师
</span>
</div>
</div>
<div
class="cxd-TreeSelection-sublist"
<li
class="cxd-Tree-item "
>
<div
class="cxd-TreeSelection-item"
class="cxd-Tree-itemLabel"
>
<div
class="cxd-TreeSelection-itemInner"
class="cxd-Tree-itemArrow"
>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
<icon-mock
classname="icon icon-down-arrow-bold"
icon="down-arrow-bold"
/>
</div>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="法师"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
>
法师
</span>
</span>
<div
class="cxd-TreeSelection-itemLabel"
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="诸葛亮"
>
<span
class=""
>
诸葛亮
</span>
</div>
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</div>
</div>
<div
class="cxd-TreeSelection-item"
>
<div
class="cxd-TreeSelection-itemInner"
>
<a
class="cxd-Table-expandBtn"
>
<icon-mock
classname="icon icon-right-arrow-bold"
icon="right-arrow-bold"
/>
</a>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-TreeSelection-itemLabel"
>
<span
class=""
>
战士
</span>
</div>
</div>
<div
class="cxd-TreeSelection-sublist"
</li>
<li
class="cxd-Tree-item "
>
<div
class="cxd-TreeSelection-item"
class="cxd-Tree-itemLabel"
>
<div
class="cxd-TreeSelection-itemInner"
class="cxd-Tree-itemArrow"
>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
<icon-mock
classname="icon icon-down-arrow-bold"
icon="down-arrow-bold"
/>
</div>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="战士"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
>
战士
</span>
</span>
<div
class="cxd-TreeSelection-itemLabel"
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="曹操"
>
<span
class=""
>
曹操
</span>
</div>
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-TreeSelection-item"
class="cxd-Tree-itemLabel"
>
<div
class="cxd-TreeSelection-itemInner"
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-TreeSelection-itemLabel"
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="钟无艳"
>
<span
class=""
>
钟无艳
</span>
</div>
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</div>
</div>
<div
class="cxd-TreeSelection-item"
>
<div
class="cxd-TreeSelection-itemInner"
>
<a
class="cxd-Table-expandBtn"
>
<icon-mock
classname="icon icon-right-arrow-bold"
icon="right-arrow-bold"
/>
</a>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-TreeSelection-itemLabel"
>
<span
class=""
>
打野
</span>
</div>
</div>
<div
class="cxd-TreeSelection-sublist"
</li>
<li
class="cxd-Tree-item "
>
<div
class="cxd-TreeSelection-item"
class="cxd-Tree-itemLabel"
>
<div
class="cxd-TreeSelection-itemInner"
class="cxd-Tree-itemArrow"
>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
<icon-mock
classname="icon icon-down-arrow-bold"
icon="down-arrow-bold"
/>
</div>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="打野"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
>
打野
</span>
</span>
<div
class="cxd-TreeSelection-itemLabel"
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="李白"
>
<span
class=""
>
李白
</span>
</div>
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-TreeSelection-item"
class="cxd-Tree-itemLabel"
>
<div
class="cxd-TreeSelection-itemInner"
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-TreeSelection-itemLabel"
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="韩信"
>
<span
class=""
>
韩信
</span>
</div>
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-TreeSelection-item"
class="cxd-Tree-itemLabel"
>
<div
class="cxd-TreeSelection-itemInner"
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
>
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-TreeSelection-itemLabel"
<input
type="checkbox"
/>
<i />
<span
class=""
/>
</label>
<div
class="cxd-Tree-itemLabel-item"
>
<span
class="cxd-Tree-itemText"
title="云中君"
>
<span
class=""
>
云中君
</span>
</div>
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
<div
@ -844,170 +926,166 @@ exports[`Renderer:tabsTransfer with deferApi 1`] = `
/>
</div>
</div>
<ul
class="cxd-Tree-sublist"
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<div
class="cxd-Tree-itemLabel"
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
<icon-mock
classname="icon icon-file"
icon="file"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Option A"
>
Option A
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
</i>
<span
class="cxd-Tree-itemText"
title="Option A"
>
Option A
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel is-checked"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<div
class="cxd-Tree-itemLabel is-checked"
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
<icon-mock
classname="icon icon-file"
icon="file"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Option B"
>
Option B
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
</i>
<span
class="cxd-Tree-itemText"
title="Option B"
>
Option B
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<div
class="cxd-Tree-itemLabel"
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
<icon-mock
classname="icon icon-file"
icon="file"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Option C"
>
Option C
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
</i>
<span
class="cxd-Tree-itemText"
title="Option C"
>
Option C
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<div
class="cxd-Tree-itemLabel"
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
<icon-mock
classname="icon icon-file"
icon="file"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Option D"
>
Option D
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
</i>
<span
class="cxd-Tree-itemText"
title="Option D"
>
Option D
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<div
class="cxd-Tree-itemLabel"
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
<icon-mock
classname="icon icon-file"
icon="file"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Option E"
>
Option E
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
</ul>
</i>
<span
class="cxd-Tree-itemText"
title="Option E"
>
Option E
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"

View File

@ -1,9 +1,52 @@
import React = require('react');
/**
* Select
*
* 1. menutpl
* 2.
* 3.
* 4. labelField valueField
* 5.
* 6.
* 7. searchable
* 8.
* 9.
* 10.
* 11.
* 12.
* 13.
*/
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {makeEnv, wait} from '../../helper';
const setup = async (items: any = {}, formOptions: any = {}) => {
const utils = render(
amisRender(
{
type: 'form',
api: '/api/mock2/form/saveForm',
body: items,
...formOptions
},
{},
makeEnv()
)
);
const select = utils.container.querySelector(
'.cxd-SelectControl .cxd-TransferDropDown'
);
expect(select).toBeInTheDocument();
return {
...utils,
select
};
};
test('Renderer:select menutpl', () => {
const {container} = render(
amisRender(
@ -622,3 +665,155 @@ test('Renderer:select virtual', async () => {
expect(container).toMatchSnapshot();
});
test('Renderer:select group mode with virtual', async () => {
const options = [...Array(20)].map((_, i) => ({
label: `group-${i + 1}`,
children: [...Array(10)].map((_, j) => ({
label: `option-${i * 10 + j + 1}`,
value: `value-${i * 10 + j + 1}`
}))
}));
const {container, select, getByText, queryByText} = await setup([
{
label: '分组',
type: 'select',
name: 'select',
selectMode: 'group',
options: options
}
]);
fireEvent.click(select!);
await wait(300);
expect(getByText('option-1')).toBeInTheDocument();
expect(await queryByText('option-200')).toBeNull();
expect(container).toMatchSnapshot();
});
test('Renderer:select table mode with virtual', async () => {
const options = [...Array(200)].map((_, i) => ({
label: `label-${i + 1}`,
value: i + 1
}));
const {container, getByText, queryByText, select} = await setup([
{
label: '表格',
type: 'select',
name: 'select',
options: options,
selectMode: 'table',
columns: [
{
name: 'label',
label: '名称'
},
{
name: 'value',
label: '值'
}
]
}
]);
fireEvent.click(select!);
await wait(300);
expect(getByText('label-1')).toBeInTheDocument();
expect(await queryByText('label-200')).toBeNull();
expect(container).toMatchSnapshot('');
});
test('Renderer:select chained mode with virtual', async () => {
const options = [...Array(101)].map((_, i) => ({
label: `group-${i + 1}`,
children: [...Array(101)].map((_, j) => ({
label: `group-${i + 1}-option-${j + 1}`,
value: `${i + 1}-${j + 1}`
}))
}));
const {container, getByText, queryByText, select} = await setup([
{
label: '级联',
type: 'select',
name: 'select',
options: options,
selectMode: 'chained'
}
]);
fireEvent.click(select!);
await wait(300);
fireEvent.click(getByText('group-1'));
await wait(300);
expect(getByText('group-1')).toBeInTheDocument();
expect(await queryByText('group-100')).toBeNull();
expect(getByText('group-1-option-1')).toBeInTheDocument();
expect(await queryByText('group-1-option-100')).toBeNull();
expect(container).toMatchSnapshot('');
});
test('Renderer:select associated mode with virtual', async () => {
const leftOptions = [...Array(10)].map((_, i) => ({
label: `group-${i + 1}`,
children: [...Array(101)].map((_, j) => ({
label: `group-${i + 1}-option-${j + 1}`,
value: `${i + 1}-${j + 1}`
}))
}));
const options = [...Array(101)].map((_, i) => ({
label: `label-${i + 1}`,
value: `value-${i + 1}`
}));
const {container, getByText, queryByText, select} = await setup([
{
label: '级联',
type: 'select',
name: 'select',
selectMode: 'associated',
leftMode: 'list',
rightMode: 'table',
columns: [
{
name: 'label',
label: '名称'
},
{
name: 'value',
label: '值得'
}
],
leftOptions: leftOptions,
options: [
{
ref: '1-1',
children: options
}
]
}
]);
fireEvent.click(select!);
await wait(300);
fireEvent.click(getByText('group-1-option-1'));
await wait(300);
expect(getByText('group-1-option-1')).toBeInTheDocument();
expect(await queryByText('group-10-option-1')).toBeNull();
expect(getByText('label-1')).toBeInTheDocument();
expect(await queryByText('label-100')).toBeNull();
expect(container).toMatchSnapshot('');
});

View File

@ -1,8 +1,39 @@
import React = require('react');
import {render} from '@testing-library/react';
/**
* Transfer 穿
*
* 1.
* 2.
* 3.
* 4.
* 5.
* 6.
* 7.
* 8.
* 9.
* 10.
*/
import {fireEvent, render, waitFor} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {makeEnv} from '../../helper';
import {makeEnv, formatStyleObject, wait} from '../../helper';
const setup = async (items: any = {}, formOptions: any = {}) => {
const utils = render(
amisRender(
{
type: 'form',
api: '/api/mock2/form/saveForm',
body: items,
...formOptions
},
{},
makeEnv()
)
);
return utils;
};
test('Renderer:transfer', () => {
const {container} = render(
@ -453,3 +484,260 @@ test('Renderer:transfer left tree', async () => {
expect(container).toMatchSnapshot();
});
test('Renderer:transfer group mode with virtual', async () => {
const options = [...Array(20)].map((_, i) => ({
label: `group-${i + 1}`,
children: [...Array(10)].map((_, j) => ({
label: `option-${i * 10 + j + 1}`,
value: `value-${i * 10 + j + 1}`
}))
}));
const {container, getByText, queryByText} = await setup([
{
label: '分组',
type: 'transfer',
name: 'transfer',
options: options,
virtualThreshold: 100,
itemHeight: 40
}
]);
const optionsOrLabel = container.querySelectorAll(
'.cxd-GroupedSelection.cxd-Transfer-selection > div > div > div > div.cxd-GroupedSelection-group'
);
expect(optionsOrLabel.length > 0).toBeTruthy();
const firstStyle = formatStyleObject(optionsOrLabel[0].getAttribute('style'));
expect(firstStyle.top).toBe(0);
expect(firstStyle.height).toBe(40);
await waitFor(() => expect(optionsOrLabel.length > 1).toBeTruthy());
expect(formatStyleObject(optionsOrLabel[1].getAttribute('style')).top).toBe(
40
);
expect(getByText('option-1')).toBeInTheDocument();
expect(await queryByText('option-100')).toBeNull();
const scrollContainer = container.querySelector(
'.cxd-GroupedSelection.cxd-Transfer-selection > div > div'
)!;
// 滚动到底部
fireEvent.scroll(scrollContainer, {
target: {
scrollTop: 9999
}
});
await wait(300);
// 最后一项
const lastOne = container.querySelector(
'.cxd-GroupedSelection.cxd-Transfer-selection > div > div > div > div.cxd-GroupedSelection-group:last-of-type'
);
expect(formatStyleObject(lastOne!.getAttribute('style')).top).toBe(8760);
expect(await queryByText('option-1')).toBeNull();
expect(container).toMatchSnapshot();
});
test('Renderer:transfer table mode with virtual', async () => {
const options = [...Array(200)].map((_, i) => ({
label: `label-${i + 1}`,
value: i + 1
}));
const value = [...Array(10)].map((_, i) => i * 2 + 1).join(',');
const {container, getByText} = await setup([
{
label: '分组',
type: 'transfer',
name: 'transfer',
options: options,
value: value,
virtualThreshold: 10,
selectMode: 'table',
columns: [
{
name: 'label',
label: '名称'
},
{
name: 'value',
label: '值'
}
]
}
]);
const virtualTable = container.querySelector(
'.cxd-Transfer-select .cxd-Table-content.is-virtual .cxd-Table-content-virtual table.cxd-Table-table'
)!;
expect(virtualTable).toBeInTheDocument();
const resultList = container.querySelector(
'.cxd-Transfer-result .cxd-Selections.cxd-Transfer-value .cxd-Selections-items'
)!;
expect(resultList.firstChild).toHaveClass('cxd-Selections-item');
expect(container).toMatchSnapshot('result not virtual');
// 点击后value 变为 11 项,右侧列表变成虚拟列表
fireEvent.click(getByText('label-2'));
await wait(300);
const firstVirtualItem = resultList.querySelector(
'div > div:first-of-type > div > .cxd-Selections-item'
);
expect(firstVirtualItem).toBeInTheDocument();
expect(formatStyleObject(firstVirtualItem!.getAttribute('style'))!.top).toBe(
0
);
expect(container).toMatchSnapshot('result virtual');
});
test('Renderer:transfer chained mode with virtual', async () => {
const options = [...Array(10)].map((_, i) => ({
label: `group-${i + 1}`,
children: [...Array(200)].map((_, j) => ({
label: `group-${i + 1}-option-${j + 1}`,
value: `${i + 1}-${j + 1}`
}))
}));
const {container, getByText, queryByText} = await setup([
{
label: '分组',
type: 'transfer',
name: 'transfer',
options: options,
virtualThreshold: 100,
selectMode: 'chained'
}
]);
const cols = container.querySelectorAll('.cxd-ChainedSelection-col');
expect(cols.length).toBe(2);
expect(cols[0].children.length).toBe(10);
expect(cols[1].firstChild).toHaveClass('cxd-ChainedSelection-placeholder');
fireEvent.click(getByText('group-2'));
await wait(300);
expect(cols[1].firstChild).not.toHaveClass(
'cxd-ChainedSelection-placeholder'
);
expect(getByText('group-2-option-1')).toBeInTheDocument();
expect(await queryByText('group-2-option-200')).toBeNull();
expect(container).toMatchSnapshot();
});
test('Renderer:transfer associated mode with virtual', async () => {
const options = [...Array(200)].map((_, i) => ({
label: `label-${i + 1}`,
value: i + 1
}));
const {container, getByText, queryByText} = await setup([
{
label: '关联选择模式',
type: 'transfer',
name: 'b',
sortable: true,
searchable: true,
selectMode: 'associated',
leftMode: 'tree',
leftOptions: [
{
label: '法师',
children: [
{
label: '诸葛亮',
value: 'zhugeliang'
}
]
},
{
label: '战士',
children: [
{
label: '曹操',
value: 'caocao'
},
{
label: '钟无艳',
value: 'zhongwuyan'
}
]
}
],
options: [
{
ref: 'zhugeliang',
children: [
{
label: 'A',
value: 'a'
}
]
},
{
ref: 'caocao',
children: [
{
label: 'B',
value: 'b'
},
{
label: 'C',
value: 'c'
}
]
},
{
ref: 'zhongwuyan',
children: options
}
]
}
]);
const rightCol = container.querySelector('.cxd-AssociatedSelection-right');
expect(rightCol).toBeInTheDocument();
fireEvent.click(getByText('诸葛亮'));
await wait(200);
expect(
rightCol!.querySelector(
'.cxd-GroupedSelection .cxd-GroupedSelection-item .cxd-GroupedSelection-itemLabel span'
)!.innerHTML
).toBe('A');
fireEvent.click(getByText('钟无艳'));
await wait(200);
expect(
rightCol!.querySelectorAll('.cxd-GroupedSelection-item').length < 200
).toBeTruthy();
expect(getByText('label-1')).toBeInTheDocument();
expect(await queryByText('label-100')).toBeNull();
expect(container).toMatchSnapshot();
});

View File

@ -0,0 +1,248 @@
import {
act,
cleanup,
fireEvent,
render,
waitFor,
waitForElementToBeRemoved
} from '@testing-library/react';
import {clearStoresCache, render as amisRender} from '../../src';
import {makeEnv, wait} from '../helper';
afterEach(() => {
cleanup();
clearStoresCache();
jest.useRealTimers();
});
test('Tree: basic & disabled children & default check children', () => {
const {container, getByText} = render(
amisRender(
{
type: 'page',
body: {
type: 'form',
api: '/api/mock2/form/saveForm',
body: [
{
type: 'input-tree',
name: 'tree',
label: 'Tree',
options: [
{
label: 'Folder A',
value: 1,
children: [
{
label: 'file A',
value: 2
},
{
label: 'Folder B',
value: 3,
children: [
{
label: 'file b1',
value: 3.1
},
{
label: 'file b2',
value: 3.2
}
]
}
]
},
{
label: 'file C',
value: 4
},
{
label: 'file D',
value: 5
}
]
}
]
}
},
{},
makeEnv({})
)
);
expect(container).toMatchSnapshot();
});
test('Tree: showOutline', () => {
const {container, getByText} = render(
amisRender(
{
type: 'page',
body: {
type: 'form',
api: '/api/mock2/form/saveForm',
body: [
{
type: 'input-tree',
name: 'tree',
label: 'Tree',
showOutline: true,
options: [
{
label: 'Folder A',
value: 1,
children: [
{
label: 'file A',
value: 2
},
{
label: 'Folder B',
value: 3,
children: [
{
label: 'file b1',
value: 3.1
},
{
label: 'file b2',
value: 3.2
}
]
}
]
},
{
label: 'file C',
value: 4
},
{
label: 'file D',
value: 5
}
]
}
]
}
},
{},
makeEnv({})
)
);
expect(container).toMatchSnapshot();
});
test('Tree: autoCheckChildren = false', async () => {
const {container, getByText} = render(
amisRender(
{
type: 'page',
body: {
type: 'form',
api: '/api/mock2/form/saveForm',
body: [
{
type: 'input-tree',
name: 'tree',
label: 'Tree',
autoCheckChildren: false,
multiple: true,
options: [
{
label: 'Folder A',
value: 1,
children: [
{
label: 'file A',
value: 2
},
{
label: 'Folder B',
value: 3,
children: [
{
label: 'file b1',
value: 3.1
},
{
label: 'file b2',
value: 3.2
}
]
}
]
},
{
label: 'file C',
value: 4
},
{
label: 'file D',
value: 5
}
]
}
]
}
},
{},
makeEnv({})
)
);
fireEvent.click(getByText('Folder B'));
await waitFor(() => container.querySelector('.is-checked'));
expect(container.querySelectorAll('.is-checked').length).toBe(1);
});
test('Tree: cascade = true', () => {
const {container, getByText} = render(
amisRender(
{
type: 'input-tree',
name: 'tree2',
label: '子节点可以反选,值包含父子节点值',
multiple: true,
cascade: true,
options: [
{
label: 'A',
value: 'a'
},
{
label: 'B',
value: 'b',
children: [
{
label: 'B-1',
value: 'b-1'
},
{
label: 'B-2',
value: 'b-2'
},
{
label: 'B-3',
value: 'b-3'
}
]
},
{
label: 'C',
value: 'c'
}
]
},
{},
makeEnv({})
)
);
fireEvent.click(getByText('B'));
expect(container.querySelectorAll('.is-checked').length).toBe(4);
});

View File

@ -0,0 +1,747 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tree: basic & disabled children & default check children 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
>
<div
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
style="position: relative;"
>
<div
class="cxd-Panel-heading"
>
<h3
class="cxd-Panel-title"
>
<span
class="cxd-TplField"
>
<span>
表单
</span>
</span>
</h3>
</div>
<div
class="cxd-Panel-body"
>
<form
class="cxd-Form cxd-Form--normal"
novalidate=""
>
<input
style="display: none;"
type="submit"
/>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
Tree
</span>
</span>
</span>
</label>
<div
class="cxd-TreeControl cxd-Form-control"
>
<div
class="cxd-Tree "
>
<ul
class="cxd-Tree-list"
>
<li
class="cxd-Tree-item "
>
<div
class="cxd-Tree-itemLabel"
>
<div
class="cxd-Tree-itemArrow"
>
<icon-mock
classname="icon icon-down-arrow-bold"
icon="down-arrow-bold"
/>
</div>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-folderIcon"
>
<icon-mock
classname="icon icon-folder"
icon="folder"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Folder A"
>
Folder A
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file A"
>
file A
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item "
>
<div
class="cxd-Tree-itemLabel"
>
<div
class="cxd-Tree-itemArrow"
>
<icon-mock
classname="icon icon-down-arrow-bold"
icon="down-arrow-bold"
/>
</div>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-folderIcon"
>
<icon-mock
classname="icon icon-folder"
icon="folder"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Folder B"
>
Folder B
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file b1"
>
file b1
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file b2"
>
file b2
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file C"
>
file C
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file D"
>
file D
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</form>
</div>
<div
class="cxd-Panel-footerWrap"
>
<div
class="cxd-Panel-btnToolbar cxd-Panel-footer"
>
<button
class="cxd-Button cxd-Button--primary cxd-Button--size-default"
type="submit"
>
<span>
提交
</span>
</button>
</div>
</div>
<div
class="resize-sensor"
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
class="resize-sensor-expand"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
/>
</div>
<div
class="resize-sensor-shrink"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
/>
</div>
<div
class="resize-sensor-appear"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Tree: showOutline 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
>
<div
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
style="position: relative;"
>
<div
class="cxd-Panel-heading"
>
<h3
class="cxd-Panel-title"
>
<span
class="cxd-TplField"
>
<span>
表单
</span>
</span>
</h3>
</div>
<div
class="cxd-Panel-body"
>
<form
class="cxd-Form cxd-Form--normal"
novalidate=""
>
<input
style="display: none;"
type="submit"
/>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
Tree
</span>
</span>
</span>
</label>
<div
class="cxd-TreeControl cxd-Form-control"
>
<div
class="cxd-Tree cxd-Tree--outline"
>
<ul
class="cxd-Tree-list"
>
<li
class="cxd-Tree-item "
>
<div
class="cxd-Tree-itemLabel"
>
<div
class="cxd-Tree-itemArrow"
>
<icon-mock
classname="icon icon-down-arrow-bold"
icon="down-arrow-bold"
/>
</div>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-folderIcon"
>
<icon-mock
classname="icon icon-folder"
icon="folder"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Folder A"
>
Folder A
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file A"
>
file A
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item "
>
<div
class="cxd-Tree-itemLabel"
>
<div
class="cxd-Tree-itemArrow"
>
<icon-mock
classname="icon icon-down-arrow-bold"
icon="down-arrow-bold"
/>
</div>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-folderIcon"
>
<icon-mock
classname="icon icon-folder"
icon="folder"
/>
</i>
<span
class="cxd-Tree-itemText"
title="Folder B"
>
Folder B
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file b1"
>
file b1
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file b2"
>
file b2
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file C"
>
file C
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
<li
class="cxd-Tree-item cxd-Tree-item--isLeaf"
>
<div
class="cxd-Tree-itemLabel"
>
<span
class="cxd-Tree-itemArrowPlaceholder"
/>
<div
class="cxd-Tree-itemLabel-item"
>
<i
class="cxd-Tree-itemIcon cxd-Tree-leafIcon"
>
<icon-mock
classname="icon icon-file"
icon="file"
/>
</i>
<span
class="cxd-Tree-itemText"
title="file D"
>
file D
</span>
<div
class="cxd-Tree-item-icons"
/>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</form>
</div>
<div
class="cxd-Panel-footerWrap"
>
<div
class="cxd-Panel-btnToolbar cxd-Panel-footer"
>
<button
class="cxd-Button cxd-Button--primary cxd-Button--size-default"
type="submit"
>
<span>
提交
</span>
</button>
</div>
</div>
<div
class="resize-sensor"
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
class="resize-sensor-expand"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
/>
</div>
<div
class="resize-sensor-shrink"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
/>
</div>
<div
class="resize-sensor-appear"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -9,7 +9,8 @@ import {
ActionObject,
isPureVariable,
resolveVariableAndFilter,
resolveEventData
resolveEventData,
toNumber
} from 'amis-core';
import {Spinner} from 'amis-ui';
import {FormOptionsSchema, SchemaApi} from '../../Schema';
@ -108,6 +109,7 @@ export interface TreeProps
| 'className'
| 'inputClassName'
| 'descriptionClassName'
| 'deferApi'
> {
enableNodePath?: boolean;
pathSeparator?: string;
@ -229,7 +231,9 @@ export default class TreeControl extends React.Component<TreeProps> {
deferLoad,
expandTreeOptions,
translate: __,
data
data,
virtualThreshold,
itemHeight
} = this.props;
let {highlightTxt} = this.props;
@ -291,6 +295,10 @@ export default class TreeControl extends React.Component<TreeProps> {
bultinCUD={!addControls && !editControls}
onDeferLoad={deferLoad}
onExpandTree={expandTreeOptions}
virtualThreshold={virtualThreshold}
itemHeight={
toNumber(itemHeight) > 0 ? toNumber(itemHeight) : undefined
}
/>
)}
</div>

View File

@ -544,7 +544,10 @@ class TransferDropdownRenderer extends BaseTransferRenderer<TransferDropDownProp
popOverContainer,
maxTagCount,
overflowTagPopover,
placeholder
placeholder,
itemHeight,
virtualThreshold,
rightMode
} = this.props;
// 目前 LeftOptions 没有接口可以动态加载
@ -584,6 +587,7 @@ class TransferDropdownRenderer extends BaseTransferRenderer<TransferDropDownProp
multiple={multiple}
columns={columns}
leftMode={leftMode}
rightMode={rightMode}
leftOptions={leftOptions}
borderMode={borderMode}
useMobileUI={useMobileUI}
@ -591,6 +595,9 @@ class TransferDropdownRenderer extends BaseTransferRenderer<TransferDropDownProp
maxTagCount={maxTagCount}
overflowTagPopover={overflowTagPopover}
placeholder={placeholder}
itemHeight={itemHeight}
virtualThreshold={virtualThreshold}
virtualListHeight={266}
/>
<Spinner overlay key="info" show={loading} />

View File

@ -15,7 +15,7 @@ import {
spliceTree
} from 'amis-core';
import {Selection as BaseSelection} from 'amis-ui';
import {ActionObject} from 'amis-core';
import {ActionObject, toNumber} from 'amis-core';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import {supportStatic} from './StaticHoc';
@ -292,7 +292,9 @@ export class TabsTransferRenderer extends BaseTabsTransferRenderer<TabsTransferP
leftDeferLoad,
disabled,
selectTitle,
resultTitle
resultTitle,
itemHeight,
virtualThreshold
} = this.props;
return (
@ -315,6 +317,10 @@ export class TabsTransferRenderer extends BaseTabsTransferRenderer<TabsTransferP
optionItemRender={this.optionItemRender}
resultItemRender={this.resultItemRender}
onTabChange={this.onTabChange}
itemHeight={
toNumber(itemHeight) > 0 ? toNumber(itemHeight) : undefined
}
virtualThreshold={virtualThreshold}
/>
<Spinner overlay key="info" show={loading} />

View File

@ -6,7 +6,7 @@ import {TabsTransferPicker} from 'amis-ui';
import {TabsTransferControlSchema} from './TabsTransfer';
import {autobind, createObject} from 'amis-core';
import {Selection as BaseSelection} from 'amis-ui';
import {ActionObject} from 'amis-core';
import {ActionObject, toNumber} from 'amis-core';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import {supportStatic} from './StaticHoc';
@ -98,7 +98,9 @@ export class TabsTransferPickerRenderer extends BaseTabsTransferRenderer<TabsTra
resultTitle,
pickerSize,
leftMode,
leftOptions
leftOptions,
itemHeight,
virtualThreshold
} = this.props;
return (
@ -125,6 +127,10 @@ export class TabsTransferPickerRenderer extends BaseTabsTransferRenderer<TabsTra
resultItemRender={this.resultItemRender}
onFocus={() => this.dispatchEvent('focus')}
onBlur={() => this.dispatchEvent('blur')}
itemHeight={
toNumber(itemHeight) > 0 ? toNumber(itemHeight) : undefined
}
virtualThreshold={virtualThreshold}
/>
<Spinner overlay key="info" show={loading} />

View File

@ -25,7 +25,7 @@ import {resolveVariable} from 'amis-core';
import {FormOptionsSchema, SchemaApi, SchemaObject} from '../../Schema';
import {Selection as BaseSelection} from 'amis-ui';
import {ResultList} from 'amis-ui';
import {ActionObject} from 'amis-core';
import {ActionObject, toNumber} from 'amis-core';
import type {ItemRenderStates} from 'amis-ui/lib/components/Selection';
import {supportStatic} from './StaticHoc';
@ -135,6 +135,16 @@ export interface TransferControlSchema extends FormOptionsSchema {
*
*/
statistics?: boolean;
/**
*
*/
itemHeight?: number;
/**
*
*/
virtualThreshold?: number;
}
export interface BaseTransferProps
@ -148,6 +158,8 @@ export interface BaseTransferProps
| 'inputClassName'
> {
resultItemRender?: (option: Option) => JSX.Element;
virtualThreshold?: number;
itemHeight?: number;
}
export class BaseTransferRenderer<
@ -445,7 +457,9 @@ export class BaseTransferRenderer<
resultSearchPlaceholder,
resultSearchable = false,
statistics,
labelField
labelField,
virtualThreshold,
itemHeight
} = this.props;
// 目前 LeftOptions 没有接口可以动态加载
@ -498,6 +512,10 @@ export class BaseTransferRenderer<
resultItemRender={this.resultItemRender}
onSelectAll={this.onSelectAll}
onRef={this.getRef}
virtualThreshold={virtualThreshold}
itemHeight={
toNumber(itemHeight) > 0 ? toNumber(itemHeight) : undefined
}
/>
<Spinner overlay key="info" show={loading} />

View File

@ -4,7 +4,7 @@ import {Spinner} from 'amis-ui';
import {BaseTransferRenderer, TransferControlSchema} from './Transfer';
import {TransferPicker} from 'amis-ui';
import {autobind, createObject} from 'amis-core';
import {ActionObject} from 'amis-core';
import {ActionObject, toNumber} from 'amis-core';
import {supportStatic} from './StaticHoc';
/**
@ -78,7 +78,9 @@ export class TransferPickerRenderer extends BaseTransferRenderer<TabsTransferPro
columns,
leftMode,
selectMode,
borderMode
borderMode,
itemHeight,
virtualThreshold
} = this.props;
// 目前 LeftOptions 没有接口可以动态加载
@ -122,6 +124,10 @@ export class TransferPickerRenderer extends BaseTransferRenderer<TabsTransferPro
resultItemRender={this.resultItemRender}
onFocus={() => this.dispatchEvent('focus')}
onBlur={() => this.dispatchEvent('blur')}
itemHeight={
toNumber(itemHeight) > 0 ? toNumber(itemHeight) : undefined
}
virtualThreshold={virtualThreshold}
/>
<Spinner overlay key="info" show={loading} />

View File

@ -7,7 +7,8 @@ import {
OptionsControl,
OptionsControlProps,
Option,
FormOptionsControl
FormOptionsControl,
toNumber
} from 'amis-core';
import {Tree as TreeSelector} from 'amis-ui';
@ -538,7 +539,9 @@ export default class TreeSelectControl extends React.Component<
selfDisabledAffectChildren,
showOutline,
autoCheckChildren,
hideRoot
hideRoot,
virtualThreshold,
itemHeight
} = this.props;
let filtedOptions =
@ -596,6 +599,8 @@ export default class TreeSelectControl extends React.Component<
onDeferLoad={deferLoad}
onExpandTree={expandTreeOptions}
selfDisabledAffectChildren={selfDisabledAffectChildren}
virtualThreshold={virtualThreshold}
itemHeight={toNumber(itemHeight) > 0 ? toNumber(itemHeight) : undefined}
/>
);
}