improve drag and drop

This commit is contained in:
chenos 2021-08-02 15:05:28 +08:00
parent 57cc9e6081
commit cf2d3fcdaf
13 changed files with 1023 additions and 251 deletions

View File

@ -29,6 +29,7 @@
"axios": "^0.21.1",
"beautiful-react-hooks": "^0.35.0",
"constate": "^3.3.0",
"html-react-parser": "^1.2.7",
"lodash": "^4.17.21",
"monaco-editor": "^0.25.2",
"react-dnd": "^14.0.2",

View File

@ -0,0 +1,100 @@
import React, { useState } from 'react';
import {
SortableContext,
useSortable,
horizontalListSortingStrategy,
verticalListSortingStrategy,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Menu, Table, Tabs } from 'antd';
import { createContext } from 'react';
import { useContext } from 'react';
import { range } from 'lodash';
const RowSyntheticListenerMapContext = createContext(null);
const CellContext = createContext(null);
import cls from 'classnames';
import {
DndContext,
DragOverlay,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
useDraggable,
useDroppable,
} from '@dnd-kit/core';
import { forwardRef } from 'react';
import { DragOutlined } from '@ant-design/icons';
import { useRef } from 'react';
const Div = forwardRef<HTMLDivElement>((props, ref) => {
return <div ref={ref} {...props} />;
});
export interface SortableItemProps {}
export const SortableItemContext = createContext<any>({});
export function DragHandle() {
return (
<SortableItemContext.Consumer>
{({ setDraggableNodeRef, attributes, listeners }) =>
setDraggableNodeRef && (
<DragOutlined
ref={setDraggableNodeRef}
{...attributes}
{...listeners}
/>
)
}
</SortableItemContext.Consumer>
);
}
export function SortableItem(props) {
const { id, data = {}, className, component, children, ...others } = props;
const previewRef = useRef<HTMLElement>();
const { isOver, setNodeRef: setDroppableNodeRef } = useDroppable({
id: `droppable-${id}`,
data: {
...data,
previewRef,
},
});
const {
isDragging,
attributes,
listeners,
setNodeRef: setDraggableNodeRef,
transform,
} = useDraggable({
id: `draggable-${id}`,
data: {
...data,
previewRef,
},
});
const Component = component || Div;
return (
<Component
{...others}
className={cls(className, `droppable-${id}`, {
isOver,
isDragging,
})}
ref={(el: HTMLElement) => {
previewRef.current = el;
setDroppableNodeRef(el);
}}
>
<SortableItemContext.Provider
value={{ setDraggableNodeRef, attributes, listeners }}
>
{children}
</SortableItemContext.Provider>
</Component>
);
}

View File

