mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-01 19:38:16 +08:00
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:
parent
cb5452d429
commit
5520004dec
@ -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` | 在选项数量超过多少时开启虚拟渲染 |
|
||||
|
||||
## 事件表
|
||||
|
||||
|
@ -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` | 在选项数量超过多少时开启虚拟渲染 |
|
||||
|
||||
## 事件表
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -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()}
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -385,6 +385,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}Tree.#{$ns}Transfer-checkboxes {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-search {
|
||||
margin: var(--gap-sm) var(--gap-sm);
|
||||
.#{$ns}InputBox {
|
||||
|
@ -63,7 +63,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--outline &-sublist &-item--isLeaf {
|
||||
&--outline &-item--isLeaf {
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
|
@ -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,
|
||||
|
@ -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')}>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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为false,ui行为为级联选中子节点,子节点禁用;值只包含父节点的值
|
||||
* 1.cascade 为false,ui行为为级联选中子节点,子节点禁用;值只包含父节点的值
|
||||
* 2.cascade为false,withChildren为true,ui行为为级联选中子节点,子节点禁用;值包含父子节点的值
|
||||
* 3.cascade为true,ui行为级联选中子节点,子节点可反选,值包含父子节点的值,此时withChildren属性失效
|
||||
* 4.cascade不论为true还是false,onlyChildren为true,ui行为级联选中子节点,子节点可反选,值只包含子节点的值
|
||||
*/
|
||||
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>
|
||||
)}
|
||||
|
@ -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,
|
||||
|
75
packages/amis-ui/src/components/virtual-list/AutoSizer.tsx
Normal file
75
packages/amis-ui/src/components/virtual-list/AutoSizer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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('');
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
248
packages/amis/__tests__/renderers/Tree.test.tsx
Normal file
248
packages/amis/__tests__/renderers/Tree.test.tsx
Normal 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);
|
||||
});
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
|
@ -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} />
|
||||
|
@ -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} />
|
||||
|
@ -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} />
|
||||
|
@ -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} />
|
||||
|
@ -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} />
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user