designable 实现尺寸拖拽方法和总结

1,954 阅读3分钟

designable 实现尺寸拖拽方法

designable的作者大佬已经实现了部分功能,我觉得代码很优雅,这里整理一下思路

image.png

首先从前端的现象到代码定位

在选中某个组件的时候,需要在上下左右四个方向添加可拖拽的操作柄。 所以我们很容易定位到代码应该从Selection组件开始看。

在代码里面,上线左右的四个点是通过 data-designer-node-resize-handler 这个属性去标识的。 上下左右四个操作柄的样式,也是 position: absolute 的,是相对于data-designer-node-helpers-id的布局。 这样保证了他和Selection组件是同样大小的。

Selection 组件

  • 查询选中的TreeNode的尺寸元素, 包括 {x, y, height, width} = nodeRect; const nodeRect = useValidNodeOffsetRect(props.node);
  • 通过createSelectionStyle方法获得外层div的样式
  • 处理原有的逻辑,在同级添加ResizeHandler, 当然,这要处理一下,避免对root也生效
    <div {...selectionId} className={prefix} style={createSelectionStyle()}>
      {!props.node.isRoot && <ResizeHandler node={props.node} />}

      {props.showHelpers && <Helpers {...props} node={props.node} nodeRect={nodeRect} />}
    </div>

ResizeHandler组件

这个组件的逻辑主要是 hook useResizeEffect 的逻辑


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) {
        // 找到 距离 操作柄最近的 Selection 组件的对应元素
        const element = handler.closest(
          `*[${engine.props.nodeSelectionIdAttrName}]`
        )
        if (element) {
          // 找到element上的nodeid ,也就是当前选中的组件的TreeNode 的id
          const nodeId = element.getAttribute(
            engine.props.nodeSelectionIdAttrName
          )
          if (nodeId) {
            // 通过nodeId 找到队友的TreeNode
            const node = engine.findNodeById(nodeId)
            if (node) {
              const axis = type.includes('x') ? 'x' : 'y'
              return { axis, type, node, element }
            }
          }
        }
      }
    }
    return
  }

  // 每次拖动触发都 +-6, 为了避免重复叠加和多次计算尺寸导致重绘重排,所以有了这个对象
  const store: ResizeStore = {}

  engine.subscribeTo(DragStartEvent, (event) => {
    const data = findStartNodeHandler(target)
    if (data) {
      const point = new Point(event.data.clientX, event.data.clientY)
      store.value = {
        ...data,
        point,
      }

    }
  })

  engine.subscribeTo(DragMoveEvent, (event) => {
    if (engine.cursor.type !== CursorType.Move) return
    if (store.value) {
      const { axis, type, node, element, point } = store.value
      const allowResize = node.allowResize()
      if (!allowResize) return
      const resizable = node.designerProps.resizable
      const rect = element.getBoundingClientRect()
      const current = new Point(event.data.clientX, event.data.clientY)
      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 (plusX && type === 'x-end' && current.x < rect.x + rect.width) return
        if (!plusX && type === 'x-end' && current.x > rect.x + rect.width)
          return
        if (plusX && type === 'x-start' && current.x > rect.x) return
        if (!plusX && type === 'x-start' && current.x < rect.x) return
        if (allowX) {
          if (plusX) {
            width.plus()
          } else {
            width.minus()
          }
        }
      } else if (axis === 'y') {
        if (plusY && type === 'y-end' && current.y < rect.y + rect.height)
          return
        if (!plusY && type === 'y-end' && current.y > rect.y + rect.height)
          return
        if (plusY && type === 'y-start' && current.y > rect.y) return
        if (!plusY && type === 'y-start' && current.y < rect.y) return
        if (allowY) {
          if (plusY) {
            height.plus()
          } else {
            height.minus()
          }
        }
      }
      store.value.point = current
    }
  })

  engine.subscribeTo(DragStopEvent, () => {
    if (engine.cursor.type !== CursorType.Move) return
    if (store.value) {
      store.value = null
      engine.cursor.setStyle('')
    }
  })
}

水平拖动布局

