From 8a7a788d4ee622fb2bc67c5a22cf9ef411b08b47 Mon Sep 17 00:00:00 2001 From: janrywang Date: Wed, 16 Jun 2021 22:12:42 +0800 Subject: [PATCH] feat(designable-antd): add DesignableArrayCards --- designable/antd/package.json | 9 +- designable/antd/playground/template.ejs | 6 + designable/antd/playground/webpack.base.ts | 7 + .../components/DesignableArrayCards/index.tsx | 242 ++++++++++ .../components/DesignableArrayTable/index.tsx | 414 ++++++++++-------- .../src/components/DesignableField/index.tsx | 69 ++- .../src/components/DesignableField/options.ts | 68 ++- .../src/components/DesignableField/types.ts | 18 +- .../DesignableFormCollapse/index.tsx | 47 +- .../components/DesignableFormTab/index.tsx | 47 +- designable/antd/src/hooks/index.ts | 1 + designable/antd/src/hooks/useDropTemplate.ts | 20 + designable/antd/src/locales/zh-CN.ts | 31 +- designable/antd/src/schemas/ArrayCards.ts | 3 + designable/antd/src/schemas/index.ts | 1 + designable/antd/src/shared.ts | 92 ++++ designable/antd/src/sources/arrays.ts | 8 + yarn.lock | 46 +- 18 files changed, 770 insertions(+), 359 deletions(-) create mode 100644 designable/antd/src/hooks/index.ts create mode 100644 designable/antd/src/hooks/useDropTemplate.ts create mode 100644 designable/antd/src/schemas/ArrayCards.ts create mode 100644 designable/antd/src/shared.ts diff --git a/designable/antd/package.json b/designable/antd/package.json index b6a955b9efd..643970a0a01 100644 --- a/designable/antd/package.json +++ b/designable/antd/package.json @@ -30,7 +30,7 @@ "start": "webpack-dev-server --config playground/webpack.dev.ts" }, "devDependencies": { - "@designable/react-settings-form": "^0.1.48", + "@designable/react-settings-form": "^0.2.0", "file-loader": "^5.0.2", "fs-extra": "^8.1.0", "html-webpack-plugin": "^3.2.0", @@ -42,7 +42,8 @@ "webpack": "^4.41.5", "webpack-bundle-analyzer": "^3.9.0", "webpack-cli": "^3.3.10", - "webpack-dev-server": "^3.10.1" + "webpack-dev-server": "^3.10.1", + "autoprefixer": "^9.0" }, "peerDependencies": { "@types/react": ">=16.8.0 || >=17.0.0", @@ -53,8 +54,8 @@ "react-is": ">=16.8.0 || >=17.0.0" }, "dependencies": { - "@designable/core": "^0.1.48", - "@designable/react": "^0.1.48", + "@designable/core": "^0.2.0", + "@designable/react": "^0.2.0", "@formily/antd": "2.0.0-beta.65", "@formily/core": "2.0.0-beta.65", "@formily/react": "2.0.0-beta.65", diff --git a/designable/antd/playground/template.ejs b/designable/antd/playground/template.ejs index d85cc29839c..034b95f4fcf 100644 --- a/designable/antd/playground/template.ejs +++ b/designable/antd/playground/template.ejs @@ -3,6 +3,12 @@ Designable Playground +
diff --git a/designable/antd/playground/webpack.base.ts b/designable/antd/playground/webpack.base.ts index d27c65630b1..e3f4746dff8 100644 --- a/designable/antd/playground/webpack.base.ts +++ b/designable/antd/playground/webpack.base.ts @@ -2,6 +2,7 @@ import path from 'path' import fs from 'fs-extra' import { GlobSync } from 'glob' import MiniCssExtractPlugin from 'mini-css-extract-plugin' +import autoprefixer from 'autoprefixer' //import { getThemeVariables } from 'antd/dist/theme' const getWorkspaceAlias = () => { @@ -70,6 +71,12 @@ export default { use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader' }, + { + loader: 'postcss-loader', + options: { + plugins: () => autoprefixer(), + }, + }, { loader: 'less-loader', options: { diff --git a/designable/antd/src/components/DesignableArrayCards/index.tsx b/designable/antd/src/components/DesignableArrayCards/index.tsx index e69de29bb2d..2e7c18a6243 100644 --- a/designable/antd/src/components/DesignableArrayCards/index.tsx +++ b/designable/antd/src/components/DesignableArrayCards/index.tsx @@ -0,0 +1,242 @@ +import React, { Fragment } from 'react' +import { Card, CardProps } from 'antd' +import { Droppable } from '../Droppable' +import { TreeNode } from '@designable/core' +import { useTreeNode, TreeNodeWidget, useNodeIdProps } from '@designable/react' +import { ArrayBase } from '@formily/antd' +import { observer } from '@formily/react' +import { LoadTemplate } from '../LoadTemplate' +import cls from 'classnames' +import { useDropTemplate } from '../../hooks' +import { + hasNodeByComponentPath, + queryNodesByComponentPath, + createEnsureTypeItemsNode, + findNodeByComponentPath, + createNodeId, +} from '../../shared' + +const ensureVoidItemsNode = createEnsureTypeItemsNode('void') + +const isArrayCardsOperation = (name: string) => + name === 'ArrayCards.Remove' || + name === 'ArrayCards.MoveDown' || + name === 'ArrayCards.MoveUp' + +export const DesignableArrayCards: React.FC = observer((props) => { + const node = useTreeNode() + const nodeId = useNodeIdProps() + const designer = useDropTemplate('ArrayCards', (source) => { + const indexNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayCards.Index', + }, + }) + const additionNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + title: 'Addition', + 'x-component': 'ArrayCards.Addition', + }, + }) + const removeNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + title: 'Addition', + 'x-component': 'ArrayCards.Remove', + }, + }) + const moveDownNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + title: 'Addition', + 'x-component': 'ArrayCards.MoveDown', + }, + }) + const moveUpNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + title: 'Addition', + 'x-component': 'ArrayCards.MoveUp', + }, + }) + + const voidNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + }, + children: [indexNode, ...source, removeNode, moveDownNode, moveUpNode], + }) + return [voidNode, additionNode] + }) + const renderCard = () => { + if (node.children.length === 0) return + const additions = queryNodesByComponentPath(node, [ + 'ArrayCards', + 'ArrayCards.Addition', + ]) + const indexes = queryNodesByComponentPath(node, [ + 'ArrayCards', + '*', + 'ArrayCards.Index', + ]) + const operations = queryNodesByComponentPath(node, [ + 'ArrayCards', + '*', + isArrayCardsOperation, + ]) + const children = queryNodesByComponentPath(node, [ + 'ArrayCards', + '*', + (name) => name.indexOf('ArrayCards.') === -1, + ]) + return ( + + + + {indexes.map((node, key) => ( + + ))} + {props.title || 'Title'} + + } + className={cls('ant-formily-array-cards-item', props.className)} + extra={ + + {operations.map((node) => ( + + ))} + {props.extra} + + } + > +
+ {children.length ? ( + children.map((node) => ( + + )) + ) : ( + + )} +
+
+
+ {additions.map((node) => ( + + ))} +
+ ) + } + + return ( +
+ {renderCard()} + { + if ( + hasNodeByComponentPath(node, [ + 'ArrayCards', + '*', + 'ArrayCards.Index', + ]) + ) + return + const indexNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayCards.Index', + }, + }) + ensureVoidItemsNode(node).appendNode(indexNode) + }, + }, + + { + title: 'addOperation', + onClick: () => { + const oldAdditionNode = findNodeByComponentPath(node, [ + 'ArrayCards', + 'ArrayCards.Addition', + ]) + if (!oldAdditionNode) { + const additionNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + title: 'Addition', + 'x-component': 'ArrayCards.Addition', + }, + }) + ensureVoidItemsNode(node).insertAfter(additionNode) + } + const oldRemoveNode = findNodeByComponentPath(node, [ + 'ArrayCards', + '*', + 'ArrayCards.Remove', + ]) + const oldMoveDownNode = findNodeByComponentPath(node, [ + 'ArrayCards', + '*', + 'ArrayCards.MoveDown', + ]) + const oldMoveUpNode = findNodeByComponentPath(node, [ + 'ArrayCards', + '*', + 'ArrayCards.MoveUp', + ]) + if (!oldRemoveNode) { + ensureVoidItemsNode(node).appendNode( + new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayCards.Remove', + }, + }) + ) + } + if (!oldMoveDownNode) { + ensureVoidItemsNode(node).appendNode( + new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayCards.MoveDown', + }, + }) + ) + } + if (!oldMoveUpNode) { + ensureVoidItemsNode(node).appendNode( + new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayCards.MoveUp', + }, + }) + ) + } + }, + }, + ]} + /> +
+ ) +}) + +ArrayBase.mixin(DesignableArrayCards) diff --git a/designable/antd/src/components/DesignableArrayTable/index.tsx b/designable/antd/src/components/DesignableArrayTable/index.tsx index ebecd853fda..b5c52b4e949 100644 --- a/designable/antd/src/components/DesignableArrayTable/index.tsx +++ b/designable/antd/src/components/DesignableArrayTable/index.tsx @@ -1,146 +1,145 @@ import React from 'react' import { Table, TableProps } from 'antd' import { Droppable } from '../Droppable' -import { TreeNode, AppendNodeEvent } from '@designable/core' -import { - useTreeNode, - useDesigner, - TreeNodeWidget, - useNodeIdProps, -} from '@designable/react' +import { TreeNode } from '@designable/core' +import { useTreeNode, TreeNodeWidget, useNodeIdProps } from '@designable/react' import { ArrayBase } from '@formily/antd' import { observer } from '@formily/react' import { LoadTemplate } from '../LoadTemplate' import cls from 'classnames' +import { + createNodeId, + queryNodesByComponentPath, + hasNodeByComponentPath, + findNodeByComponentPath, + createEnsureTypeItemsNode, +} from '../../shared' +import { useDropTemplate } from '../../hooks' -const parseColumns = (nodes: TreeNode[]) => { - return nodes.filter((node) => { - return node.props['x-component'] === 'ArrayTable.Column' - }) -} - -const parseAdditionComponents = (nodes: TreeNode[]) => { - for (let i = 0; i < nodes.length; i++) { - if (nodes[i].props['x-component'] === 'ArrayTable.Addition') - return - } -} +const ensureObjectItemsNode = createEnsureTypeItemsNode('object') export const DesignableArrayTable: React.FC> = observer( (props) => { const node = useTreeNode() const nodeId = useNodeIdProps() - const designer = useDesigner((designer) => { - return designer.subscribeTo(AppendNodeEvent, (event) => { - const { source, target } = event.data - if (Array.isArray(target)) return - if (!Array.isArray(source)) return - if (target.props['x-component'] === 'ArrayTable') { - if ( - source.every( - (node) => node.props['x-component'] !== 'ArrayTable.Column' - ) - ) { - const sortHandleNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.Column', - }, - children: [ - { - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.SortHandle', - }, - }, - ], - }) - const indexNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.Column', - }, - children: [ - { - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.Index', - }, - }, - ], - }) - const columnNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.Column', - }, - }) - const additionNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - title: 'Addition', - 'x-component': 'ArrayTable.Addition', - }, - }) - const operationNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.Column', - }, - children: [ - { - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.Remove', - }, - }, - { - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.MoveDown', - }, - }, - { - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.MoveUp', - }, - }, - ], - }) - target.appendNode( - sortHandleNode, - indexNode, - columnNode, - additionNode, - operationNode - ) - columnNode.appendNode(...source) - } - } + const designer = useDropTemplate('ArrayTable', (source) => { + const sortHandleNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.Column', + }, + children: [ + { + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.SortHandle', + }, + }, + ], + }) + const indexNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.Column', + }, + children: [ + { + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.Index', + }, + }, + ], + }) + const columnNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.Column', + }, + children: source.map((node) => { + node.props.title = undefined + return node + }), + }) + + const operationNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.Column', + }, + children: [ + { + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.Remove', + }, + }, + { + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.MoveDown', + }, + }, + { + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.MoveUp', + }, + }, + ], + }) + const objectNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'object', + }, + children: [sortHandleNode, indexNode, columnNode, operationNode], + }) + const additionNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + title: 'Addition', + 'x-component': 'ArrayTable.Addition', + }, }) + return [objectNode, additionNode] }) - const columns = parseColumns(node.children) - const addition = parseAdditionComponents(node.children) + const columns = queryNodesByComponentPath(node, [ + 'ArrayTable', + '*', + 'ArrayTable.Column', + ]) + const additions = queryNodesByComponentPath(node, [ + 'ArrayTable', + 'ArrayTable.Addition', + ]) const defaultRowKey = () => { return node.id } - const getColumnId = (props: any) => { - return { - [designer.props.nodeIdAttrName]: - props.className.match(/data-id\:([^\s]+)/)?.[1], - } + const createColumnId = (props: any) => { + return createNodeId( + designer, + props.className.match(/data-id\:([^\s]+)/)?.[1] + ) } + + useDropTemplate('ArrayTable.Column', (source) => { + return source.map((node) => { + node.props.title = undefined + return node + }) + }) + return (
@@ -158,7 +157,7 @@ export const DesignableArrayTable: React.FC> = observer( header: { cell: (props: any) => { return ( - + {props.children} ) @@ -167,7 +166,7 @@ export const DesignableArrayTable: React.FC> = observer( body: { cell: (props: any) => { return ( - + {props.children} ) @@ -176,8 +175,8 @@ export const DesignableArrayTable: React.FC> = observer( }} > {columns.map((node, key) => { - const children = node.children.map((child, key) => { - return + const children = node.children.map((child) => { + return }) return ( > = observer( } /> )} - {addition} + {additions.map((child) => { + return + })} > = observer( title: 'addTableSortHandle', onClick: () => { if ( - node.find( - (node) => - node.props['x-component'] === 'ArrayTable.SortHandle' - ) + hasNodeByComponentPath(node, [ + 'ArrayTable', + '*', + 'ArrayTable.Column', + 'ArrayTable.SortHandle', + ]) ) return const tableColumn = new TreeNode({ @@ -230,16 +233,19 @@ export const DesignableArrayTable: React.FC> = observer( }, ], }) - node.prependNode(tableColumn) + ensureObjectItemsNode(node).prependNode(tableColumn) }, }, { - title: 'addTableIndex', + title: 'addIndex', onClick: () => { if ( - node.find( - (node) => node.props['x-component'] === 'ArrayTable.Index' - ) + hasNodeByComponentPath(node, [ + 'ArrayTable', + '*', + 'ArrayTable.Column', + 'ArrayTable.Index', + ]) ) return const tableColumn = new TreeNode({ @@ -258,13 +264,34 @@ export const DesignableArrayTable: React.FC> = observer( }, ], }) - node.prependNode(tableColumn) + const sortNode = findNodeByComponentPath(node, [ + 'ArrayTable', + '*', + 'ArrayTable.Column', + 'ArrayTable.SortHandle', + ]) + if (sortNode) { + sortNode.parent.insertAfter(tableColumn) + } else { + ensureObjectItemsNode(node).prependNode(tableColumn) + } }, }, { title: 'addTableColumn', onClick: () => { - const lastColumn = columns[columns.length - 1] + const operationNpde = findNodeByComponentPath(node, [ + 'ArrayTable', + '*', + 'ArrayTable.Column', + (name) => { + return ( + name === 'ArrayTable.Remove' || + name === 'ArrayTable.MoveDown' || + name === 'ArrayTable.MoveUp' + ) + }, + ]) const tableColumn = new TreeNode({ componentName: 'DesignableField', props: { @@ -272,71 +299,76 @@ export const DesignableArrayTable: React.FC> = observer( 'x-component': 'ArrayTable.Column', }, }) - if ( - lastColumn && - lastColumn.props?.['x-component-props']?.['data-is-operation'] - ) { - lastColumn.insertBefore(tableColumn) + if (operationNpde) { + operationNpde.parent.insertBefore(tableColumn) } else { - node.appendNode(tableColumn) + ensureObjectItemsNode(node).appendNode(tableColumn) } }, }, { title: 'addOperation', onClick: () => { - const additionNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - title: 'Addition', - 'x-component': 'ArrayTable.Addition', + const oldOperationNode = findNodeByComponentPath(node, [ + 'ArrayTable', + '*', + 'ArrayTable.Column', + (name) => { + return ( + name === 'ArrayTable.Remove' || + name === 'ArrayTable.MoveDown' || + name === 'ArrayTable.MoveUp' + ) }, - }) - const operationNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.Column', - 'x-component-props': { - 'data-is-operation': true, + ]) + const oldAdditionNode = findNodeByComponentPath(node, [ + 'ArrayTable', + 'ArrayTable.Addition', + ]) + if (!oldOperationNode) { + const operationNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.Column', }, - }, - children: [ - { - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.Remove', + children: [ + { + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.Remove', + }, }, - }, - { - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.MoveDown', + { + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.MoveDown', + }, }, - }, - { - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'ArrayTable.MoveUp', + { + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'ArrayTable.MoveUp', + }, }, + ], + }) + ensureObjectItemsNode(node).appendNode(operationNode) + } + if (!oldAdditionNode) { + const additionNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + title: 'Addition', + 'x-component': 'ArrayTable.Addition', }, - ], - }) - - if ( - node.find( - (node) => - node.props['x-component'] === 'ArrayTable.Addition' - ) - ) { - node.appendNode(operationNode) - return + }) + ensureObjectItemsNode(node).insertAfter(additionNode) } - node.appendNode(additionNode, operationNode) }, }, ]} diff --git a/designable/antd/src/components/DesignableField/index.tsx b/designable/antd/src/components/DesignableField/index.tsx index 1047515d647..947e88c7111 100644 --- a/designable/antd/src/components/DesignableField/index.tsx +++ b/designable/antd/src/components/DesignableField/index.tsx @@ -17,6 +17,7 @@ import { FormItemSwitcher } from '../FormItemSwitcher' import { DesignableObject } from '../DesignableObject' import { createOptions } from './options' import { IDesignableFieldProps } from './types' +import { includesComponent } from '../../shared' import * as defaultSchemas from '../../schemas' Schema.silent() @@ -128,9 +129,7 @@ export const createDesignableField = (options: IDesignableFieldProps = {}) => { } if (node.props.type === 'void') { - if ( - !realOptions.dropReactionComponents.includes(node.props['x-component']) - ) { + if (!includesComponent(node, realOptions.dropReactionComponents)) { Object.assign(base.properties, { 'x-reactions': { 'x-decorator': 'FormItem', @@ -139,9 +138,7 @@ export const createDesignableField = (options: IDesignableFieldProps = {}) => { }, }) } - if ( - !realOptions.dropFormItemComponents.includes(node.props['x-component']) - ) { + if (!includesComponent(node, realOptions.dropFormItemComponents)) { Object.assign(base.properties, { 'x-decorator': { type: 'string', @@ -163,9 +160,7 @@ export const createDesignableField = (options: IDesignableFieldProps = {}) => { delete base.properties.description } } else { - if ( - !realOptions.dropReactionComponents.includes(node.props['x-component']) - ) { + if (!includesComponent(node, realOptions.dropReactionComponents)) { Object.assign(base.properties, { 'x-reactions': { 'x-decorator': 'FormItem', @@ -210,7 +205,7 @@ export const createDesignableField = (options: IDesignableFieldProps = {}) => { if (restrictChildrenComponents?.length) { if ( source.every((node) => - restrictChildrenComponents.includes(node.props['x-component']) + includesComponent(node, restrictChildrenComponents) ) || target.children.length === 0 ) { @@ -218,25 +213,7 @@ export const createDesignableField = (options: IDesignableFieldProps = {}) => { } return false } - if (target.props['type'] === 'object') return true - if ( - targetComponent && - source.every((node) => { - const restrictParentComponents = - realOptions.restrictParentComponents?.[node.props['x-component']] - if (restrictParentComponents?.length) { - if (restrictParentComponents.includes(targetComponent)) { - return true - } - return false - } - return true - }) - ) { - return true - } - - return false + return true } GlobalRegistry.registerDesignerProps({ @@ -245,22 +222,36 @@ export const createDesignableField = (options: IDesignableFieldProps = {}) => { const message = GlobalRegistry.getDesignerMessage( `components.${componentName}` ) + const isObjectNode = node.props.type === 'object' + const isArrayNode = node.props.type === 'array' + const isVoidNode = node.props.type === 'void' const title = typeof message === 'string' ? message : message?.title const nodeTitle = - node.props.type === 'object' + title || + (isObjectNode ? GlobalRegistry.getDesignerMessage('components.Object') - : title + : isVoidNode + ? GlobalRegistry.getDesignerMessage('components.Void') + : '') + return { title: nodeTitle, - draggable: !realOptions.notDraggableComponents.includes(componentName), - droppable: !realOptions.notDroppableComponents.includes(componentName), + draggable: !includesComponent(node, realOptions.notDraggableComponents), + droppable: + isObjectNode || + isArrayNode || + !includesComponent(node, realOptions.notDroppableComponents), selfRenderChildren: - node.props.type === 'array' || - realOptions.selfRenderChildrenComponents.includes(componentName), - inlineLayout: - realOptions.inlineLayoutComponents.includes(componentName), - inlineChildrenLayout: - realOptions.inlineChildrenLayoutComponents.includes(componentName), + isArrayNode || + includesComponent(node, realOptions.selfRenderChildrenComponents), + inlineLayout: includesComponent( + node, + realOptions.inlineLayoutComponents + ), + inlineChildrenLayout: includesComponent( + node, + realOptions.inlineChildrenLayoutComponents + ), allowAppend(target, source) { return ( (target.props.type === 'void' || diff --git a/designable/antd/src/components/DesignableField/options.ts b/designable/antd/src/components/DesignableField/options.ts index 52c2392af42..a2023fc962e 100644 --- a/designable/antd/src/components/DesignableField/options.ts +++ b/designable/antd/src/components/DesignableField/options.ts @@ -27,6 +27,38 @@ import { createDesignableContainer } from '../DesignableContainer' import { DesignableFormTab } from '../DesignableFormTab' import { DesignableFormCollapse } from '../DesignableFormCollapse' import { DesignableArrayTable } from '../DesignableArrayTable' +import { DesignableArrayCards } from '../DesignableArrayCards' +import { TreeNode } from '@designable/core' + +const isChildrenComponents = + (parentName: string, names?: string[]) => (name: string) => + Array.isArray(names) && names.length > 0 + ? names.some((key) => { + return `${parentName}.${key}` === name + }) + : name.indexOf(`${parentName}.`) > -1 + +const InlineArrayChildren = [ + 'Index', + 'SortHandle', + 'Remove', + 'MoveDown', + 'MoveUp', +] + +const isFormTabChildren = isChildrenComponents('FormTab') +const isFormCollapseChildren = isChildrenComponents('FormCollapse') +const isArrayTableInlineChildren = isChildrenComponents( + 'ArrayTable', + InlineArrayChildren +) +const isArrayCardsInlineChildren = isChildrenComponents( + 'ArrayCards', + InlineArrayChildren +) +const isObjectNode = (name: string, node: TreeNode) => { + return node.props['type'] === 'object' +} export const createOptions = ( options: IDesignableFieldProps = {} @@ -36,26 +68,23 @@ export const createOptions = ( ...options, notDraggableComponents: [ ...(options.notDraggableComponents || []), - 'FormTab.TabPane', - 'FormCollapse.CollapsePanel', + isFormTabChildren, + isFormCollapseChildren, ], notDroppableComponents: [ ...(options.notDroppableComponents || []), - 'ArrayTable.SortHandle', - 'ArrayTable.Index', - 'ArrayTable.Remove', - 'ArrayTable.MoveDown', - 'ArrayTable.MoveUp', + isChildrenComponents('ArrayTable', [...InlineArrayChildren, 'Addition']), + isChildrenComponents('ArrayCards'), ], dropFormItemComponents: [ ...(options.dropFormItemComponents || []), - 'FormTab.TabPane', - 'FormCollapse.CollapsePanel', + isFormTabChildren, + isFormCollapseChildren, ], dropReactionComponents: [ ...(options.dropReactionComponents || []), - 'FormTab.TabPane', - 'FormCollapse.CollapsePanel', + isFormTabChildren, + isFormCollapseChildren, ], selfRenderChildrenComponents: [ ...(options.selfRenderChildrenComponents || []), @@ -70,24 +99,14 @@ export const createOptions = ( ], inlineLayoutComponents: [ ...(options.inlineLayoutComponents || []), - 'ArrayTable.Column', - 'ArrayTable.SortHandle', - 'ArrayTable.Index', - 'ArrayTable.Remove', - 'ArrayTable.MoveDown', - 'ArrayTable.MoveUp', + isArrayTableInlineChildren, + isArrayCardsInlineChildren, ], restrictChildrenComponents: { FormTab: ['FormTab.TabPane'], FormCollapse: ['FormCollapse.CollapsePanel'], - ArrayTable: ['ArrayTable.Column', 'ArrayTable.Addition'], - }, - restrictParentComponents: { - 'FormTab.TabPane': ['FormTab'], - 'FormCollapse.CollapsePanel': ['FormCollapse'], - 'ArrayTable.Column': ['ArrayTable'], - 'ArrayTable.Addition': ['ArrayTable'], + ArrayTable: [isObjectNode, 'ArrayTable.Addition'], }, components: { ...options.components, @@ -97,6 +116,7 @@ export const createOptions = ( FormTab: DesignableFormTab, FormCollapse: DesignableFormCollapse, ArrayTable: DesignableArrayTable, + ArrayCards: DesignableArrayCards, FormItem, DatePicker, Checkbox, diff --git a/designable/antd/src/components/DesignableField/types.ts b/designable/antd/src/components/DesignableField/types.ts index f27eb494f6b..5897aead054 100644 --- a/designable/antd/src/components/DesignableField/types.ts +++ b/designable/antd/src/components/DesignableField/types.ts @@ -1,15 +1,15 @@ import { ISchema } from '@formily/react' +import { ComponentNameMatcher } from '../../shared' export interface IDesignableFieldProps { name?: string components?: Record> componentsPropsSchema?: Record - notDraggableComponents?: string[] - notDroppableComponents?: string[] - dropFormItemComponents?: string[] - dropReactionComponents?: string[] - selfRenderChildrenComponents?: string[] - inlineChildrenLayoutComponents?: string[] - inlineLayoutComponents?: string[] - restrictChildrenComponents?: Record - restrictParentComponents?: Record + notDraggableComponents?: ComponentNameMatcher[] + notDroppableComponents?: ComponentNameMatcher[] + dropFormItemComponents?: ComponentNameMatcher[] + dropReactionComponents?: ComponentNameMatcher[] + selfRenderChildrenComponents?: ComponentNameMatcher[] + inlineChildrenLayoutComponents?: ComponentNameMatcher[] + inlineLayoutComponents?: ComponentNameMatcher[] + restrictChildrenComponents?: Record } diff --git a/designable/antd/src/components/DesignableFormCollapse/index.tsx b/designable/antd/src/components/DesignableFormCollapse/index.tsx index e312d094b40..c15f358a29d 100644 --- a/designable/antd/src/components/DesignableFormCollapse/index.tsx +++ b/designable/antd/src/components/DesignableFormCollapse/index.tsx @@ -2,21 +2,18 @@ import React, { Fragment, useState } from 'react' import { observer } from '@formily/react' import { Collapse } from 'antd' import { CollapseProps, CollapsePanelProps } from 'antd/lib/collapse' -import { - useDesigner, - useTreeNode, - useNodeIdProps, - TreeNodeWidget, -} from '@designable/react' +import { useTreeNode, useNodeIdProps, TreeNodeWidget } from '@designable/react' import { toArr } from '@formily/shared' import { Droppable } from '../Droppable' -import { TreeNode, AppendNodeEvent } from '@designable/core' +import { TreeNode } from '@designable/core' import { LoadTemplate } from '../LoadTemplate' +import { useDropTemplate } from '../../hooks' +import { matchComponent } from '../../shared' const parseCollpase = (parent: TreeNode) => { const tabs: TreeNode[] = [] parent.children.forEach((node) => { - if (node.props['x-component'] === 'FormCollapse.CollapsePanel') { + if (matchComponent(node, 'FormCollapse.CollapsePanel')) { tabs.push(node) } }) @@ -45,30 +42,18 @@ export const DesignableFormCollapse: React.FC & { const [activeKey, setActiveKey] = useState([]) const node = useTreeNode() const nodeId = useNodeIdProps() - const designer = useDesigner((designer) => { - return designer.subscribeTo(AppendNodeEvent, (event) => { - const { source, target } = event.data - if (Array.isArray(target)) return - if (!Array.isArray(source)) return - if (target.props['x-component'] === 'FormCollapse') { - if ( - source.every( - (node) => node.props['x-component'] !== 'FormCollapse.CollapsePanel' - ) - ) { - const paneNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'FormCollapse.CollapsePanel', - }, - }) - target.appendNode(paneNode) - paneNode.appendNode(...source) - setActiveKey(toArr(activeKey).concat(paneNode.id)) - } - } + const designer = useDropTemplate('FormCollapse', (source) => { + const panelNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'FormCollapse.CollapsePanel', + }, + children: source, }) + + setActiveKey(toArr(activeKey).concat(panelNode.id)) + return [panelNode] }) const panels = parseCollpase(node) const renderCollapse = () => { diff --git a/designable/antd/src/components/DesignableFormTab/index.tsx b/designable/antd/src/components/DesignableFormTab/index.tsx index 270e4aea3fe..80ae32399f7 100644 --- a/designable/antd/src/components/DesignableFormTab/index.tsx +++ b/designable/antd/src/components/DesignableFormTab/index.tsx @@ -2,20 +2,17 @@ import React, { Fragment, useState } from 'react' import { observer } from '@formily/react' import { Tabs } from 'antd' import { TabsProps, TabPaneProps } from 'antd/lib/tabs' -import { - useDesigner, - useNodeIdProps, - useTreeNode, - TreeNodeWidget, -} from '@designable/react' +import { useNodeIdProps, useTreeNode, TreeNodeWidget } from '@designable/react' import { Droppable } from '../Droppable' -import { TreeNode, AppendNodeEvent } from '@designable/core' +import { TreeNode } from '@designable/core' import { LoadTemplate } from '../LoadTemplate' +import { useDropTemplate } from '../../hooks' +import { matchComponent } from '../../shared' const parseTabs = (parent: TreeNode) => { const tabs: TreeNode[] = [] parent.children.forEach((node) => { - if (node.props['x-component'] === 'FormTab.TabPane') { + if (matchComponent(node, 'FormTab.TabPane')) { tabs.push(node) } }) @@ -34,29 +31,17 @@ export const DesignableFormTab: React.FC & { const [activeKey, setActiveKey] = useState() const nodeId = useNodeIdProps() const node = useTreeNode() - const designer = useDesigner((designer) => { - return designer.subscribeTo(AppendNodeEvent, (event) => { - const { source, target } = event.data - if (Array.isArray(target)) return - if (!Array.isArray(source)) return - if (target.props['x-component'] === 'FormTab') { - if ( - source.every( - (node) => node.props['x-component'] !== 'FormTab.TabPane' - ) - ) { - const paneNode = new TreeNode({ - componentName: 'DesignableField', - props: { - type: 'void', - 'x-component': 'FormTab.TabPane', - }, - }) - target.appendNode(paneNode) - paneNode.appendNode(...source) - } - } - }) + const designer = useDropTemplate('FormTab', (source) => { + return [ + new TreeNode({ + componentName: 'DesignableField', + props: { + type: 'void', + 'x-component': 'FormTab.TabPane', + }, + children: source, + }), + ] }) const tabs = parseTabs(node) const renderTabs = () => { diff --git a/designable/antd/src/hooks/index.ts b/designable/antd/src/hooks/index.ts new file mode 100644 index 00000000000..6ab5860beb4 --- /dev/null +++ b/designable/antd/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useDropTemplate' diff --git a/designable/antd/src/hooks/useDropTemplate.ts b/designable/antd/src/hooks/useDropTemplate.ts new file mode 100644 index 00000000000..5302956d282 --- /dev/null +++ b/designable/antd/src/hooks/useDropTemplate.ts @@ -0,0 +1,20 @@ +import { AppendNodeEvent, TreeNode } from '@designable/core' +import { useDesigner } from '@designable/react' +import { matchComponent } from '../shared' + +export const useDropTemplate = ( + name: string, + getChildren: (source: TreeNode[]) => TreeNode[] +) => { + return useDesigner((designer) => { + return designer.subscribeTo(AppendNodeEvent, (event) => { + const { source, target } = event.data + if (Array.isArray(target)) return + if (!Array.isArray(source)) return + if (matchComponent(target, name) && target.children.length === 0) { + target.setNodeChildren(...getChildren(source)) + return false + } + }) + }) +} diff --git a/designable/antd/src/locales/zh-CN.ts b/designable/antd/src/locales/zh-CN.ts index 97d69f6b3fa..9697f36cb8c 100644 --- a/designable/antd/src/locales/zh-CN.ts +++ b/designable/antd/src/locales/zh-CN.ts @@ -367,6 +367,15 @@ const FieldLocale = { }, } +const ArrayOperationsLocale = { + Index: '索引', + SortHandle: '排序手柄', + Addition: '新增按钮', + Remove: '删除按钮', + MoveDown: '下移按钮', + MoveUp: '上移按钮', +} + export default { 'zh-CN': { components: { @@ -398,15 +407,23 @@ export default { FormTab: { title: '选项卡布局', TabPane: '选项卡面板' }, FormCollapse: { title: '手风琴布局', CollapsePanel: '手风琴面板' }, Object: '数据对象', + Void: '虚拟容器', ArrayTable: { title: '自增表格', Column: '表格列', - Index: '索引', - SortHandle: '排序手柄', - Addition: '新增按钮', - Remove: '删除按钮', - MoveDown: '下移按钮', - MoveUp: '上移按钮', + ...ArrayOperationsLocale, + }, + ArrayCards: { + title: '自增卡片', + ...ArrayOperationsLocale, + }, + ArrayTabs: { + title: '自增选项卡', + ...ArrayOperationsLocale, + }, + ArrayCollapse: { + title: '自增手风琴', + ...ArrayOperationsLocale, }, FormItem: '表单项容器', }, @@ -419,7 +436,7 @@ export default { addCollapsePanel: '添加手风琴卡片', addTableColumn: '添加表格列', addTableSortHandle: '添加排序', - addTableIndex: '添加索引', + addIndex: '添加索引', addOperation: '添加操作', }, } diff --git a/designable/antd/src/schemas/ArrayCards.ts b/designable/antd/src/schemas/ArrayCards.ts new file mode 100644 index 00000000000..e478031135d --- /dev/null +++ b/designable/antd/src/schemas/ArrayCards.ts @@ -0,0 +1,3 @@ +import { Card } from './Card' + +export const ArrayCards = Card diff --git a/designable/antd/src/schemas/index.ts b/designable/antd/src/schemas/index.ts index ac1a5248057..3d6ba7ece0b 100644 --- a/designable/antd/src/schemas/index.ts +++ b/designable/antd/src/schemas/index.ts @@ -23,3 +23,4 @@ export * from './Space' export * from './FormTab' export * from './FormCollapse' export * from './ArrayTable' +export * from './ArrayCards' diff --git a/designable/antd/src/shared.ts b/designable/antd/src/shared.ts new file mode 100644 index 00000000000..7746aeff2f9 --- /dev/null +++ b/designable/antd/src/shared.ts @@ -0,0 +1,92 @@ +import { TreeNode, Engine } from '@designable/core' + +export type ComponentNameMatcher = + | string + | string[] + | ((name: string, node: TreeNode) => boolean) + +export const matchComponent = (node: TreeNode, name: ComponentNameMatcher) => { + if (name === '*') return true + const componentName = node?.props?.['x-component'] + if (typeof name === 'function') return name(componentName || '', node) + if (Array.isArray(name)) return name.includes(componentName) + return componentName === name +} + +export const includesComponent = ( + node: TreeNode, + names: ComponentNameMatcher[] +) => { + return names.some((name) => matchComponent(node, name)) +} + +export const queryNodesByComponentPath = ( + node: TreeNode, + path: ComponentNameMatcher[] +): TreeNode[] => { + if (path?.length === 0) return [] + if (path?.length === 1) { + if (matchComponent(node, path[0])) { + return [node] + } + } + return matchComponent(node, path[0]) + ? node.children.reduce((buf, child) => { + return buf.concat(queryNodesByComponentPath(child, path.slice(1))) + }, []) + : [] +} + +export const findNodeByComponentPath = ( + node: TreeNode, + path: ComponentNameMatcher[] +): TreeNode => { + if (path?.length === 0) return + if (path?.length === 1) { + if (matchComponent(node, path[0])) { + return node + } + } + if (matchComponent(node, path[0])) { + for (let i = 0; i < node.children.length; i++) { + const next = findNodeByComponentPath(node.children[i], path.slice(1)) + if (next) { + return next + } + } + } +} + +export const hasNodeByComponentPath = ( + node: TreeNode, + path: ComponentNameMatcher[] +) => !!findNodeByComponentPath(node, path) + +export const matchArrayItemsNode = (node: TreeNode) => { + return ( + node?.parent?.props?.type === 'array' && + node?.parent?.children?.[0] === node + ) +} + +export const createNodeId = (designer: Engine, id: string) => { + return { + [designer.props.nodeIdAttrName]: id, + } +} + +export const createEnsureTypeItemsNode = (type: string) => (node: TreeNode) => { + const objectNode = node.children.find((child) => child.props['type'] === type) + if (objectNode && objectNode.designerProps.droppable) { + return objectNode + } else { + const newObjectNode = new TreeNode({ + componentName: 'DesignableField', + props: { + type, + }, + }) + node.prependNode(newObjectNode) + return newObjectNode + } +} diff --git a/designable/antd/src/sources/arrays.ts b/designable/antd/src/sources/arrays.ts index a6030341191..526e6fe8c2e 100644 --- a/designable/antd/src/sources/arrays.ts +++ b/designable/antd/src/sources/arrays.ts @@ -9,4 +9,12 @@ GlobalDragSource.appendSourcesByGroup('arrays', [ 'x-component': 'ArrayTable', }, }, + { + componentName: 'DesignableField', + props: { + type: 'array', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayCards', + }, + }, ]) diff --git a/yarn.lock b/yarn.lock index a77a2727e44..d4820ca4cfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1162,12 +1162,12 @@ resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz#c3c5ae543c897caa9c2a68630bed355be5f9990f" integrity sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ== -"@designable/core@0.1.48", "@designable/core@^0.1.48": - version "0.1.48" - resolved "https://registry.yarnpkg.com/@designable/core/-/core-0.1.48.tgz#889ecf2977a5a9676116969fad99068dc43e0e16" - integrity sha512-gt241zGAdboYvCrAZ4Oh0sq3xu4m14UkEwVKP1nBTZL0QxMCoZC0VMUeKVOJKIa/V6ML7c5sTBwrPF981lJxWA== +"@designable/core@0.2.0", "@designable/core@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@designable/core/-/core-0.2.0.tgz#dca2e8e32876f9e93cd61a4982795a8661f76dae" + integrity sha512-RRWlm2BH6AQ4Lltwu88ACzp/GOfuqM0/0CrCTTD0efoeE0Zw+dlaMEMUGsMplNPI0njQoNJzEdFk+r4iByJKPQ== dependencies: - "@designable/shared" "0.1.48" + "@designable/shared" "0.2.0" "@formily/json-schema" "^2.0.0-beta.50" "@formily/path" "^2.0.0-beta.50" "@formily/reactive" "^2.0.0-beta.50" @@ -1183,14 +1183,14 @@ "@formily/reactive" "^2.0.0-beta.50" "@juggle/resize-observer" "^3.3.1" -"@designable/react-settings-form@^0.1.48": - version "0.1.48" - resolved "https://registry.yarnpkg.com/@designable/react-settings-form/-/react-settings-form-0.1.48.tgz#a2e944ae333ceac44b6f053fa31142ed7166941b" - integrity sha512-8WX6szjICWQSeG5dlDpdggrxJ36NlQyljSFba4iwuCABeOszf6IDlmRuJREjW8Nl1o/IRwfqf7+JgfzL5WYKDg== +"@designable/react-settings-form@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@designable/react-settings-form/-/react-settings-form-0.2.0.tgz#5acc698c8729c2e0c582855cb9e5b62130355ab8" + integrity sha512-2gMfq6LfMroNKDYti6+OpcHOWdhErm4i97YFzXQ2h4DtY5IJjJjFZ5MnBMVyMKMnP2g9Rxt9Ng+HcNYn5IJLQg== dependencies: - "@designable/core" "0.1.48" - "@designable/react" "0.1.48" - "@designable/shared" "0.1.48" + "@designable/core" "0.2.0" + "@designable/react" "0.2.0" + "@designable/shared" "0.2.0" "@formily/antd" "^2.0.0-beta.50" "@formily/core" "^2.0.0-beta.50" "@formily/react" "^2.0.0-beta.50" @@ -1198,13 +1198,13 @@ "@formily/reactive-react" "^2.0.0-beta.50" react-color "^2.19.3" -"@designable/react@0.1.48", "@designable/react@^0.1.48": - version "0.1.48" - resolved "https://registry.yarnpkg.com/@designable/react/-/react-0.1.48.tgz#123eb4dd41aee3811d51de9ccbda63285dd2bc7a" - integrity sha512-bIjsBjm9TxGPkGraEwaUGfRgBsjpsLeC25uJ0gkCKsm9+Qlu0K9fHN11rktF7Q7Zw635MQpPzsMo8oZeqOFPiQ== +"@designable/react@0.2.0", "@designable/react@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@designable/react/-/react-0.2.0.tgz#8ad67ee2cd9182dc3fc1284f45638c07f2ee14ae" + integrity sha512-q3WkWJbUcrCQwILRzEtfdBhBrdCLDRBu6VuYR3xU6dQbVui1cVMVB/EzwYbVdssRZhiFbNAgkptzZi0p4VESvg== dependencies: - "@designable/core" "0.1.48" - "@designable/shared" "0.1.48" + "@designable/core" "0.2.0" + "@designable/shared" "0.2.0" "@formily/reactive" "^2.0.0-beta.50" "@formily/reactive-react" "^2.0.0-beta.50" "@juggle/resize-observer" "^3.3.1" @@ -1216,10 +1216,10 @@ dependencies: requestidlecallback "^0.3.0" -"@designable/shared@0.1.48": - version "0.1.48" - resolved "https://registry.yarnpkg.com/@designable/shared/-/shared-0.1.48.tgz#ae817718aa30cd6816dc4e3915467cf0e1cb2888" - integrity sha512-3xSpgW2zNkfkxUdLOQRtv/3Ns1qUwQDLbfPTVZwO6B6NYiK2HKvmPecBTjA3BwaU5c7FN4kFvzzTVcN1HEgjAQ== +"@designable/shared@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@designable/shared/-/shared-0.2.0.tgz#2d0d498ce0ed8aeb1143b10dd955d5ab2dd4f539" + integrity sha512-7X+qW1F/N8IBx3QkVYniRYVBW1qmhOY+x2JsM39Cr/VmJ/lfIRcDEYfXz0GqcuKJHyio/b0CeUTQc7TDy3Un1A== dependencies: requestidlecallback "^0.3.0" @@ -4343,7 +4343,7 @@ autocomplete.js@0.36.0: dependencies: immediate "^3.2.3" -autoprefixer@^9.5.1, autoprefixer@^9.6.1: +autoprefixer@^9.0, autoprefixer@^9.5.1, autoprefixer@^9.6.1: version "9.8.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==