From 0215c27bd3bf4136c2dccdb2be49718a7f40f90c Mon Sep 17 00:00:00 2001 From: "zhili.wzl" Date: Fri, 3 Dec 2021 03:00:50 +0800 Subject: [PATCH] feat: support resizable to designerProps --- .../antd/src/components/FormGrid/preview.tsx | 24 ++++ .../next/src/components/FormGrid/preview.tsx | 20 +++ packages/core/src/effects/index.ts | 1 + .../core/src/effects/useDragDropEffect.ts | 8 +- packages/core/src/effects/useResizeEffect.ts | 119 ++++++++++++++++++ .../core/src/effects/useSelectionEffect.ts | 2 +- packages/core/src/models/Cursor.ts | 21 +++- packages/core/src/models/Engine.ts | 2 +- packages/core/src/models/TreeNode.ts | 11 +- packages/core/src/presets.ts | 2 + packages/core/src/types.ts | 26 +++- .../simulators/ResponsiveSimulator/index.tsx | 10 +- .../src/widgets/AuxToolWidget/Helpers.tsx | 4 - .../widgets/AuxToolWidget/ResizeHandler.tsx | 24 ++-- .../src/widgets/AuxToolWidget/Selection.tsx | 8 +- .../react/src/widgets/AuxToolWidget/index.tsx | 44 ------- .../src/widgets/AuxToolWidget/styles.less | 8 +- 17 files changed, 246 insertions(+), 88 deletions(-) diff --git a/formily/antd/src/components/FormGrid/preview.tsx b/formily/antd/src/components/FormGrid/preview.tsx index c39b8d194..ac8037e62 100644 --- a/formily/antd/src/components/FormGrid/preview.tsx +++ b/formily/antd/src/components/FormGrid/preview.tsx @@ -86,6 +86,30 @@ FormGrid.Behavior = createBehavior( selector: (node) => node.props['x-component'] === 'FormGrid.GridColumn', designerProps: { droppable: true, + resizable: { + step: 30, + width(node) { + const span = Number(node.props['x-component-props']?.gridSpan ?? 1) + return { + plus: () => { + if (span + 1 > 12) return + node.props['x-component-props'] = + node.props['x-component-props'] || {} + node.props['x-component-props'].gridSpan = span + 1 + }, + minus: () => { + if (span - 1 < 1) return + node.props['x-component-props'] = + node.props['x-component-props'] || {} + node.props['x-component-props'].gridSpan = span - 1 + }, + } + }, + }, + resizeXPath: 'x-component-props.gridSpan', + resizeStep: 1, + resizeMin: 1, + resizeMax: 12, allowDrop: (node) => node.props['x-component'] === 'FormGrid', propsSchema: createFieldSchema(AllSchemas.FormGrid.GridColumn), }, diff --git a/formily/next/src/components/FormGrid/preview.tsx b/formily/next/src/components/FormGrid/preview.tsx index 0fc49b62b..9d63179a0 100644 --- a/formily/next/src/components/FormGrid/preview.tsx +++ b/formily/next/src/components/FormGrid/preview.tsx @@ -86,6 +86,26 @@ FormGrid.Behavior = createBehavior( selector: (node) => node.props['x-component'] === 'FormGrid.GridColumn', designerProps: { droppable: true, + resizable: { + step: 30, + width(node) { + const span = Number(node.props['x-component-props']?.gridSpan ?? 1) + return { + plus: () => { + if (span + 1 > 12) return + node.props['x-component-props'] = + node.props['x-component-props'] || {} + node.props['x-component-props'].gridSpan = span + 1 + }, + minus: () => { + if (span - 1 < 1) return + node.props['x-component-props'] = + node.props['x-component-props'] || {} + node.props['x-component-props'].gridSpan = span - 1 + }, + } + }, + }, allowDrop: (node) => node.props['x-component'] === 'FormGrid', propsSchema: createFieldSchema(AllSchemas.FormGrid.GridColumn), }, diff --git a/packages/core/src/effects/index.ts b/packages/core/src/effects/index.ts index ef0c48816..dbf2377cc 100644 --- a/packages/core/src/effects/index.ts +++ b/packages/core/src/effects/index.ts @@ -1,6 +1,7 @@ export * from './useCursorEffect' export * from './useViewportEffect' export * from './useDragDropEffect' +export * from './useResizeEffect' export * from './useSelectionEffect' export * from './useFreeSelectionEffect' export * from './useKeyboardEffect' diff --git a/packages/core/src/effects/useDragDropEffect.ts b/packages/core/src/effects/useDragDropEffect.ts index ddc8b4444..fdee06eae 100644 --- a/packages/core/src/effects/useDragDropEffect.ts +++ b/packages/core/src/effects/useDragDropEffect.ts @@ -19,11 +19,13 @@ export const useDragDropEffect = (engine: Engine) => { const handler = target?.closest( `*[${engine.props.nodeDragHandlerAttrName}]` ) - const helper = handler?.closest(`*[${engine.props.nodeHelpersIdAttrName}]`) + const helper = handler?.closest( + `*[${engine.props.nodeSelectionIdAttrName}]` + ) if (!el?.getAttribute && !handler) return const sourceId = el?.getAttribute(engine.props.sourceIdAttrName) const outlineId = el?.getAttribute(engine.props.outlineNodeIdAttrName) - const handlerId = helper?.getAttribute(engine.props.nodeHelpersIdAttrName) + const handlerId = helper?.getAttribute(engine.props.nodeSelectionIdAttrName) const nodeId = el?.getAttribute(engine.props.nodeIdAttrName) engine.workbench.eachWorkspace((currentWorkspace) => { const operation = currentWorkspace.operation @@ -50,6 +52,7 @@ export const useDragDropEffect = (engine: Engine) => { } } }) + engine.cursor.setStyle('move') }) engine.subscribeTo(DragMoveEvent, (event) => { @@ -159,5 +162,6 @@ export const useDragDropEffect = (engine: Engine) => { } operation.dragClean() }) + engine.cursor.setStyle('') }) } diff --git a/packages/core/src/effects/useResizeEffect.ts b/packages/core/src/effects/useResizeEffect.ts index e69de29bb..47f4e467d 100644 --- a/packages/core/src/effects/useResizeEffect.ts +++ b/packages/core/src/effects/useResizeEffect.ts @@ -0,0 +1,119 @@ +import { Engine, CursorType } from '../models' +import { DragStartEvent, DragMoveEvent, DragStopEvent } from '../events' +import { TreeNode } from '../models' +import { Point } from '@designable/shared' + +type ResizeData = { + element?: Element + node?: TreeNode + axis?: 'x' | 'y' | (string & {}) + type?: 'x-start' | 'x-end' | 'y-start' | 'y-end' | (string & {}) + start?: Point + point?: Point + xIndex?: number + yIndex?: number +} + +type ResizeStore = { + value?: ResizeData +} + +export const useResizeEffect = (engine: Engine) => { + const findStartNodeHandler = (target: HTMLElement): ResizeData => { + const handler = target?.closest( + `*[${engine.props.nodeResizeHandlerAttrName}]` + ) + if (handler) { + const type = handler.getAttribute(engine.props.nodeResizeHandlerAttrName) + if (type) { + const element = handler.closest( + `*[${engine.props.nodeSelectionIdAttrName}]` + ) + if (element) { + const nodeId = element.getAttribute( + engine.props.nodeSelectionIdAttrName + ) + if (nodeId) { + const node = engine.findNodeById(nodeId) + if (node) { + const axis = type.includes('x') ? 'x' : 'y' + return { axis, type, node, element, xIndex: 0, yIndex: 0 } + } + } + } + } + } + return + } + + const store: ResizeStore = {} + + engine.subscribeTo(DragStartEvent, (event) => { + if (engine.cursor.type !== CursorType.Move) return + const target = event.data.target as HTMLElement + const data = findStartNodeHandler(target) + if (data) { + const start = new Point(event.data.clientX, event.data.clientY) + store.value = { + ...data, + start, + point: start, + } + if (data.axis === 'x') { + engine.cursor.setStyle('ew-resize') + } else if (data.axis === 'y') { + engine.cursor.setStyle('ns-resize') + } + } + }) + + engine.subscribeTo(DragMoveEvent, (event) => { + if (engine.cursor.type !== CursorType.Move) return + if (store.value) { + const { axis, type, node, element, point, start } = store.value + const allowResize = node.allowResize() + if (!allowResize) return + const resizable = node.designerProps.resizable + const step = resizable.step ?? 1 + const current = new Point(event.data.clientX, event.data.clientY) + const xIndex = Math.floor((current.x - start.x) / step) + const yIndex = Math.floor((current.x - start.x) / step) + const plusX = type === 'x-end' ? current.x > point.x : current.x < point.x + const plusY = type === 'y-end' ? current.y > point.y : current.y < point.y + const allowX = allowResize.includes('x') + const allowY = allowResize.includes('y') + const width = resizable.width?.(node, element) + const height = resizable.height?.(node, element) + if (axis === 'x') { + if (xIndex === store.value.xIndex) return + if (allowX) { + if (plusX) { + width.plus() + } else { + width.minus() + } + } + } else if (axis === 'y') { + if (yIndex === store.value.yIndex) return + if (allowY) { + if (plusY) { + height.plus() + } else { + height.minus() + } + } + } + store.value.point = current + store.value.xIndex = xIndex + store.value.yIndex = yIndex + } + }) + + engine.subscribeTo(DragStopEvent, () => { + if (engine.cursor.type !== CursorType.Move) return + if (store.value) { + store.value = null + engine.cursor.setStyle('') + } + }) +} diff --git a/packages/core/src/effects/useSelectionEffect.ts b/packages/core/src/effects/useSelectionEffect.ts index 9c41176ea..d2de2f1b8 100644 --- a/packages/core/src/effects/useSelectionEffect.ts +++ b/packages/core/src/effects/useSelectionEffect.ts @@ -11,7 +11,7 @@ export const useSelectionEffect = (engine: Engine) => { *[${engine.props.outlineNodeIdAttrName}] `) const isHelpers = target?.closest?.( - `*[${engine.props.nodeHelpersIdAttrName}]` + `*[${engine.props.nodeSelectionIdAttrName}]` ) const currentWorkspace = engine.workbench.activeWorkspace if (!currentWorkspace) return diff --git a/packages/core/src/models/Cursor.ts b/packages/core/src/models/Cursor.ts index b7f750fdd..a8aabb612 100644 --- a/packages/core/src/models/Cursor.ts +++ b/packages/core/src/models/Cursor.ts @@ -11,9 +11,6 @@ export enum CursorStatus { export enum CursorType { Move = 'MOVE', Selection = 'SELECTION', - Resize = 'RESIZE', - ResizeWidth = 'RESIZE_WIDTH', - ResizeHeight = 'RESIZE_HEIGHT', } export interface ICursorPosition { @@ -67,6 +64,17 @@ const DEFAULT_SCROLL_OFFSET = { scrollY: 0, } +const setCursorStyle = (contentWindow: Window, style: string) => { + const currentRoot = document?.getElementsByTagName?.('html')?.[0] + const root = contentWindow?.document?.getElementsByTagName('html')?.[0] + if (root && root.style.cursor !== style) { + root.style.cursor = style + } + if (currentRoot && currentRoot.style.cursor !== style) { + currentRoot.style.cursor = style + } +} + export class Cursor { engine: Engine @@ -101,6 +109,7 @@ export class Cursor { dragEndPosition: observable.ref, dragEndScrollOffset: observable.ref, view: observable.ref, + setStyle: action, setPosition: action, setStatus: action, setType: action, @@ -115,6 +124,12 @@ export class Cursor { this.type = type } + setStyle(style: string) { + this.engine.workbench.eachWorkspace((workspace) => { + setCursorStyle(workspace.viewport.contentWindow, style) + }) + } + setPosition(position?: ICursorPosition) { this.position = { ...this.position, diff --git a/packages/core/src/models/Engine.ts b/packages/core/src/models/Engine.ts index 0a7635120..1238c7f94 100644 --- a/packages/core/src/models/Engine.ts +++ b/packages/core/src/models/Engine.ts @@ -97,7 +97,7 @@ export class Engine extends Event { contentEditableAttrName: 'data-content-editable', contentEditableNodeIdAttrName: 'data-content-editable-node-id', clickStopPropagationAttrName: 'data-click-stop-propagation', - nodeHelpersIdAttrName: 'data-designer-node-helpers-id', + nodeSelectionIdAttrName: 'data-designer-node-helpers-id', nodeDragHandlerAttrName: 'data-designer-node-handler', nodeResizeHandlerAttrName: 'data-designer-node-resize-handler', outlineNodeIdAttrName: 'data-designer-outline-node-id', diff --git a/packages/core/src/models/TreeNode.ts b/packages/core/src/models/TreeNode.ts index 84700f5b5..8e11b5290 100644 --- a/packages/core/src/models/TreeNode.ts +++ b/packages/core/src/models/TreeNode.ts @@ -385,12 +385,13 @@ export class TreeNode { return this.designerProps.draggable ?? true } - allowResize() { + allowResize(): false | Array<'x' | 'y'> { if (this === this.root && !this.isSourceNode) return false - return ( - (this.designerProps.resizeXPath || this.designerProps.resizeYPath) ?? - false - ) + const { resizable } = this.designerProps + if (!resizable) return false + if (resizable.width && resizable.height) return ['x', 'y'] + if (resizable.width) return ['x'] + return ['y'] } allowDelete() { diff --git a/packages/core/src/presets.ts b/packages/core/src/presets.ts index e9356e2bb..66fb573f8 100644 --- a/packages/core/src/presets.ts +++ b/packages/core/src/presets.ts @@ -11,6 +11,7 @@ import { useViewportEffect, useDragDropEffect, useSelectionEffect, + useResizeEffect, useKeyboardEffect, useAutoScrollEffect, useWorkspaceEffect, @@ -36,6 +37,7 @@ export const DEFAULT_EFFECTS = [ useCursorEffect, useViewportEffect, useDragDropEffect, + useResizeEffect, useSelectionEffect, useKeyboardEffect, useAutoScrollEffect, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 794b60fde..32d2c7538 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -19,7 +19,7 @@ export type IEngineProps = IEventProps & { contentEditableNodeIdAttrName?: string //原地编辑指定Node Id属性名 clickStopPropagationAttrName?: string //点击阻止冒泡属性 outlineNodeIdAttrName?: string //大纲树节点ID的dom属性名 - nodeHelpersIdAttrName?: string //节点工具栏属性名 + nodeSelectionIdAttrName?: string //节点工具栏属性名 nodeDragHandlerAttrName?: string //节点拖拽手柄属性名 nodeResizeHandlerAttrName?: string //节点尺寸拖拽手柄属性名 defaultComponentTree?: ITreeNode //默认组件树 @@ -34,6 +34,24 @@ export type IEngineContext = { viewport: Viewport } +export type IResizable = { + width?: ( + node: TreeNode, + element: Element + ) => { + plus: () => void + minus: () => void + } + height?: ( + node: TreeNode, + element: Element + ) => { + plus: () => void + minus: () => void + } + step?: number +} + export interface IDesignerProps { package?: string //npm包名 registry?: string //web npm注册平台地址 @@ -46,11 +64,7 @@ export interface IDesignerProps { draggable?: boolean //是否可拖拽,默认为true deletable?: boolean //是否可删除,默认为true cloneable?: boolean //是否可拷贝,默认为true - resizeXPath?: string //尺寸X轴属性修改路径 - resizeYPath?: string //尺寸Y轴属性修改路径 - resizeStep?: number //尺寸拖拽步数 - resizeMin?: number //尺寸拖拽最小值 - resizeMax?: number //尺寸拖拽最大值 + resizable?: IResizable inlineChildrenLayout?: boolean //子节点内联,用于指定复杂布局容器,强制内联 selfRenderChildren?: boolean //是否自己渲染子节点 propsSchema?: ISchema //Formily JSON Schema diff --git a/packages/react/src/simulators/ResponsiveSimulator/index.tsx b/packages/react/src/simulators/ResponsiveSimulator/index.tsx index 387caef3d..4f7aa32cd 100644 --- a/packages/react/src/simulators/ResponsiveSimulator/index.tsx +++ b/packages/react/src/simulators/ResponsiveSimulator/index.tsx @@ -31,6 +31,12 @@ const useResizeEffect = ( let animationX = null let animationY = null + const getStyle = (status: ResizeHandleType) => { + if (status === ResizeHandleType.Resize) return 'nwse-resize' + if (status === ResizeHandleType.ResizeHeight) return 'ns-resize' + if (status === ResizeHandleType.ResizeWidth) return 'ew-resize' + } + const updateSize = (deltaX: number, deltaY: number) => { const containerRect = container.current?.getBoundingClientRect() if (status === ResizeHandleType.Resize) { @@ -63,7 +69,7 @@ const useResizeEffect = ( status = target.getAttribute( 'data-designer-resize-handle' ) as ResizeHandleType - engine.cursor.setType(status) + engine.cursor.setStyle(getStyle(status)) startX = e.data.topClientX startY = e.data.topClientY startWidth = rect.width @@ -108,7 +114,7 @@ const useResizeEffect = ( engine.subscribeTo(DragStopEvent, () => { if (!status) return status = null - engine.cursor.setType(CursorType.Move) + engine.cursor.setStyle('') if (animationX) { animationX = animationX() } diff --git a/packages/react/src/widgets/AuxToolWidget/Helpers.tsx b/packages/react/src/widgets/AuxToolWidget/Helpers.tsx index dd9da1e29..919bffd72 100644 --- a/packages/react/src/widgets/AuxToolWidget/Helpers.tsx +++ b/packages/react/src/widgets/AuxToolWidget/Helpers.tsx @@ -97,12 +97,8 @@ export const Helpers: React.FC = ({ node, nodeRect }) => { if (!nodeRect || !node) return null - const helpersId = { - [designer.props?.nodeHelpersIdAttrName]: node.id, - } return (
= (props) => { const createHandler = (value: string) => { return { [designer.props.nodeResizeHandlerAttrName]: value, + className: cls(prefix, value), } } - if (!props.node.allowResize()) return null - const xPath = props.node.designerProps?.resizeXPath - const yPath = props.node.designerProps?.resizeYPath + const allowResize = props.node.allowResize() + if (!allowResize) return null + const allowX = allowResize.includes('x') + const allowY = allowResize.includes('y') return ( <> - {xPath && ( -
- )} - {xPath && ( -
- )} - {yPath && ( -
- )} - {yPath && ( -
- )} + {allowX &&
} + {allowX &&
} + {allowY &&
} + {allowY &&
} ) } diff --git a/packages/react/src/widgets/AuxToolWidget/Selection.tsx b/packages/react/src/widgets/AuxToolWidget/Selection.tsx index cd1b41713..b96e6243b 100644 --- a/packages/react/src/widgets/AuxToolWidget/Selection.tsx +++ b/packages/react/src/widgets/AuxToolWidget/Selection.tsx @@ -8,6 +8,7 @@ import { useCursor, useDragon, usePrefix, + useDesigner, } from '../../hooks' import { observer } from '@formily/reactive-react' import { TreeNode } from '@designable/core' @@ -17,6 +18,7 @@ export interface ISelectionBoxProps { } export const SelectionBox: React.FC = (props) => { + const designer = useDesigner() const prefix = usePrefix('aux-selection-box') const innerPrefix = usePrefix('aux-selection-box-inner') const nodeRect = useValidNodeOffsetRect(props.node) @@ -38,8 +40,12 @@ export const SelectionBox: React.FC = (props) => { if (!nodeRect.width || !nodeRect.height) return null + const selectionId = { + [designer.props?.nodeSelectionIdAttrName]: props.node.id, + } + return ( -
+
{props.showHelpers && ( diff --git a/packages/react/src/widgets/AuxToolWidget/index.tsx b/packages/react/src/widgets/AuxToolWidget/index.tsx index 019eed44c..34aa8a884 100644 --- a/packages/react/src/widgets/AuxToolWidget/index.tsx +++ b/packages/react/src/widgets/AuxToolWidget/index.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useRef } from 'react' -import { CursorStatus, CursorType, ClosestPosition } from '@designable/core' import { useViewport, useCursor, @@ -15,24 +14,10 @@ import { Cover } from './Cover' import { DashedBox } from './DashedBox' import './styles.less' -const setCursorState = (contentWindow: Window, state: string) => { - const currentRoot = document?.getElementsByTagName?.('html')?.[0] - const root = contentWindow?.document?.getElementsByTagName('html')?.[0] - if (root) { - root.style.cursor = state - } - if (currentRoot) { - currentRoot.style.cursor = state - } -} - export const AuxToolWidget = () => { const engine = useDesigner() const viewport = useViewport() - const operation = useOperation() - const cursor = useCursor() const prefix = usePrefix('auxtool') - const viewportDragon = useDragon() const ref = useRef() useEffect(() => { return engine.subscribeWith('viewport:scroll', () => { @@ -42,35 +27,6 @@ export const AuxToolWidget = () => { }) }, [engine, viewport]) - useEffect(() => { - return engine.subscribeWith(['drag:move', 'drag:stop'], () => { - if (cursor.status !== CursorStatus.Dragging) { - setCursorState(viewport.contentWindow, 'default') - } else { - if (cursor.type === CursorType.Move) { - if (operation.getDragNodes().length) { - // todo: update cusor will trigger document layout rerender https://bugs.chromium.org/p/chromium/issues/detail?id=664066 - // if (viewportDragon.closestDirection === ClosestPosition.Inner) { - // setCursorState(viewport.contentWindow, 'copy') - // } else { - setCursorState(viewport.contentWindow, 'move') - //} - } - } else { - if (cursor.type === CursorType.ResizeWidth) { - setCursorState(viewport.contentWindow, 'ew-resize') - } else if (cursor.type === CursorType.ResizeHeight) { - setCursorState(viewport.contentWindow, 'ns-resize') - } else if (cursor.type === CursorType.Resize) { - setCursorState(viewport.contentWindow, 'nwse-resize') - } else { - setCursorState(viewport.contentWindow, 'default') - } - } - } - }) - }, [engine, cursor, viewportDragon, viewport, operation]) - if (!viewport) return null return ( diff --git a/packages/react/src/widgets/AuxToolWidget/styles.less b/packages/react/src/widgets/AuxToolWidget/styles.less index b01dee71c..a92aa7ee8 100644 --- a/packages/react/src/widgets/AuxToolWidget/styles.less +++ b/packages/react/src/widgets/AuxToolWidget/styles.less @@ -232,28 +232,28 @@ pointer-events: all; background-color: var(--dn-brand-color); - &.left { + &.x-start { left: 0; top: 50%; transform: translate(calc(-50% - 1px), -50%); cursor: ew-resize; } - &.right { + &.x-end { left: 100%; top: 50%; transform: translate(calc(-50% + 1px), -50%); cursor: ew-resize; } - &.top { + &.y-start { left: 50%; top: 0; transform: translate(-50%, calc(-50% - 1px)); cursor: ns-resize; } - &.bottom { + &.y-end { left: 50%; top: 100%; transform: translate(-50%, calc(-50% + 1px));