上面的resize功能大部分功能都满足了,但是还缺少一些优化

  • 在左边的操作柄上往左拖动,也就是 minus X, 现在demo是直接修改 width 属性,所以导致看起来是右边的border在左移
  • resize 之后是都固定在靠左的.

实现水平方向的拖动。

  • 在designerProps中新增属性 freeHorizontal
designerProps: {
    droppable: true,
    propsSchema: createVoidFieldSchema(AllSchemas.Card),
    resizable: {
      width(node, element) {
        const width = Number(
          node.props['x-component-props']?.style?.width ??
            element.getBoundingClientRect().width
        )
        return {
          plus: () => {
            node.props['x-component-props'] =
              node.props['x-component-props'] || {}
            node.props['x-component-props'].style =
              node.props['x-component-props'].style || {}
            node.props['x-component-props'].style.width = width + 6
          },
          minus: () => {
            node.props['x-component-props'] =
              node.props['x-component-props'] || {}
            node.props['x-component-props'].style =
              node.props['x-component-props'].style || {}
            node.props['x-component-props'].style.width = width - 6
          },
        }
      },
      height(node, element) {
        const height = Number(
          node.props['x-component-props']?.style?.height ??
            element.getBoundingClientRect().height
        )
        return {
          plus: () => {
            node.props['x-component-props'] =
              node.props['x-component-props'] || {}
            node.props['x-component-props'].style =
              node.props['x-component-props'].style || {}
            node.props['x-component-props'].style.height = height + 6
          },
          minus: () => {
            node.props['x-component-props'] =
              node.props['x-component-props'] || {}
            node.props['x-component-props'].style =
              node.props['x-component-props'].style || {}
            node.props['x-component-props'].style.height = height - 6
          },
        }
      },
      freeHorizontal(node, element, diffX) {
        const left = parseInt(
          node.props['x-component-props']?.style?.left ??
            element.style.left
        ) || 0;
        return {
          setHorizontalFreeLayout: () => {
            console.log('diffX  ', diffX)
            console.log('left  ', left)
            node.props['x-component-props'] = node.props['x-component-props'] || {};
            node.props['x-component-props'].style = node.props['x-component-props'].style || {};
            node.props['x-component-props'].style.left = (left + parseInt(diffX)) + 'px';
          }
        }
      }
    },
  }
  • 在 core/effects新增 hook , 只有在长按住Shift键的时候,才能实现水平方向的拖动。这是为了区分长按拖动布局。
import { Engine } from '../models'
import { KeyDownEvent, KeyUpEvent } from '../events'
import { KeyCode } from '@designable/shared';

(window as any).longPressShift = false;


export const useKeyLongPress = (engine: Engine) => {
  engine.subscribeTo(KeyDownEvent, (event) => {
    const keyboard = engine.keyboard
    if (!keyboard) return
    if (event.data !== KeyCode.Shift) {
      return;
    }
    if (!(window as any).longPressShift) {
      (window as any).longPressShift = true;
    }
  })

  engine.subscribeTo(KeyUpEvent, (event) => {
    const keyboard = engine.keyboard
    if (!keyboard) return
    (window as any).longPressShift = false;
  })
}
  • 修改 useDragDropEffect
let startPoint: Point;

  const handleHorizationMove = (event) => {
    const target = event.data.target as HTMLElement
    const el = target?.closest(`
      *[${engine.props.nodeIdAttrName}]
    `);
    engine.workbench.eachWorkspace((currentWorkspace) => {
      const operation = currentWorkspace.operation
      const point = new Point(event.data.topClientX, event.data.topClientY)
      const dragNodes = operation.getDragNodes()
      if (!dragNodes.length) return
      const diffX = point.x - startPoint?.x;
      const resizable = dragNodes[0].designerProps.resizable;
      const freeHorizontal = resizable.freeHorizontal?.(dragNodes[0], el, diffX);
      freeHorizontal.setHorizontalFreeLayout();
      // 避免叠加
      startPoint = point;
    })
  }
  engine.subscribeTo(DragMoveEvent, (event) => {
    if (engine.cursor.type !== CursorType.Move) return
    // 在长按状态下
    if ((window as any).longPressShift) {
      handleHorizationMove(event);
      return;
    }
    ...
  })