feat: inputTree/treeSelect事件&动作扩充

This commit is contained in:
zhaojianhui 2022-03-17 20:25:51 +08:00
parent 62611cc6ea
commit 94da4a253e
10 changed files with 584 additions and 21 deletions

View File

@ -835,3 +835,25 @@ true false true [{label: 'A/B/C', value: 'a/b/c'},{label: 'A
| treeContainerClassName | `string` | | tree 最外层容器类名 |
| enableNodePath | `boolean` | `false` | 是否开启节点路径模式 |
| pathSeparator | `string` | `/` | 节点路径的分隔符,`enableNodePath`为`true`时生效 |
## 事件表
| 事件名称 | 事件参数 | 说明 |
|--------------- |------------------------ |----------------------|
| change | value: `string` 更新后的数据 | 选中值更改 |
| add | value: `string` 新增节点信息 | 新增选项 |
| edit | value: `string` 编辑节点信息 | 编辑选项 |
| delete | value: `string` 删除节点信息 | 删除选项 |
| loadFinished | value: `json` 懒加载返回的数据 | 懒加载完成触发 |
## 动作表
| 动作名称 | 动作配置 | 说明 |
|----------------|-------------------------------------------------- |---------------------|
| expand | openLevel: `number`<br />initiallyOpen为false时生效| 配置展开层级 |
| collapse | - | 关闭树|
| clear | - | 清除数据 |
| reset | - | 重置数据 |
| choose | value: `string` | 更新选中值 |

View File

@ -189,3 +189,25 @@ order: 60
| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | --------- | ------- | ------------------------------------------- |
| hideNodePathLabel | `boolean` | `false` | 是否隐藏选择框中已选择节点的路径 label 信息 |
## 事件表
| 事件名称 | 事件参数 | 说明 |
|--------------- |------------------------ |----------------------|
| change | value: `string` 更新后的数据 | 选中值更改 |
| add | value: `string` 新增节点信息 | 新增选项 |
| edit | value: `string` 编辑节点信息 | 编辑选项 |
| delete | value: `string` 删除节点信息 | 删除选项 |
| loadFinished | value: `json` 懒加载返回的数据 | 懒加载完成触发 |
| blur | - | 输入框失去焦点|
| focus | - | 输入框获取焦点 |
## 动作表
| 动作名称 | 动作配置 | 说明 |
|----------------|------------------------ |---------------------|
| clear | - | 清除数据 |
| reset | - | 重置数据 |
| choose | value: `string` | 更新选中值 |

View File

@ -0,0 +1,203 @@
export default {
type: 'page',
title: '树形选择框',
regions: ['body', 'toolbar', 'header'],
body: [
{
type: 'form',
debug: true,
body: [
{
name: "input-tree-clear",
type: "action",
label: 'clear触发器',
level: 'primary',
className: 'mr-3',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'input-tree-action',
description: '点击清空内容'
}
]
}
}
},
{
name: "input-tree-reset",
type: "action",
label: 'reset触发器',
level: 'primary',
className: 'mr-3',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'input-tree-action',
description: '点击清空内容'
}
]
}
}
},
{
name: "input-tree-expand",
type: "action",
label: 'expand触发器openLevel: 1',
level: 'primary',
className: 'mr-3',
onEvent: {
click: {
actions: [
{
actionType: 'expand',
componentId: 'input-tree-action',
description: '点击展开',
args: {
openLevel: 1
},
}
]
}
}
},
{
name: "input-tree-collapse",
type: "action",
label: 'collapse触发器',
level: 'primary',
className: 'mr-3',
onEvent: {
click: {
actions: [
{
actionType: 'collapse',
componentId: 'input-tree-action',
description: '点击收起'
}
]
}
}
},
{
name: "input-tree-choose",
type: "action",
label: 'choose触发器Folder A',
level: 'primary',
className: 'mr-3',
onEvent: {
click: {
actions: [
{
actionType: 'choose',
componentId: 'input-tree-action',
description: '点击选中特定值',
args: {
value: 1
},
}
]
}
}
},
{
type: 'input-tree',
id: 'input-tree-action',
name: 'tree',
label: 'Tree',
creatable: true,
removable: true,
editable: true,
initiallyOpen: false,
unfoldedLevel: 0,
deferApi: '/api/mock2/form/deferOptions?label=${label}&waitSeconds=2',
options: [
{
label: 'Folder A',
value: 1,
children: [
{
label: 'file A(懒加载)',
defer: true,
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
}
],
onEvent: {
change: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发change事件'
}
]
},
add: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发add事件'
}
]
},
edit: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发edit事件'
}
]
},
delete: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发delete事件'
}
]
},
loadFinished: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发load事件'
}
]
}
}
}
]
}
]
};

