improve drag and drop
This commit is contained in:
parent
57cc9e6081
commit
cf2d3fcdaf
@ -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",
|
||||
|
100
packages/client/src/components/Sortable/index.tsx
Normal file
100
packages/client/src/components/Sortable/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -272,60 +272,68 @@ 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': {},
|
||||
'x-component-props': {
|
||||
},
|
||||
properties: {
|
||||
item1: {
|
||||
dropdown1: {
|
||||
type: 'void',
|
||||
title: `操作1`,
|
||||
'x-designable-bar': 'Action.DesignableBar',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-component': 'Action.Dropdown',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
modal1: {
|
||||
item1: {
|
||||
type: 'void',
|
||||
title: '对话框标题',
|
||||
'x-component': 'Action.Modal',
|
||||
title: `操作1`,
|
||||
'x-designable-bar': 'Action.DesignableBar',
|
||||
'x-component': 'Menu.Action',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'string',
|
||||
title: '输入框',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-designable-bar': 'Input.DesignableBar',
|
||||
},
|
||||
grid: {
|
||||
modal1: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-component-props': {
|
||||
addNewComponent: 'AddNew.FormItem',
|
||||
title: '对话框标题',
|
||||
'x-component': 'Action.Modal',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'string',
|
||||
title: '输入框',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-designable-bar': 'Input.DesignableBar',
|
||||
},
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-component-props': {
|
||||
addNewComponent: 'AddNew.FormItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
item2: {
|
||||
type: 'void',
|
||||
title: `操作2`,
|
||||
'x-designable-bar': 'Menu.DesignableBar',
|
||||
'x-component': 'Menu.Action',
|
||||
properties: {
|
||||
modal2: {
|
||||
type: 'void',
|
||||
title: '对话框标题',
|
||||
'x-component': 'Action.Modal',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
item2: {
|
||||
type: 'void',
|
||||
title: `操作2`,
|
||||
'x-designable-bar': 'Menu.DesignableBar',
|
||||
'x-component': 'Menu.Action',
|
||||
properties: {
|
||||
modal2: {
|
||||
type: 'void',
|
||||
title: '对话框标题',
|
||||
'x-component': 'Action.Modal',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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,23 +35,26 @@ export const Action: any = observer((props: any) => {
|
||||
const isDropdownOrPopover =
|
||||
child &&
|
||||
['Action.Dropdown', 'Action.Popover'].includes(child['x-component']);
|
||||
const button = (
|
||||
<Button
|
||||
{...others}
|
||||
icon={<IconPicker type={icon} />}
|
||||
onClick={async () => {
|
||||
setVisible(true);
|
||||
await run();
|
||||
}}
|
||||
>
|
||||
{schema.title}
|
||||
<DesignableBar path={getSchemaPath(schema)}/>
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<VisibleContext.Provider value={[visible, setVisible]}>
|
||||
{!isDropdownOrPopover && (
|
||||
<Button
|
||||
{...others}
|
||||
icon={<IconPicker type={icon} />}
|
||||
onClick={async () => {
|
||||
setVisible(true);
|
||||
await run();
|
||||
}}
|
||||
>
|
||||
{schema.title}
|
||||
<DesignableBar />
|
||||
</Button>
|
||||
)}
|
||||
<RecursionField schema={schema} onlyRenderProperties />
|
||||
</VisibleContext.Provider>
|
||||
<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);
|
||||
|
@ -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,58 +225,67 @@ 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',
|
||||
},
|
||||
icon: 'EllipsisOutlined',
|
||||
},
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
title: '操作 1',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
minWidth: 150,
|
||||
},
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
name: 'action1',
|
||||
title: '查看',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-designable-bar': 'Table.Action.DesignableBar',
|
||||
'x-component': 'Action.Dropdown',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
title: '查看',
|
||||
'x-component': 'Action.Modal',
|
||||
title: '操作 1',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-component-props': {
|
||||
bodyStyle: {
|
||||
background: '#f0f2f5',
|
||||
paddingTop: 0,
|
||||
style: {
|
||||
minWidth: 150,
|
||||
},
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
name: 'action1',
|
||||
title: '查看',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-designable-bar': 'Table.Action.DesignableBar',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
title: '查看',
|
||||
'x-component': 'Action.Modal',
|
||||
'x-component-props': {
|
||||
bodyStyle: {
|
||||
background: '#f0f2f5',
|
||||
// paddingTop: 0,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-component-props': {
|
||||
tab: 'Tab1',
|
||||
},
|
||||
'x-component': 'Tabs',
|
||||
'x-designable-bar': 'Tabs.DesignableBar',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
title: '详情',
|
||||
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-component-props': {
|
||||
addNewComponent: 'AddNew.PaneItem',
|
||||
},
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-component-props': {
|
||||
addNewComponent: 'AddNew.PaneItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -282,44 +294,44 @@ function generateCardItemSchema(component) {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
title: '编辑',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-designable-bar': 'Table.Action.DesignableBar',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
title: '编辑数据',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useResource: '{{ Table.useResource }}',
|
||||
useValues: '{{ Table.useTableRowRecord }}',
|
||||
},
|
||||
'x-component': 'Action.Modal',
|
||||
'x-component-props': {
|
||||
useOkAction: '{{ Table.useTableUpdateAction }}',
|
||||
},
|
||||
title: '编辑',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-designable-bar': 'Table.Action.DesignableBar',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
title: '编辑数据',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useResource: '{{ Table.useResource }}',
|
||||
useValues: '{{ Table.useTableRowRecord }}',
|
||||
},
|
||||
'x-component': 'Action.Modal',
|
||||
'x-component-props': {
|
||||
addNewComponent: 'AddNew.FormItem',
|
||||
useOkAction: '{{ Table.useTableUpdateAction }}',
|
||||
},
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-component-props': {
|
||||
addNewComponent: 'AddNew.FormItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
title: '删除',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ Table.useTableDestroyAction }}',
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
title: '删除',
|
||||
'x-component': 'Menu.Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ Table.useTableDestroyAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -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 }}
|
||||
|
@ -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,53 +137,71 @@ export const Menu: any = observer((props: any) => {
|
||||
sideMenuElement.style.display = isSubMenu ? 'block' : 'none';
|
||||
}, [selectedKey]);
|
||||
|
||||
const [dragOverlayContent, setDragOverlayContent] = useState('');
|
||||
|
||||
return (
|
||||
<MenuModeContext.Provider value={mode}>
|
||||
<AntdMenu
|
||||
defaultSelectedKeys={defaultSelectedKeys}
|
||||
{...others}
|
||||
mode={mode === 'mix' ? 'horizontal' : mode}
|
||||
onSelect={(info) => {
|
||||
if (mode === 'mix') {
|
||||
setSelectedKey(info.key);
|
||||
}
|
||||
const selectedSchema = schema.properties[info.key];
|
||||
console.log({ selectedSchema });
|
||||
onSelect && onSelect({ ...info, schema: selectedSchema });
|
||||
}}
|
||||
>
|
||||
<RecursionField schema={schema} onlyRenderProperties />
|
||||
<Menu.AddNew key={uid()} path={path}>
|
||||
{/* <PlusOutlined className={'nb-add-new-icon'} /> */}
|
||||
<Button
|
||||
className={`nb-add-new-menu-item menu-mode-${
|
||||
mode === 'mix' ? 'horizontal' : mode
|
||||
}`}
|
||||
block
|
||||
type={mode == 'inline' ? 'dashed' : 'primary'}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
</Menu.AddNew>
|
||||
</AntdMenu>
|
||||
{mode === 'mix' && (
|
||||
<div ref={ref}>
|
||||
<SideMenu
|
||||
path={path}
|
||||
onSelect={(info) => {
|
||||
const keyPath = [selectedKey, ...[...info.keyPath].reverse()];
|
||||
const selectedSchema = findPropertyByPath(schema, keyPath);
|
||||
console.log('keyPath', keyPath, selectedSchema);
|
||||
onSelect &&
|
||||
onSelect({ ...info, keyPath, schema: selectedSchema });
|
||||
}}
|
||||
defaultSelectedKeys={defaultSelectedKeys || []}
|
||||
selectedKey={selectedKey}
|
||||
sideMenuRef={sideMenuRef}
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<MenuModeContext.Provider value={mode}>
|
||||
<AntdMenu
|
||||
defaultSelectedKeys={defaultSelectedKeys}
|
||||
{...others}
|
||||
mode={mode === 'mix' ? 'horizontal' : mode}
|
||||
onSelect={(info) => {
|
||||
if (mode === 'mix') {
|
||||
setSelectedKey(info.key);
|
||||
}
|
||||
const selectedSchema = schema.properties[info.key];
|
||||
console.log({ selectedSchema });
|
||||
onSelect && onSelect({ ...info, schema: selectedSchema });
|
||||
}}
|
||||
>
|
||||
<RecursionField schema={schema} onlyRenderProperties />
|
||||
<Menu.AddNew key={uid()} path={path}>
|
||||
{/* <PlusOutlined className={'nb-add-new-icon'} /> */}
|
||||
<Button
|
||||
className={`nb-add-new-menu-item menu-mode-${
|
||||
mode === 'mix' ? 'horizontal' : mode
|
||||
}`}
|
||||
block
|
||||
type={mode == 'inline' ? 'dashed' : 'primary'}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
</Menu.AddNew>
|
||||
</AntdMenu>
|
||||
{mode === 'mix' && (
|
||||
<div ref={ref}>
|
||||
<SideMenu
|
||||
path={path}
|
||||
onSelect={(info) => {
|
||||
const keyPath = [selectedKey, ...[...info.keyPath].reverse()];
|
||||
const selectedSchema = findPropertyByPath(schema, keyPath);
|
||||
console.log('keyPath', keyPath, selectedSchema);
|
||||
onSelect &&
|
||||
onSelect({ ...info, keyPath, schema: selectedSchema });
|
||||
}}
|
||||
defaultSelectedKeys={defaultSelectedKeys || []}
|
||||
selectedKey={selectedKey}
|
||||
sideMenuRef={sideMenuRef}
|
||||
/>
|
||||
</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}
|
||||
>
|
||||
{schema.title}
|
||||
<DesignableBar />
|
||||
<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}
|
||||
>
|
||||
{schema.title}
|
||||
<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,
|
||||
|
226
packages/client/src/schemas/table/Sortable.tsx
Normal file
226
packages/client/src/schemas/table/Sortable.tsx
Normal 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;
|
||||
}
|
@ -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 (
|
||||
<DisplayedMapProvider>
|
||||
<div className={cls('nb-action-bar', `align-${align}`)}>
|
||||
<div>
|
||||
<Actions align={'left'} />
|
||||
<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 style={{ width: '50%' }}>
|
||||
<Actions align={'left'} />
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', width: '50%', textAlign: 'right' }}>
|
||||
<Actions align={'right'} />
|
||||
</div>
|
||||
<AddActionButton />
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<Actions align={'right'} />
|
||||
</div>
|
||||
<AddActionButton />
|
||||
</div>
|
||||
</DisplayedMapProvider>
|
||||
</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) => {
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
78
packages/client/src/schemas/tabs/style.less
Normal file
78
packages/client/src/schemas/tabs/style.less
Normal 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;
|
||||
}
|
||||
}
|
36
yarn.lock
36
yarn.lock
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user