@ -3,7 +3,7 @@ import { Resource } from '../resource';
export const useResource = (options: any = {}) => {
const { collection } = useCollectionContext();
const resource = Resource.make(collection.name);
const resource = Resource.make(collection?.name);
return {
initialValues: {},
loading: false,

View File

@ -272,10 +272,16 @@ import { SchemaRenderer } from '@nocobase/client';
const schema = {
type: 'void',
name: 'dropdown1',
name: 'action1',
title: '下拉菜单',
'x-component': 'Action.Dropdown',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
},
properties: {
dropdown1: {
type: 'void',
'x-component': 'Action.Dropdown',
'x-component-props': {},
properties: {
item1: {
@ -327,6 +333,8 @@ const schema = {
},
},
},
}
},
};
export default () => {

View File

@ -20,9 +20,11 @@ import cls from 'classnames';
import { MenuOutlined } from '@ant-design/icons';
import { FormLayout } from '@formily/antd';
import IconPicker from '../../components/icon-picker';
import { useSchemaComponent } from '../../components/schema-renderer';
import { getSchemaPath, useSchemaComponent } from '../../components/schema-renderer';
import { VisibleContext } from '../../context';
export const ButtonComponentContext = createContext(null);
export const Action: any = observer((props: any) => {
const { useAction = useDefaultAction, icon, ...others } = props;
const { run } = useAction();
@ -33,9 +35,7 @@ export const Action: any = observer((props: any) => {
const isDropdownOrPopover =
child &&
['Action.Dropdown', 'Action.Popover'].includes(child['x-component']);
return (
<VisibleContext.Provider value={[visible, setVisible]}>
{!isDropdownOrPopover && (
const button = (
<Button
{...others}
icon={<IconPicker type={icon} />}
@ -45,11 +45,16 @@ export const Action: any = observer((props: any) => {
}}
>
{schema.title}
<DesignableBar />
<DesignableBar path={getSchemaPath(schema)}/>
</Button>
)}
);
return (
<ButtonComponentContext.Provider value={button}>
<VisibleContext.Provider value={[visible, setVisible]}>
{!isDropdownOrPopover && button}
<RecursionField schema={schema} onlyRenderProperties />
</VisibleContext.Provider>
</ButtonComponentContext.Provider>
);
});
@ -187,49 +192,29 @@ Action.Drawer = observer((props: any) => {
});
Action.Dropdown = observer((props: any) => {
const { buttonProps = {}, ...others } = props;
const { schema } = useDesignable();
let componentProps = {...buttonProps};
let title = schema.title;
if (schema.parent['x-component'] === 'Action') {
title = schema.parent.title;
componentProps = {...buttonProps, ...(schema.parent['x-component-props'] || {})}
}
const icon = componentProps['icon'];
const button = useContext(ButtonComponentContext);
return (
<Dropdown
trigger={['click']}
{...others}
{...props}
overlay={
<Menu>
<RecursionField schema={schema} onlyRenderProperties />
</Menu>
}
>
<Button
{...buttonProps}
{...componentProps}
icon={<IconPicker type={icon} />}
>
{title}
</Button>
<span>{button}</span>
</Dropdown>
);
});
Action.Popover = observer((props) => {
const fieldSchema = useFieldSchema();
const { schema } = useDesignable();
const form = useForm();
const isFormDecorator = schema['x-decorator'] === 'Form';
const [visible, setVisible] = useContext(VisibleContext);
console.log('Action.Popover', schema, fieldSchema);
const componentProps =
(schema.parent && schema.parent['x-component-props']) || {};
const designableBarComponent = schema.parent
? schema.parent['x-designable-bar']
: null;
const DesignableBar = useSchemaComponent(designableBarComponent);
const button = useContext(ButtonComponentContext);
return (
<Popover
visible={visible}
@ -266,18 +251,15 @@ Action.Popover = observer((props) => {
</div>
}
>
<Button {...componentProps}>
{schema.parent.title || schema.title}
{DesignableBar && <DesignableBar />}
</Button>
{button}
</Popover>
);
});
Action.DesignableBar = () => {
Action.DesignableBar = (props: any) => {
const field = useField();
// const schema = useFieldSchema();
const { schema, insertAfter, refresh } = useDesignable();
const { schema, insertAfter, refresh } = useDesignable(props.path);
const [visible, setVisible] = useState(false);
return (
<div className={cls('designable-bar', { active: visible })}>
@ -297,7 +279,7 @@ Action.DesignableBar = () => {
<Menu>
<Menu.Item
onClick={(e) => {
// console.log({ field, schema });
console.log('按钮文案被修改了', { schema });
schema.title = '按钮文案被修改了';
// field.setTitle('按钮文案被修改了');
// setVisible(false);

View File

@ -109,7 +109,7 @@ const isGridBlock = (schema: Schema) => {
};
function generateCardItemSchema(component) {
const defaults = {
const defaults: { [key: string]: ISchema } = {
'Markdown.Void': {
type: 'void',
default: '这是一段演示文字,**支持使用 Markdown 语法**',
@ -162,6 +162,21 @@ function generateCardItemSchema(component) {
fieldNames: [],
},
},
[uid()]: {
type: 'void',
name: 'action1',
title: '删除',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'destroy',
},
'x-component': 'Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},
},
[uid()]: {
type: 'void',
name: 'action1',
@ -172,6 +187,9 @@ function generateCardItemSchema(component) {
displayName: 'create',
},
'x-component': 'Action',
'x-component-props': {
type: 'primary',
},
'x-designable-bar': 'Table.Action.DesignableBar',
properties: {
modal: {
@ -194,21 +212,6 @@ function generateCardItemSchema(component) {
},
},
},
[uid()]: {
type: 'void',
name: 'action1',
title: '删除',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'destroy',
},
'x-component': 'Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},
},
},
},
[uid()]: {
@ -222,12 +225,17 @@ function generateCardItemSchema(component) {
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Dropdown',
name: 'action1',
'x-component': 'Action',
// 'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
buttonProps: {
icon: 'EllipsisOutlined',
},
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Dropdown',
'x-component-props': {},
properties: {
[uid()]: {
type: 'void',
@ -254,19 +262,21 @@ function generateCardItemSchema(component) {
'x-component-props': {
bodyStyle: {
background: '#f0f2f5',
paddingTop: 0,
// paddingTop: 0,
},
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Tabs',
'x-designable-bar': 'Tabs.DesignableBar',
properties: {
[uid()]: {
type: 'void',
title: '详情',
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
'x-component': 'Tabs.TabPane',
'x-component-props': {
tab: 'Tab1',
},
properties: {
[uid()]: {
@ -328,6 +338,8 @@ function generateCardItemSchema(component) {
},
},
},
},
},
Form: {
type: 'void',
name: uid(),
@ -512,7 +524,11 @@ AddNew.CardItem = observer((props: any) => {
key={view.key}
title={view.title}
>
<Menu.ItemGroup className={'display-fields'} key={`${view.key}-select`} title={'所属数据表'}>
<Menu.ItemGroup
className={'display-fields'}
key={`${view.key}-select`}
title={'所属数据表'}
>
{collections?.map((item) => (
<Menu.Item
style={{ minWidth: 150 }}

View File

@ -60,6 +60,13 @@ import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
import deepmerge from 'deepmerge';
import { onFieldChange } from '@formily/core';
import { VisibleContext } from '../../context';
import {
DragHandle,
SortableItem,
SortableItemContext,
} from '../../components/Sortable';
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { createPortal } from 'react-dom';
export const MenuModeContext = createContext(null);
@ -130,7 +137,24 @@ export const Menu: any = observer((props: any) => {
sideMenuElement.style.display = isSubMenu ? 'block' : 'none';
}, [selectedKey]);
const [dragOverlayContent, setDragOverlayContent] = useState('');
return (
<DndContext onDragStart={(event) => {
setDragOverlayContent(event.active.data?.current?.title || '');
}}>
{createPortal(
<DragOverlay
style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}
dropAnimation={{
duration: 10,
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
}}
>
{dragOverlayContent}
</DragOverlay>,
document.body,
)}
<MenuModeContext.Provider value={mode}>
<AntdMenu
defaultSelectedKeys={defaultSelectedKeys}
@ -177,6 +201,7 @@ export const Menu: any = observer((props: any) => {
</div>
)}
</MenuModeContext.Provider>
</DndContext>
);
});
@ -188,12 +213,22 @@ Menu.Item = observer((props: any) => {
return (
<AntdMenu.Item
{...props}
icon={<IconPicker type={icon} />}
icon={null}
eventKey={schema.name}
key={schema.name}
>
<SortableItem
id={schema.name}
data={{
title: schema.title,
}}
>
{icon && (
<span style={{ marginRight: 10 }}><IconPicker type={icon} /></span>
)}
{schema.title}
<DesignableBar />
</SortableItem>
</AntdMenu.Item>
);
});
@ -204,11 +239,22 @@ Menu.Link = observer((props: any) => {
return (
<AntdMenu.Item
{...props}
icon={<IconPicker type={icon} />}
icon={null}
eventKey={schema.name}
key={schema.name}
>
<SortableItem
id={schema.name}
data={{
title: schema.title,
}}
>
{icon && (
<span style={{ marginRight: 10 }}><IconPicker type={icon} /></span>
)}
{schema.title}
<DesignableBar />
</SortableItem>
{/* <Link to={props.to || schema.name}>{schema.title}</Link> */}
<DesignableBar />
</AntdMenu.Item>
@ -553,6 +599,7 @@ Menu.DesignableBar = (props) => {
}}
className={'designable-bar-actions'}
>
<DragHandle />
<Dropdown
overlayStyle={{
minWidth: 150,

View File

@ -0,0 +1,226 @@
import React, { forwardRef, useState } from 'react';
import {
SortableContext,
useSortable,
horizontalListSortingStrategy,
verticalListSortingStrategy,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Table } from 'antd';
import { createContext } from 'react';
import { useContext } from 'react';
import { range } from 'lodash';
import parse from 'html-react-parser';
import cls from 'classnames';
import { MenuOutlined } from '@ant-design/icons';
import {
DndContext,
DragOverlay,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
useDroppable,
useDraggable,
} from '@dnd-kit/core';
import { createPortal } from 'react-dom';
import { useRef } from 'react';
import { SortableItem } from '../../components/Sortable';
export const RowDraggableContext = createContext({});
export const ColDraggableContext = createContext(null);
export const CellContext = createContext(null);
export function SortableColumn(props) {
const { className } = props;
const { isOver, setNodeRef: setDroppableNodeRef } = useDroppable({
id: `droppable-${props['id']}`,
});
const {
isDragging,
attributes,
listeners,
setNodeRef: setDraggableNodeRef,
transform,
} = useDraggable({
id: `draggable-${props['id']}`,
});
return (
<div
{...props}
className={cls(className, `draggable-${props['id']}`, {
isOver,
isDragging,
})}
ref={setDroppableNodeRef}
// {...attributes}
>
<ColDraggableContext.Provider
value={{ setDraggableNodeRef, attributes, listeners }}
>
{props.children}
</ColDraggableContext.Provider>
{/* <span ref={setDraggableNodeRef} {...attributes} {...listeners}>
Drag
</span> */}
</div>
);
}
export function SortableBodyRow(props: any) {
const {
className,
style: prevStyle,
['data-row-key']: dataRowKey,
...others
} = props;
const {
isDragging,
attributes,
listeners,
setNodeRef,
setDraggableNodeRef,
overIndex,
transform,
transition,
} = useSortable({ id: dataRowKey });
const style = {
...prevStyle,
transform: CSS.Transform.toString(transform),
transition,
};
return (
<RowDraggableContext.Provider
value={{ listeners, setDraggableNodeRef, attributes }}
>
<tr
className={cls(
{ isDragging },
props.className,
`droppable-${props['data-row-key']}`,
)}
// className={cls(className)}
ref={setNodeRef}
{...others}
{...attributes}
style={style}
>
<SortableContext
strategy={horizontalListSortingStrategy}
items={React.Children.map(props.children, (child) => {
console.log(child.key, 'child.key');
return `td${child.key}`;
})}
>
{React.Children.map(props.children, (child, index) => (
<CellContext.Provider value={`td${child.key}`}>
{child}
</CellContext.Provider>
))}
</SortableContext>
</tr>
</RowDraggableContext.Provider>
);
}
export function SortableHeaderRow(props) {
const [dragOverlayContent, setDragOverlayContent] = useState('');
return (
<tr {...props}>
<DndContext
onDragStart={(event) => {
const previewRef = event.active.data?.current?.previewRef as {
current: HTMLElement;
};
if (previewRef?.current) {
setDragOverlayContent(previewRef.current.textContent || '');
}
}}
>
{createPortal(
<DragOverlay style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}>
{dragOverlayContent}
</DragOverlay>,
document.body,
)}
{React.Children.map(props.children, (child, index) => (
<CellContext.Provider value={child.key}>{child}</CellContext.Provider>
))}
</DndContext>
</tr>
);
}
export function SortableHeaderCell(props) {
const id = useContext(CellContext);
if (['RC_TABLE_KEY', 'addnew'].includes(id)) {
return <th {...props} />;
}
return (
<SortableItem
{...props}
id={id}
component={forwardRef<any>((props, ref) => (
<th ref={ref} {...props} />
))}
/>
);
}
export function SortableBodyCell(props) {
const id = useContext(CellContext);
const {
isDragging,
attributes,
listeners,
setNodeRef,
setDraggableNodeRef,
setDroppableNodeRef,
transform,
transition,
} = useSortable({ id: id });
const style = {
...props.style,
transform: CSS.Transform.toString(transform),
transition,
};
return (
<td
ref={setDroppableNodeRef}
{...props}
className={cls({ isDragging }, props.className)}
// {...attributes}
// {...listeners}
style={style}
>
<div style={{ height: 0, width: 0, opacity: 0, overflow: 'hidden' }}>
<span ref={setDraggableNodeRef} {...attributes} {...listeners}>
Drag
</span>
</div>
{props.children}
</td>
);
}
export function SortableRowHandle(props) {
const { className, ...others } = props;
const { setDraggableNodeRef, attributes, listeners } =
useContext<any>(RowDraggableContext);
return setDraggableNodeRef ? (
<MenuOutlined
{...others}
// ref={setDraggableNodeRef}
// {...attributes}
{...listeners}
className={cls(`nb-table-sort-handle`, className)}
/>
) : null;
}

View File

@ -18,8 +18,12 @@ import useRequest from '@ahooksjs/use-request';
import { BaseResult } from '@ahooksjs/use-request/lib/types';
import cls from 'classnames';
import { MenuOutlined, DragOutlined } from '@ant-design/icons';
import { DndContext } from '@dnd-kit/core';
import { SortableContext, useSortable } from '@dnd-kit/sortable';
import { DndContext, DragOverlay } from '@dnd-kit/core';
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Select, Dropdown, Menu, Switch, Button, Space } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
@ -44,6 +48,16 @@ import { useResource as useGeneralResource } from '../../hooks/useResource';
import SwitchMenuItem from '../../components/SwitchMenuItem';
import { useMemo } from 'react';
import { createForm } from '@formily/core';
import {
ColDraggableContext,
SortableBodyCell,
SortableBodyRow,
SortableColumn,
SortableHeaderCell,
SortableHeaderRow,
SortableRowHandle,
} from './Sortable';
import { DragHandle, SortableItem } from '../../components/Sortable';
const SyntheticListenerMapContext = createContext(null);
@ -272,7 +286,7 @@ const useTableColumns = () => {
const columnProps = column['x-component-props'] || {};
const collectionField = getField(columnProps.fieldName);
return {
title: (
title: () => (
<CollectionFieldContext.Provider value={collectionField}>
<RecursionField name={column.name} schema={column} onlyRenderSelf />
</CollectionFieldContext.Provider>
@ -402,6 +416,7 @@ const TableMain = () => {
const columns = useTableColumns();
const dataSource = useDataSource();
const actionBars = useTableActionBars();
const [html, setHtml] = useState('');
return (
<div className={'nb-table'}>
<DndContext
@ -421,7 +436,10 @@ const TableMain = () => {
}
/>
))}
<SortableContext items={dataSource}>
<SortableContext
items={dataSource}
strategy={verticalListSortingStrategy}
>
<AntdTable
pagination={false}
onChange={(pagination) => {}}
@ -429,9 +447,32 @@ const TableMain = () => {
rowKey={rowKey}
dataSource={dataSource}
columns={columns}
// components={{
// body: {
// row: DragableBodyRow,
// },
// }}
components={{
header: {
row: SortableHeaderRow,
cell: SortableHeaderCell,
},
body: {
row: DragableBodyRow,
// wrapper: (props) => {
// return (
// <tbody {...props}>
// <DragOverlay
// className={'ant-table-row'}
// wrapperElement={'tr'}
// >
// <div />
// </DragOverlay>
// {props.children}
// </tbody>
// );
// },
row: SortableBodyRow,
// cell: SortableBodyCell,
},
}}
rowSelection={{
@ -749,7 +790,11 @@ function Actions(props: any) {
if (currentAlign !== align) {
return null;
}
return <RecursionField name={s.name} schema={s} />;
return (
<SortableItem id={s.name} data={{ title: s.title }}>
<RecursionField name={s.name} schema={s} />
</SortableItem>
);
})}
</Space>
);
@ -758,20 +803,33 @@ function Actions(props: any) {
Table.ActionBar = observer((props: any) => {
const { align = 'top' } = props;
const { schema, designable } = useDesignable();
const designableBar = schema['x-designable-bar'];
console.log('Table.ActionBar', { schema });
const [dragOverlayContent, setDragOverlayContent] = useState('');
return (
<DndContext onDragStart={(event) => {
setDragOverlayContent(event.active.data?.current?.title || '');
// const previewRef = event.active.data?.current?.previewRef;
// if (previewRef) {
// setDragOverlayContent(previewRef?.current?.innerHTML);
// } else {
// setDragOverlayContent('');
// }
}}>
<DragOverlay style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}>
{dragOverlayContent}
{/* <div style={{ transform: 'translateX(-100%)' }} dangerouslySetInnerHTML={{__html: dragOverlayContent}}></div> */}
</DragOverlay>
<DisplayedMapProvider>
<div className={cls('nb-action-bar', `align-${align}`)}>
<div>
<div style={{ width: '50%' }}>
<Actions align={'left'} />
</div>
<div style={{ marginLeft: 'auto' }}>
<div style={{ marginLeft: 'auto', width: '50%', textAlign: 'right' }}>
<Actions align={'right'} />
</div>
<AddActionButton />
</div>
</DisplayedMapProvider>
</DndContext>
);
});
@ -922,6 +980,7 @@ Table.OperationDesignableBar = () => {
}}
className={cls('designable-bar-actions', { active: visible })}
>
<DragHandle />
<Dropdown
trigger={['click']}
visible={visible}
@ -975,6 +1034,7 @@ Table.Action.DesignableBar = () => {
}}
className={cls('designable-bar-actions', { active: visible })}
>
<DragHandle />
<Dropdown
trigger={['click']}
visible={visible}
@ -1090,6 +1150,7 @@ Table.Column.DesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
const ctx = useContext(ColDraggableContext);
return (
<div className={cls('designable-bar', { active: visible })}>
<span
@ -1098,6 +1159,7 @@ Table.Column.DesignableBar = () => {
}}
className={cls('designable-bar-actions', { active: visible })}
>
<DragHandle />
<Dropdown
trigger={['click']}
visible={visible}
@ -1147,16 +1209,7 @@ Table.Index = observer(() => {
});
Table.SortHandle = observer((props: any) => {
const listeners = useContext(SyntheticListenerMapContext);
const { className, ...others } = props;
return (
<MenuOutlined
{...listeners}
className={cls(`nb-table-sort-handle`, className)}
{...others}
/>
);
return <SortableRowHandle {...props} />;
});
Table.DesignableBar = observer((props) => {

View File

@ -44,11 +44,18 @@ const schema = {
properties: {
tabs: {
type: 'void',
'x-decorator': 'Card',
'x-component': 'Tabs',
'x-designable-bar': 'Tabs.DesignableBar',
'x-component-props': {
// singleton: true,
},
properties: {
tab1: {
type: 'void',
title: 'Tab1',
'x-component': 'Tabs.TabPane',
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
'x-component-props': {
tab: 'Tab1',
},
@ -64,7 +71,9 @@ const schema = {
},
tab2: {
type: 'void',
title: 'Tab2',
'x-component': 'Tabs.TabPane',
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
'x-component-props': {
tab: 'Tab2',
},

View File

@ -1,6 +1,228 @@
import { observer, connect } from '@formily/react';
import { observer, connect, useField, RecursionField } from '@formily/react';
import React from 'react';
import { FormTab } from '@formily/antd';
import '@formily/antd/lib/form-tab/style';
import { Button, Tabs as AntdTabs, Dropdown, Menu, Switch } from 'antd';
import { useDesignable } from '../../components/schema-renderer';
import { Schema, SchemaKey } from '@formily/react';
import { PlusOutlined, MenuOutlined } from '@ant-design/icons';
import { useState } from 'react';
import cls from 'classnames';
import { createSchema, removeSchema } from '..';
import './style.less';
import { uid } from '@formily/shared';
import { DragHandle, SortableItem } from '../../components/Sortable';
import { DndContext, DragOverlay } from '@dnd-kit/core';
export const Tabs = FormTab;
const useTabs = () => {
const tabsField = useField();
const { schema } = useDesignable();
const tabs: { name: SchemaKey; props: any; schema: Schema }[] = [];
schema.mapProperties((schema, name) => {
const field = tabsField.query(tabsField.address.concat(name)).take();
if (field?.display === 'none' || field?.display === 'hidden') return;
if (schema['x-component']?.indexOf('TabPane') > -1) {
tabs.push({
name,
props: {
key: schema?.['x-component-props']?.key || name,
...schema?.['x-component-props'],
},
schema,
});
}
});
return tabs;
};
export const Tabs: any = observer((props: any) => {
const { singleton, ...others } = props;
const { schema, DesignableBar, appendChild } = useDesignable();
const tabs = useTabs();
const [dragOverlayContent, setDragOverlayContent] = useState('');
if (singleton) {
return (
<div className={'nb-tabs'}>
<DesignableBar />
{tabs.map(({ props, schema, name }, key) => (
<RecursionField schema={schema} name={name} onlyRenderProperties />
))}
</div>
);
}
return (
<div className={'nb-tabs'}>
<DesignableBar />
<DndContext
onDragStart={(event) => {
setDragOverlayContent(event.active.data?.current?.title || '');
}}
>
<DragOverlay style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}>
{dragOverlayContent}
</DragOverlay>
<AntdTabs
{...others}
className={cls({ singleton })}
tabBarExtraContent={
<Button
type={'dashed'}
icon={<PlusOutlined />}
onClick={async () => {
const data = appendChild({
type: 'void',
name: uid(),
title: uid(),
'x-component': 'Tabs.TabPane',
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
'x-component-props': {
tab: uid(),
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.PaneItem',
},
},
},
});
await createSchema(data);
}}
>
</Button>
}
>
{tabs.map(({ props, schema, name }, key) => (
<AntdTabs.TabPane
{...props}
tab={
<RecursionField schema={schema} name={name} onlyRenderSelf />
}
key={key}
>
<RecursionField
schema={schema}
name={name}
onlyRenderProperties
/>
</AntdTabs.TabPane>
))}
</AntdTabs>
</DndContext>
</div>
);
});
Tabs.TabPane = observer((props: any) => {
const { schema, DesignableBar } = useDesignable();
return (
<SortableItem
id={schema.name}
data={{
title: schema.title,
}}
>
<div className={'nb-tab-pane'}>
{schema.title} <DesignableBar />
</div>
</SortableItem>
);
});
Tabs.DesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const field = useField();
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Dropdown
trigger={['click']}
visible={visible}
placement={'bottomRight'}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
onClick={async () => {
const singleton = !field.componentProps.singleton;
schema['x-component-props'] =
schema['x-component-props'] || {};
schema['x-component-props'].singleton = singleton;
field.componentProps.singleton = singleton;
}}
>
<span style={{ marginRight: 24 }}></span>{' '}
<Switch size={'small'} />
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
);
};
Tabs.TabPane.DesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<DragHandle />{' '}
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
onClick={() => {
schema.title = uid();
schema['x-component-props'] =
schema['x-component-props'] || {};
schema['x-component-props']['tab'] = uid();
refresh();
}}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={async () => {
const data = remove();
await removeSchema(data);
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
);
};

View File

@ -0,0 +1,78 @@
.nb-tabs,
.nb-tab-pane {
&:hover {
> .designable-bar {
display: block;
}
}
> .designable-bar {
pointer-events: none;
display: none;
position: absolute;
top: 0;
right: -16px;
left: -16px;
bottom: 0;
border-radius: 0;
border: 2px solid #1890ff;
&.active {
display: block;
}
.designable-bar-actions {
pointer-events: all;
position: absolute;
right: 0;
line-height: 1rem;
background-color: #1890ff;
color: #fff;
z-index: 10;
padding: 0 3px;
.anticon {
font-size: 10px;
margin-right: 0;
}
}
}
}
.ant-tabs-tab:first-child {
.nb-tab-pane {
.designable-bar {
left: 0;
}
}
}
.ant-tabs-tab:last-child {
.nb-tab-pane {
.designable-bar {
right: 0;
}
}
}
.ant-tabs-dropdown-menu-item {
position: relative;
}
.ant-tabs.singleton {
.ant-tabs-nav {
display: none;
}
}
.nb-tabs {
position: relative;
> .designable-bar {
left: 0;
right: 0;
}
}
.ant-card-body,
.ant-modal-body {
.nb-tabs {
margin: -24px;
padding: 24px;
}
}

View File

@ -7571,7 +7571,7 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
domhandler@^4.0.0, domhandler@^4.2.0:
domhandler@4.2.0, domhandler@^4.0.0, domhandler@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059"
integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==
@ -9645,6 +9645,14 @@ hsla-regex@^1.0.0:
resolved "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
html-dom-parser@1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.0.1.tgz#5d147fed6656c12918edbcea4a423eefe8d0e715"
integrity sha512-uKXISKlHzB/l9A08jrs2wseQJ9b864ZfEdmIZskj10cuP6HxCOMHSK0RdluV8NVQaWs0PwefN7d8wqG3jR0IbQ==
dependencies:
domhandler "4.2.0"
htmlparser2 "6.1.0"
html-element-map@^1.2.0:
version "1.3.1"
resolved "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz#44b2cbcfa7be7aa4ff59779e47e51012e1c73c08"
@ -9672,12 +9680,22 @@ html-escaper@^2.0.0:
resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
html-react-parser@^1.2.7:
version "1.2.7"
resolved "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.2.7.tgz#1674ed4b96b3440ad922962a3ff000e7f3325293"
integrity sha512-gUUEgrZV0YaCxtZO2XuJDUnHSq7gOqKu1krye97cxgiZ+ipaIzspGMhATeq9lhy9gwYmwBF2YCHe/accrMMo8Q==
dependencies:
domhandler "4.2.0"
html-dom-parser "1.0.1"
react-property "1.0.1"
style-to-js "1.1.0"
html-void-elements@^1.0.0:
version "1.0.5"
resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483"
integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==
htmlparser2@^6.1.0:
htmlparser2@6.1.0, htmlparser2@^6.1.0:
version "6.1.0"
resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
@ -15976,6 +15994,11 @@ react-native-swipeout@^2.2.2:
prop-types "^15.5.10"
react-tween-state "^0.1.5"
react-property@1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/react-property/-/react-property-1.0.1.tgz#4ae4211557d0a0ae050a71aa8ad288c074bea4e6"
integrity sha512-1tKOwxFn3dXVomH6pM9IkLkq2Y8oh+fh/lYW3MJ/B03URswUTqttgckOlbxY2XHF3vPG6uanSc4dVsLW/wk3wQ==
react-redux@^7.1.0:
version "7.2.4"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
@ -18024,7 +18047,14 @@ style-inject@^0.3.0:
resolved "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3"
integrity sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==
style-to-object@^0.3.0:
style-to-js@1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.0.tgz#631cbb20fce204019b3aa1fcb5b69d951ceac4ac"
integrity sha512-1OqefPDxGrlMwcbfpsTVRyzwdhr4W0uxYQzeA2F1CBc8WG04udg2+ybRnvh3XYL4TdHQrCahLtax2jc8xaE6rA==
dependencies:
style-to-object "0.3.0"
style-to-object@0.3.0, style-to-object@^0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46"
integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==