View File

@ -0,0 +1,198 @@
const options = [
{
label: 'Folder A',
value: 1,
children: [
{
label: 'file A(懒加载)',
defer: true,
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
}
]
const onEvent = {
blur: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发blur事件'
}
]
},
focus: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发focus事件'
}
]
},
change: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发change事件'
}
]
},
add: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发add事件'
}
]
},
edit: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发edit事件'
}
]
},
delete: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发delete事件'
}
]
},
load: {
actions: [
{
actionType: 'toast',
msgType: 'info',
msg: '派发load事件'
}
]
}
}
export default {
type: 'page',
title: '树形选择器',
regions: ['body', 'toolbar', 'header'],
body: [
{
type: 'form',
debug: true,
body: [
{
name: "tree-select-clear",
type: "action",
label: 'clear触发器',
level: 'primary',
className: 'mr-3',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'tree-select-action',
description: '点击清空内容'
}
]
}
}
},
{
name: "tree-select-reset",
type: "action",
label: 'reset触发器',
level: 'primary',
className: 'mr-3',
onEvent: {
click: {
actions: [
{
actionType: 'clear',
componentId: 'tree-select-action',
description: '点击清空内容'
}
]
}
}
},
{
name: "tree-select-choose",
type: "action",
label: 'choose触发器Folder A',
level: 'primary',
className: 'mr-3',
onEvent: {
click: {
actions: [
{
actionType: 'choose',
componentId: 'tree-select-action',
description: '点击选中特定值',
args: {
value: 1
},
}
]
}
}
},
{
type: 'tree-select',
id: 'tree-select-action',
name: 'tree',
label: 'Tree',
creatable: true,
removable: true,
editable: true,
deferApi: '/api/mock2/form/deferOptions?label=${label}&waitSeconds=2',
options,
onEvent
},
{
type: 'tree-select',
id: 'tree-select-action2',
name: 'tree多选',
label: 'tree多选',
multiple: true,
creatable: true,
removable: true,
editable: true,
deferApi: '/api/mock2/form/deferOptions?label=${label}&waitSeconds=2',
options,
onEvent
}
]
}
]
};

View File

