From cf2d3fcdaf2b59cc319f2865e08986a712f14e48 Mon Sep 17 00:00:00 2001 From: chenos Date: Mon, 2 Aug 2021 15:05:28 +0800 Subject: [PATCH] improve drag and drop --- packages/client/package.json | 1 + .../client/src/components/Sortable/index.tsx | 100 ++++++++ packages/client/src/hooks/useResource.ts | 2 +- packages/client/src/schemas/action/index.md | 90 +++---- packages/client/src/schemas/action/index.tsx | 78 +++--- packages/client/src/schemas/add-new/index.tsx | 168 +++++++------ packages/client/src/schemas/menu/index.tsx | 147 +++++++---- .../client/src/schemas/table/Sortable.tsx | 226 +++++++++++++++++ packages/client/src/schemas/table/index.tsx | 109 ++++++--- packages/client/src/schemas/tabs/index.md | 9 + packages/client/src/schemas/tabs/index.tsx | 230 +++++++++++++++++- packages/client/src/schemas/tabs/style.less | 78 ++++++ yarn.lock | 36 ++- 13 files changed, 1023 insertions(+), 251 deletions(-) create mode 100644 packages/client/src/components/Sortable/index.tsx create mode 100644 packages/client/src/schemas/table/Sortable.tsx create mode 100644 packages/client/src/schemas/tabs/style.less diff --git a/packages/client/package.json b/packages/client/package.json index 183167a4b..c9f977b72 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -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", diff --git a/packages/client/src/components/Sortable/index.tsx b/packages/client/src/components/Sortable/index.tsx new file mode 100644 index 000000000..ac9c84038 --- /dev/null +++ b/packages/client/src/components/Sortable/index.tsx @@ -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((props, ref) => { + return
; +}); + +export interface SortableItemProps {} + +export const SortableItemContext = createContext({}); + +export function DragHandle() { + return ( + + {({ setDraggableNodeRef, attributes, listeners }) => + setDraggableNodeRef && ( + + ) + } + + ); +} + +export function SortableItem(props) { + const { id, data = {}, className, component, children, ...others } = props; + const previewRef = useRef(); + 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 ( + { + previewRef.current = el; + setDroppableNodeRef(el); + }} + > + + {children} + + + ); +} diff --git a/packages/client/src/hooks/useResource.ts b/packages/client/src/hooks/useResource.ts index 5eb7f4330..baec3af24 100644 --- a/packages/client/src/hooks/useResource.ts +++ b/packages/client/src/hooks/useResource.ts @@ -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, diff --git a/packages/client/src/schemas/action/index.md b/packages/client/src/schemas/action/index.md index 4d0204914..1df7c9dca 100644 --- a/packages/client/src/schemas/action/index.md +++ b/packages/client/src/schemas/action/index.md @@ -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', - }, - }, - }, - }, - }, + } }, }; diff --git a/packages/client/src/schemas/action/index.tsx b/packages/client/src/schemas/action/index.tsx index 33c773515..3590f3bed 100644 --- a/packages/client/src/schemas/action/index.tsx +++ b/packages/client/src/schemas/action/index.tsx @@ -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 = ( + + ); return ( - - {!isDropdownOrPopover && ( - - )} - - + + + {!isDropdownOrPopover && button} + + + ); }); @@ -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 ( } > - + {button} ); }); 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 ( {
} > - + {button} ); }); -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 (
@@ -297,7 +279,7 @@ Action.DesignableBar = () => { { - // console.log({ field, schema }); + console.log('按钮文案被修改了', { schema }); schema.title = '按钮文案被修改了'; // field.setTitle('按钮文案被修改了'); // setVisible(false); diff --git a/packages/client/src/schemas/add-new/index.tsx b/packages/client/src/schemas/add-new/index.tsx index 399ef56a8..0c392e63f 100644 --- a/packages/client/src/schemas/add-new/index.tsx +++ b/packages/client/src/schemas/add-new/index.tsx @@ -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} > - + {collections?.map((item) => ( { sideMenuElement.style.display = isSubMenu ? 'block' : 'none'; }, [selectedKey]); + const [dragOverlayContent, setDragOverlayContent] = useState(''); + return ( - - { - if (mode === 'mix') { - setSelectedKey(info.key); - } - const selectedSchema = schema.properties[info.key]; - console.log({ selectedSchema }); - onSelect && onSelect({ ...info, schema: selectedSchema }); - }} - > - - - {/* */} - - - - {mode === 'mix' && ( -
- { - 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} - /> -
+ { + setDragOverlayContent(event.active.data?.current?.title || ''); + }}> + {createPortal( + + {dragOverlayContent} + , + document.body, )} -
+ + { + if (mode === 'mix') { + setSelectedKey(info.key); + } + const selectedSchema = schema.properties[info.key]; + console.log({ selectedSchema }); + onSelect && onSelect({ ...info, schema: selectedSchema }); + }} + > + + + {/* */} + + + + {mode === 'mix' && ( +
+ { + 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} + /> +
+ )} +
+ ); }); @@ -188,12 +213,22 @@ Menu.Item = observer((props: any) => { return ( } + icon={null} eventKey={schema.name} key={schema.name} > - {schema.title} - + + {icon && ( + + )} + {schema.title} + + ); }); @@ -204,11 +239,22 @@ Menu.Link = observer((props: any) => { return ( } + icon={null} eventKey={schema.name} key={schema.name} > - {schema.title} + + {icon && ( + + )} + {schema.title} + + {/* {schema.title} */} @@ -553,6 +599,7 @@ Menu.DesignableBar = (props) => { }} className={'designable-bar-actions'} > + + + {props.children} + + {/* + Drag + */} +
+ ); +} + +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 ( + + + { + console.log(child.key, 'child.key'); + return `td${child.key}`; + })} + > + {React.Children.map(props.children, (child, index) => ( + + {child} + + ))} + + + + ); +} + +export function SortableHeaderRow(props) { + const [dragOverlayContent, setDragOverlayContent] = useState(''); + return ( + + { + const previewRef = event.active.data?.current?.previewRef as { + current: HTMLElement; + }; + if (previewRef?.current) { + setDragOverlayContent(previewRef.current.textContent || ''); + } + }} + > + {createPortal( + + {dragOverlayContent} + , + document.body, + )} + {React.Children.map(props.children, (child, index) => ( + {child} + ))} + + + ); +} + +export function SortableHeaderCell(props) { + const id = useContext(CellContext); + if (['RC_TABLE_KEY', 'addnew'].includes(id)) { + return ; + } + return ( + ((props, ref) => ( + + ))} + /> + ); +} + +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 ( + +
+ + Drag + +
+ {props.children} + + ); +} + +export function SortableRowHandle(props) { + const { className, ...others } = props; + const { setDraggableNodeRef, attributes, listeners } = + useContext(RowDraggableContext); + return setDraggableNodeRef ? ( + + ) : null; +} diff --git a/packages/client/src/schemas/table/index.tsx b/packages/client/src/schemas/table/index.tsx index d438ecc8d..978350550 100644 --- a/packages/client/src/schemas/table/index.tsx +++ b/packages/client/src/schemas/table/index.tsx @@ -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: () => ( @@ -402,6 +416,7 @@ const TableMain = () => { const columns = useTableColumns(); const dataSource = useDataSource(); const actionBars = useTableActionBars(); + const [html, setHtml] = useState(''); return (
{ } /> ))} - + {}} @@ -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 ( + // + // + //
+ // + // {props.children} + // + // ); + // }, + row: SortableBodyRow, + // cell: SortableBodyCell, }, }} rowSelection={{ @@ -749,7 +790,11 @@ function Actions(props: any) { if (currentAlign !== align) { return null; } - return ; + return ( + + + + ); })} ); @@ -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 ( - -
-
- + { + setDragOverlayContent(event.active.data?.current?.title || ''); + // const previewRef = event.active.data?.current?.previewRef; + // if (previewRef) { + // setDragOverlayContent(previewRef?.current?.innerHTML); + // } else { + // setDragOverlayContent(''); + // } + }}> + + {dragOverlayContent} + {/*
*/} +
+ +
+
+ +
+
+ +
+
-
- -
- -
- + + ); }); @@ -922,6 +980,7 @@ Table.OperationDesignableBar = () => { }} className={cls('designable-bar-actions', { active: visible })} > + { }} className={cls('designable-bar-actions', { active: visible })} > + { const { schema, remove, refresh, insertAfter } = useDesignable(); const [visible, setVisible] = useState(false); const displayed = useDisplayedMapContext(); + const ctx = useContext(ColDraggableContext); return (
{ }} className={cls('designable-bar-actions', { active: visible })} > + { }); Table.SortHandle = observer((props: any) => { - const listeners = useContext(SyntheticListenerMapContext); - const { className, ...others } = props; - - return ( - - ); + return ; }); Table.DesignableBar = observer((props) => { diff --git a/packages/client/src/schemas/tabs/index.md b/packages/client/src/schemas/tabs/index.md index 92dbfc4c2..d3e286b84 100644 --- a/packages/client/src/schemas/tabs/index.md +++ b/packages/client/src/schemas/tabs/index.md @@ -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', }, diff --git a/packages/client/src/schemas/tabs/index.tsx b/packages/client/src/schemas/tabs/index.tsx index d26c971c7..10840d295 100644 --- a/packages/client/src/schemas/tabs/index.tsx +++ b/packages/client/src/schemas/tabs/index.tsx @@ -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 ( +
+ + {tabs.map(({ props, schema, name }, key) => ( + + ))} +
+ ); + } + + return ( +
+ + { + setDragOverlayContent(event.active.data?.current?.title || ''); + }} + > + + {dragOverlayContent} + + } + 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); + }} + > + 新增标签页 + + } + > + {tabs.map(({ props, schema, name }, key) => ( + + } + key={key} + > + + + ))} + + +
+ ); +}); + +Tabs.TabPane = observer((props: any) => { + const { schema, DesignableBar } = useDesignable(); + return ( + +
+ {schema.title} +
+
+ ); +}); + +Tabs.DesignableBar = () => { + const { schema, remove, refresh, insertAfter } = useDesignable(); + const [visible, setVisible] = useState(false); + const field = useField(); + return ( +
+ { + e.stopPropagation(); + }} + className={cls('designable-bar-actions', { active: visible })} + > + { + setVisible(visible); + }} + overlay={ + + { + const singleton = !field.componentProps.singleton; + schema['x-component-props'] = + schema['x-component-props'] || {}; + schema['x-component-props'].singleton = singleton; + field.componentProps.singleton = singleton; + }} + > + 禁用标签页 {' '} + + + + } + > + + + +
+ ); +}; + +Tabs.TabPane.DesignableBar = () => { + const { schema, remove, refresh, insertAfter } = useDesignable(); + const [visible, setVisible] = useState(false); + + return ( +
+ { + e.stopPropagation(); + }} + className={cls('designable-bar-actions', { active: visible })} + > + {' '} + { + setVisible(visible); + }} + overlay={ + + { + schema.title = uid(); + schema['x-component-props'] = + schema['x-component-props'] || {}; + schema['x-component-props']['tab'] = uid(); + refresh(); + }} + > + 修改名称和图标 + + + { + const data = remove(); + await removeSchema(data); + setVisible(false); + }} + > + 删除 + + + } + > + + + +
+ ); +}; diff --git a/packages/client/src/schemas/tabs/style.less b/packages/client/src/schemas/tabs/style.less new file mode 100644 index 000000000..23bcf96ad --- /dev/null +++ b/packages/client/src/schemas/tabs/style.less @@ -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; + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b099f901b..a2b8f670b 100644 --- a/yarn.lock +++ b/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==