tree支持添加/编辑/删除

This commit is contained in:
catchonme 2019-09-10 14:10:03 +08:00
parent 77e2158540
commit 1689e32097
3 changed files with 473 additions and 41 deletions

View File

@ -29,6 +29,33 @@
&.is-folded {
display: none;
}
> li {
> i {
display: inline-block;
cursor: pointer;
margin-left: 5px;
}
> input {
margin-left: 15px;
padding: px2rem(5px);
width: px2rem(150px);
height: px2rem(25px);
color: $Form-input-color;
//height: $Form-input-lineHeight * $Form-input-fontSize;
&::placeholder {
color: $Form-input-placeholderColor;
user-select: none;
}
&:focus {
outline: none;
border: $borderWidth solid $info;
}
}
}
}
&-item {
@ -41,11 +68,45 @@
&:hover {
text-decoration: none;
}
> span > i {
display: inline-block;
cursor: pointer;
margin-left: 5px;
}
}
&--isLeaf > a {
padding-left: $Tree-itemArrowWidth + $gap-xs;
}
&--isEdit {
display: inline-block;
> i {
display: inline-block;
cursor: pointer;
margin-left: 5px;
}
> input {
margin-left: 15px;
padding: px2rem(5px);
width: px2rem(150px);
height: px2rem(25px);
color: $Form-input-color;
//height: $Form-input-lineHeight * $Form-input-fontSize;
&::placeholder {
color: $Form-input-placeholderColor;
user-select: none;
}
&:focus {
outline: none;
border: $borderWidth solid $info;
}
}
}
}
&-rootItem > a > i {

View File

@ -5,16 +5,17 @@
*/
import React from 'react';
import {eachTree, isVisible} from '../utils/helper';
import {eachTree, isVisible, isObject} from '../utils/helper';
import {Option, Options, value2array} from './Checkboxes';
import {ClassNamesFn, themeable} from '../theme';
import {highlight} from '../renderers/Form/Options';
import debounce = require('lodash/debounce');
interface TreeSelectorProps {
classPrefix: string;
classnames: ClassNamesFn;
highlightTxt: string;
highlightTxt?: string;
showIcon?: boolean;
// 是否默认都展开
@ -53,11 +54,24 @@ interface TreeSelectorProps {
selfDisabledAffectChildren?: boolean;
minLength?: number;
maxLength?: number;
addMode?: string;
addItem?: Function;
openAddDialog?: Function;
editMode?: string;
editItem?: Function;
openEditDialog?: Function;
deletable?: boolean;
deleteItem?: Function;
}
interface TreeSelectorState {
value: Array<any>;
unfolded: {[propName: string]: string};
hoverItem: Option | null;
editItem: Option | null;
addItem: Option | null;
addingItem: Option | null;
editingItem: Option | null;
}
export class TreeSelector extends React.Component<TreeSelectorProps, TreeSelectorState> {
@ -92,6 +106,18 @@ export class TreeSelector extends React.Component<TreeSelectorProps, TreeSelecto
this.clearSelect = this.clearSelect.bind(this);
this.handleCheck = this.handleCheck.bind(this);
this.toggleUnfolded = this.toggleUnfolded.bind(this);
this.handleEnter = this.handleEnter.bind(this);
this.handleMove = this.handleMove.bind(this);
this.handleLeave = this.handleLeave.bind(this);
this.addItem = this.addItem.bind(this);
this.onChangeAddItem = this.onChangeAddItem.bind(this);
this.confirmAddItem = this.confirmAddItem.bind(this);
this.cancelAddItem = this.cancelAddItem.bind(this);
this.editItem = this.editItem.bind(this);
this.onChangeEditItem = this.onChangeEditItem.bind(this);
this.confirmEditItem = this.confirmEditItem.bind(this);
this.cancelEditItem = this.cancelEditItem.bind(this);
this.deleteItem = this.deleteItem.bind(this);
const props = this.props;
@ -104,7 +130,12 @@ export class TreeSelector extends React.Component<TreeSelectorProps, TreeSelecto
valueField: props.valueField,
options: props.data
}),
unfolded: this.syncUnFolded(props)
unfolded: this.syncUnFolded(props),
hoverItem: null, // 鼠标覆盖选中的 item
editItem: null, // 点击编辑时的 item
addItem: null, // 点击添加时的 item
addingItem: null, // 添加后的 item
editingItem: null // 编辑后的 item
});
}
@ -157,6 +188,7 @@ export class TreeSelector extends React.Component<TreeSelectorProps, TreeSelecto
toggleUnfolded(node: any) {
this.setState({
addItem: null,
unfolded: {
...this.state.unfolded,
[node[this.props.valueField as string]]: !this.state.unfolded[node[this.props.valueField as string]]
@ -273,6 +305,135 @@ export class TreeSelector extends React.Component<TreeSelectorProps, TreeSelecto
);
}
handleEnter(item: Option) {
this.setState({
hoverItem: item
});
}
handleMove(e: MouseEvent, item: Option) {
const target = e.target as HTMLElement;
const tagName = target.tagName;
if (tagName === 'LI') {
const current = target.childNodes[0].textContent;
if (current == item['label'] && (!this.state.hoverItem || current !== (this.state.hoverItem as Option)['label'])) {
this.setState({
hoverItem: item
});
}
}
}
handleLeave() {
this.setState({
hoverItem: null
});
}
addItem(item: Option, isFolder: boolean) {
const {addMode, openAddDialog, valueField} = this.props;
let {hoverItem, unfolded} = this.state;
if (addMode === 'dialog') {
openAddDialog && openAddDialog(hoverItem)
} else if (addMode === 'normal') {
// 添加时,默认折叠的文件夹需要展开
if (isFolder && !unfolded[item[valueField as string]]) {
unfolded = {
...unfolded,
[item[valueField as string]]: !unfolded[item[valueField as string]],
}
}
this.setState({
addItem: item,
editItem: null,
unfolded
});
}
}
editItem(item: Option) {
const {editMode, openEditDialog} = this.props;
const {hoverItem, addItem} = this.state;
if (editMode === 'dialog') {
openEditDialog && openEditDialog(hoverItem);
addItem && this.setState({
addItem: null
});
} else if (editMode === 'normal') {
this.setState({
editItem: item,
addItem: null
});
}
}
deleteItem(item: Option) {
const {deleteItem} = this.props;
deleteItem && deleteItem(item);
this.setState({
hoverItem: null
});
}
confirmAddItem() {
const {addItem} = this.props;
const {addItem: parent, addingItem} = this.state;
addItem && addItem({
...addingItem,
parent: parent
});
this.setState({
addingItem: null,
addItem: null
})
}
cancelAddItem() {
this.setState({
addItem: null
});
}
confirmEditItem() {
const {editItem} = this.props;
let {editingItem, editItem: prevItem} = this.state;
editItem && editItem({
...editingItem,
prev: prevItem
});
this.setState({
editingItem: null,
editItem: null
});
}
cancelEditItem() {
this.setState({
editItem: null
});
}
onChangeAddItem(value: string) {
this.setState({
addingItem: {
label: value
}
});
}
onChangeEditItem(item: Option, value: string) {
let {editItem} = this.state;
this.setState({
editingItem: {
...item,
label: value || (editItem as Option)['label']
}
});
}
renderList(
list: Options,
value: Option[],
@ -295,7 +456,10 @@ export class TreeSelector extends React.Component<TreeSelectorProps, TreeSelecto
highlightTxt,
data,
maxLength,
minLength
minLength,
addMode,
editMode,
deletable
} = this.props;
let childrenChecked = 0;
@ -369,48 +533,82 @@ export class TreeSelector extends React.Component<TreeSelectorProps, TreeSelecto
className={cx(`Tree-item ${itemClassName || ''}`, {
'Tree-item--isLeaf': isLeaf
})}
onMouseMove={(e) =>
!nodeDisabled && this.handleMove(e, item)
}
onMouseEnter={() =>
!nodeDisabled && this.handleEnter(item)
}
onMouseLeave={() =>
!nodeDisabled && this.handleLeave()
}
>
<a>
{!isLeaf ? (
<i
onClick={() => this.toggleUnfolded(item)}
className={cx('Tree-itemArrow', {
'is-folded': !this.state.unfolded[item[valueField]]
})}
/>
) : null}
{!this.state.editItem || isObject(this.state.editItem) && (this.state.editItem as Option)[valueField] !== item[valueField] ? (
<a>
{!isLeaf ? (
<i
onClick={() => this.toggleUnfolded(item)}
className={cx('Tree-itemArrow', {
'is-folded': !this.state.unfolded[item[valueField]],
})}
/>
) : null}
{showIcon ? (
<i
className={cx(
`Tree-itemIcon ${item[iconField] ||
{showIcon ? (
<i
className={cx(
`Tree-itemIcon ${item[iconField] ||
(childrenItems ? 'Tree-folderIcon' : 'Tree-leafIcon')}`
)}
/>
) : null}
)}
/>
) : null}
{checkbox}
{checkbox}
<span
className={cx('Tree-itemText', {
'is-children-checked': multiple && !cascade && tmpChildrenChecked && !nodeDisabled,
'is-checked': checked,
'is-disabled': nodeDisabled
})}
onClick={() =>
!nodeDisabled &&
(multiple ? this.handleCheck(item, !selfChecked) : this.handleSelect(item))
}
>
{highlightTxt ? highlight(item[nameField], highlightTxt) : item[nameField]}
</span>
</a>
{childrenItems ? (
<span
className={cx('Tree-itemText', {
'is-children-checked': multiple && !cascade && tmpChildrenChecked && !nodeDisabled,
'is-checked': checked,
'is-disabled': nodeDisabled,
})}
onClick={() =>
!nodeDisabled &&
(multiple ? this.handleCheck(item, !selfChecked) : this.handleSelect(item))
}
>
{highlightTxt ? highlight(item[nameField], highlightTxt) : item[nameField]}
</span>
{/* 非添加时 && 非编辑时 && 鼠标覆盖时是当前item时显示添加/编辑/删除图标 */}
{!this.state.addItem
&& !this.state.editItem
&& isObject(this.state.hoverItem)
&& (this.state.hoverItem as Option)[valueField as string] === item[valueField] ? (
<span>
{addMode ? <i className="fa fa-plus" onClick={() => this.addItem(item, !isLeaf)}></i> : null}
{deletable ? <i className="fa fa-minus" onClick={() => this.deleteItem(item)}></i> : null}
{editMode ? <i className="fa fa-pencil" onClick={() => this.editItem(item)}></i> : null}
</span>
) : null}
</a>
) : (
<div className={cx('Tree-item--isEdit')}>
<input placeholder='label' defaultValue={item['label']} onChange={(e) => this.onChangeEditItem(item, e.currentTarget.value)}/>
<i className="fa fa-check" onClick={this.confirmEditItem}></i>
<i className="fa fa-close" onClick={this.cancelEditItem}></i>
</div>
)}
{/* 有children而且为展开状态 或者 添加child时 */}
{((childrenItems && this.state.unfolded[item[valueField]]) || this.state.addItem && (this.state.addItem[valueField] === item[valueField])) ? (
<ul
className={cx('Tree-sublist', {
'is-folded': !this.state.unfolded[item[valueField]]
})}
className={cx('Tree-sublist')}
>
{this.state.addItem && this.state.addItem[valueField] === item[valueField] ? (
<li>
<input placeholder='label' onChange={(e) => this.onChangeAddItem(e.currentTarget.value)}/>
<i className="fa fa-check" onClick={this.confirmAddItem}></i>
<i className="fa fa-close" onClick={this.cancelAddItem}></i>
</li>
) : null}
{childrenItems}
</ul>
) : null}

View File

@ -2,6 +2,11 @@ import React from 'react';
import cx from 'classnames';
import TreeSelector from '../../components/Tree';
import {OptionsControl, OptionsControlProps} from './Options';
import {autobind, createObject} from '../../utils/helper';
import {Action, Schema, PlainObject, Api, Payload} from '../../types';
import {isEffectiveApi} from '../../utils/api';
import {filter} from '../../utils/tpl';
import {Option} from '../../components/Checkboxes';
export interface TreeProps extends OptionsControlProps {
placeholder?: any;
@ -12,9 +17,24 @@ export interface TreeProps extends OptionsControlProps {
cascade?: boolean; // 父子之间是否完全独立。
withChildren?: boolean; // 选父级的时候是否把子节点的值也包含在内。
onlyChildren?: boolean; // 选父级的时候,是否只把子节点的值包含在内
addApi?: Api;
addMode?: string;
addDialog?: Schema;
editApi?: Api;
editMode?: string;
editDialog?: Schema;
deleteApi?: Api;
deleteConfirmText?: string;
}
export default class TreeControl extends React.Component<TreeProps, any> {
export interface TreeState {
isAddModalOpened: boolean,
isEditModalOpened: boolean,
parent: Option | null,
prev: Option | null
}
export default class TreeControl extends React.Component<TreeProps, TreeState> {
static defaultProps: Partial<TreeProps> = {
placeholder: '选项加载中...',
multiple: false,
@ -24,11 +44,123 @@ export default class TreeControl extends React.Component<TreeProps, any> {
showIcon: true
};
state: TreeState = {
isAddModalOpened: false,
isEditModalOpened: false,
parent: null,
prev: null
}
reload() {
const reload = this.props.reloadOptions;
reload && reload();
}
@autobind
addItem(values: PlainObject) {
this.saveRemote(values, 'add');
}
@autobind
handleAddModalConfirm(values: Array<any>, action: Action, ctx: any, components: Array<any>) {
this.saveRemote({
...values,
parent: this.state.parent
}, 'add');
this.closeAddDialog();
}
@autobind
editItem(values: PlainObject) {
this.saveRemote(values, 'add');
}
@autobind
handleEditModalConfirm(values: Array<any>, action: Action, ctx: any, components: Array<any>) {
this.saveRemote({
...values,
prev: this.state.prev
}, 'add');
this.closeEditDialog();
}
@autobind
async saveRemote(item: any, type: 'add' | 'edit') {
const {
addApi,
editApi,
data,
env
} = this.props;
let remote: Payload | null = null;
if (type == 'add' && isEffectiveApi(addApi, createObject(data, item))) {
remote = await env.fetcher(addApi, createObject(data, item));
} else if (type == 'edit' && isEffectiveApi(editApi, createObject(data, item))) {
remote = await env.fetcher(editApi, createObject(data, item));
}
if (remote && !remote.ok) {
env.notify('error', remote.msg || '保存失败');
return;
}
this.reload();
}
@autobind
async deleteItem(item: any) {
const {deleteConfirmText, deleteApi, data, env} = this.props;
const ctx = createObject(data, item);
if (isEffectiveApi(deleteApi, ctx)) {
const confirmed = await env.confirm(deleteConfirmText ? filter(deleteConfirmText, ctx) : '确认要删除?');
if (!confirmed) {
return;
}
const result = await env.fetcher(deleteApi, ctx);
if (!result.ok) {
env.notify('error', '删除失败');
return;
}
this.reload();
}
}
@autobind
openAddDialog(parent: Option) {
this.setState({
isAddModalOpened: true,
parent
});
}
@autobind
closeAddDialog() {
this.setState({
isAddModalOpened: false,
parent: null
});
}
@autobind
openEditDialog(prev: Option) {
this.setState({
isEditModalOpened: true,
prev
})
}
@autobind
closeEditDialog() {
this.setState({
isEditModalOpened: false,
prev: null
});
}
render() {
const {
className,
@ -55,7 +187,12 @@ export default class TreeControl extends React.Component<TreeProps, any> {
rootValue,
showIcon,
showRadio,
render
render,
addMode,
addDialog,
editMode,
editDialog,
deleteApi
} = this.props;
return (
@ -90,8 +227,44 @@ export default class TreeControl extends React.Component<TreeProps, any> {
value={value || ''}
nameField="label"
selfDisabledAffectChildren={false}
addMode={addMode}
addItem={this.addItem}
openAddDialog={this.openAddDialog}
editMode={editMode}
editItem={this.editItem}
openEditDialog={this.openEditDialog}
deleteItem={this.deleteItem}
deletable={isEffectiveApi(deleteApi)}
/>
)}
{render(
'modal',
{
type: 'dialog',
...addDialog
},
{
key: 'addModal',
onConfirm: this.handleAddModalConfirm,
onClose: this.closeAddDialog,
show: this.state.isAddModalOpened
}
)}
{render(
'modal',
{
type: 'dialog',
...editDialog
},
{
key: 'editModal',
onConfirm: this.handleEditModalConfirm,
onClose: this.closeEditDialog,
show: this.state.isEditModalOpened
}
)}
</div>
);
}