designable 实现尺寸拖拽方法
designable的作者大佬已经实现了部分功能,我觉得代码很优雅,这里整理一下思路
首先从前端的现象到代码定位
在选中某个组件的时候,需要在上下左右四个方向添加可拖拽的操作柄。
所以我们很容易定位到代码应该从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;
}
...
})