@ -83,6 +83,8 @@ import ButtonEventActionSchema from './EventAction/ButtonEvent';
import InputRatingEventSchema from './EventAction/InputRatingEvent';
import ExcelEventSchema from './EventAction/ExcelEvent';
import WizardEventSchema from './EventAction/WizardEvent';
import InputTreeEventSchema from './EventAction/InputTreeEvent';
import treeSelectEventSchema from './EventAction/treeSelectEvent';
import WizardSchema from './Wizard';
import ChartSchema from './Chart';
import EChartsEditorSchema from './ECharts';
@ -587,6 +589,16 @@ export const examples = [
path: 'examples/event/wizard',
component: makeSchemaRenderer(WizardEventSchema)
},
{
label: '树形选择框',
path: 'examples/event/input-tree',
component: makeSchemaRenderer(InputTreeEventSchema)
},
{
label: '树形选择器',
path: 'examples/event/tree-select',
component: makeSchemaRenderer(treeSelectEventSchema)
},
]
},
{

View File

@ -41,6 +41,8 @@ export interface IDropInfo {
interface TreeSelectorProps extends ThemeProps, LocaleProps {
highlightTxt?: string;
onRef: any;
showIcon?: boolean;
// 是否默认都展开
initiallyOpen?: boolean;
@ -206,6 +208,7 @@ export class TreeSelector extends React.Component<
componentDidMount() {
const {enableNodePath} = this.props;
this.props.onRef(this)
enableNodePath && this.expandLazyLoadNodes();
}
@ -246,20 +249,23 @@ export class TreeSelector extends React.Component<
onExpandTree?.(nodePathArr);
}
syncUnFolded(props: TreeSelectorProps) {
syncUnFolded(props: TreeSelectorProps, unfoldedLevel?: Number) {
// 传入默认展开层级需要重新初始化unfolded
let initFoldedLevel = typeof unfoldedLevel !== 'undefined';
let expandLevel = initFoldedLevel ? unfoldedLevel : props.unfoldedLevel;
// 初始化树节点的展开状态
let unfolded = this.unfolded;
const {foldedField, unfoldedField} = this.props;
eachTree(props.options, (node: Option, index, level) => {
if (unfolded.has(node)) {
if (unfolded.has(node) && !initFoldedLevel) {
return;
}
if (node.children && node.children.length) {
let ret: any = true;
if (node.defer && node.loaded) {
if (node.defer && node.loaded && !initFoldedLevel) {
ret = true;
} else if (
unfoldedField &&
@ -270,7 +276,7 @@ export class TreeSelector extends React.Component<
ret = !node[foldedField];
} else {
ret = !!props.initiallyOpen;
if (!ret && level <= (props.unfoldedLevel as number)) {
if (!ret && level <= (expandLevel as number)) {
ret = true;
}
}
@ -278,6 +284,8 @@ export class TreeSelector extends React.Component<
}
});
initFoldedLevel && this.forceUpdate();
return unfolded;
}

View File

@ -8,6 +8,8 @@ import {
} from './Options';
import {Spinner} from '../../components';
import {SchemaApi} from '../../Schema';
import {autobind, createObject} from '../../utils/helper';
import {Action} from '../../types';
/**
* Tree
@ -98,12 +100,51 @@ export default class TreeControl extends React.Component<TreeProps> {
enableNodePath: false,
pathSeparator: '/'
};
treeRef: any
reload() {
const reload = this.props.reloadOptions;
reload && reload();
}
doAction(action: Action, data: {openLevel: Number, value: any}, throwErrors: boolean) {
const {resetValue, onChange, options} = this.props;
if (action.actionType && ['clear', 'reset'].includes(action.actionType)) {
onChange && onChange(resetValue ?? '');
}
if (action.actionType === 'expand') {
this.treeRef.syncUnFolded(this.props, data.openLevel);
}
if (action.actionType === 'collapse') {
this.treeRef.syncUnFolded(this.props, 0);
}
if (action.actionType === 'choose') {
onChange && onChange(data.value || '');
}
}
@autobind
async handleChange(value: any) {
const {onChange, dispatchEvent, data} = this.props;
const rendererEvent = await dispatchEvent('change', createObject(data, {
value
}));
if (rendererEvent?.prevented) {
return;
}
onChange && onChange(value);
}
@autobind
domRef(ref: any) {
this.treeRef = ref;
}
render() {
const {
className,
@ -112,7 +153,6 @@ export default class TreeControl extends React.Component<TreeProps> {
value,
enableNodePath,
pathSeparator = '/',
onChange,
disabled,
joinValues,
extractValue,
@ -162,11 +202,12 @@ export default class TreeControl extends React.Component<TreeProps> {
{loading ? null : (
<TreeSelector
classPrefix={ns}
onRef={this.domRef}
labelField={labelField}
valueField={valueField}
iconField={iconField}
disabled={disabled}
onChange={onChange}
onChange={this.handleChange}
joinValues={joinValues}
extractValue={extractValue}
delimiter={delimiter}

View File

@ -454,10 +454,10 @@ export function registerOptionsControl(config: OptionsConfig) {
this.toDispose = [];
}
async dispatchChangeEvent(eventData: any = '') {
async dispatchOptionEvent(eventName: string, eventData: any = '') {
const {dispatchEvent, options, data} = this.props;
const rendererEvent = await dispatchEvent(
'change',
eventName,
createObject(data, {
value: eventData,
options,
@ -599,7 +599,7 @@ export function registerOptionsControl(config: OptionsConfig) {
value
);
const isPrevented = await this.dispatchChangeEvent(newValue);
const isPrevented = await this.dispatchOptionEvent('change', newValue);
isPrevented || (onChange && onChange(newValue, submitOnChange, changeImmediately));
}
@ -667,7 +667,7 @@ export function registerOptionsControl(config: OptionsConfig) {
? []
: formItem.filteredOptions.concat();
const newValue = this.formatValueArray(valueArray);
const isPrevented = await this.dispatchChangeEvent(newValue);
const isPrevented = await this.dispatchOptionEvent('change', newValue);
isPrevented || (onChange && onChange(newValue));
}
@ -767,7 +767,7 @@ export function registerOptionsControl(config: OptionsConfig) {
}
@autobind
deferLoad(option: Option) {
async deferLoad(option: Option) {
const {deferApi, source, env, formItem, data} = this.props;
const api = option.deferApi || deferApi || source;
@ -779,7 +779,9 @@ export function registerOptionsControl(config: OptionsConfig) {
return;
}
formItem?.deferLoadOptions(option, api, createObject(data, option));
const json = await formItem?.deferLoadOptions(option, api, createObject(data, option));
// 触发事件通知,加载完成
this.dispatchOptionEvent('loadFinished',json);
}
@autobind
@ -992,6 +994,11 @@ export function registerOptionsControl(config: OptionsConfig) {
[valueField || 'value']: result[labelField || 'label']
};
}
// 触发事件通知
const isPrevented = await this.dispatchOptionEvent('add', {...result, idx});
if (isPrevented) {
return;
}
// 如果是懒加载的,只懒加载当前节点。
if (parent?.defer) {
@ -1095,6 +1102,12 @@ export function registerOptionsControl(config: OptionsConfig) {
return;
}
// 触发事件通知
const isPrevented = await this.dispatchOptionEvent('edit', result);
if (isPrevented) {
return;
}
if (source && editApi) {
this.reload();
} else {
@ -1141,6 +1154,12 @@ export function registerOptionsControl(config: OptionsConfig) {
return;
}
// 触发事件通知
const isPrevented = await this.dispatchOptionEvent('delete', ctx);
if (isPrevented) {
return;
}
// 通过 deleteApi 删除。
try {
if (!deleteApi) {

View File

@ -18,9 +18,10 @@ import {Api} from '../../types';
import {isEffectiveApi} from '../../utils/api';
import Spinner from '../../components/Spinner';
import ResultBox from '../../components/ResultBox';
import {autobind, getTreeAncestors, isMobile} from '../../utils/helper';
import {autobind, getTreeAncestors, isMobile, createObject} from '../../utils/helper';
import {findDOMNode} from 'react-dom';
import {normalizeOptions} from '../../components/Select';
import {Action} from '../../types';
/**
* Tree
@ -121,6 +122,8 @@ export default class TreeSelectControl extends React.Component<
pathSeparator: '/'
};
treeRef: any
container: React.RefObject<HTMLDivElement> = React.createRef();
input: React.RefObject<any> = React.createRef();
@ -185,16 +188,20 @@ export default class TreeSelectControl extends React.Component<
);
}
handleFocus() {
handleFocus(e: any) {
const {dispatchEvent} = this.props;
this.setState({
isFocused: true
});
dispatchEvent('focus', e);
}
handleBlur() {
handleBlur(e: any) {
const {dispatchEvent} = this.props;
this.setState({
isFocused: false
});
dispatchEvent('blur', e);
}
handleKeyPress(e: React.KeyboardEvent) {
@ -257,19 +264,19 @@ export default class TreeSelectControl extends React.Component<
}
handleChange(value: any) {
const {onChange, multiple} = this.props;
const {multiple} = this.props;
if (!multiple) {
this.close();
}
multiple || !this.state.inputValue
? onChange(value)
? this.resultChangeEvent(value)
: this.setState(
{
inputValue: ''
},
() => onChange(value)
() => this.resultChangeEvent(value)
);
}
@ -410,14 +417,13 @@ export default class TreeSelectControl extends React.Component<
extractValue,
delimiter,
valueField,
onChange,
multiple
} = this.props;
let newValue: any = Array.isArray(value) ? value.concat() : [];
if (!multiple && !newValue.length) {
onChange('');
this.resultChangeEvent('');
return;
}
@ -428,8 +434,31 @@ export default class TreeSelectControl extends React.Component<
if (joinValues) {
newValue = newValue.join(delimiter || ',');
}
this.resultChangeEvent(newValue);
}
onChange(newValue);
doAction(action: Action, data: {value: any}, throwErrors: boolean) {
const {onChange} = this.props;
if (action.actionType && ['clear', 'reset'].includes(action.actionType)) {
this.clearValue();
}
if (action.actionType === 'choose') {
onChange && onChange(data.value || '');
}
}
@autobind
async resultChangeEvent(value: any) {
const {onChange, dispatchEvent, data} = this.props;
const rendererEvent = await dispatchEvent('change', createObject(data, {
value
}));
if (rendererEvent?.prevented) {
return;
}
onChange && onChange(value);
}
@autobind
@ -449,6 +478,11 @@ export default class TreeSelectControl extends React.Component<
}`;
}
@autobind
domRef(ref: any) {
this.treeRef = ref;
}
renderOuter() {
const {
value,
@ -506,6 +540,7 @@ export default class TreeSelectControl extends React.Component<
return (
<TreeSelector
classPrefix={ns}
onRef={this.domRef}
onlyChildren={onlyChildren}
labelField={labelField}
valueField={valueField}

View File

@ -104,6 +104,9 @@ export interface Action extends Button {
| 'clear-and-submit'
| 'toast'
| 'goto-step'
| 'expand'
| 'collapse'
| 'choose'
| 'step-submit';
api?: Api;
asyncApi?: Api;