资讯详情

资讯详情

Preact Moveable Groupable 完全指南:多目标组的拖拽、缩放、旋转与捏合实战

前端UI组件【免费下载链接】moveableMoveable! Draggable! Resizable! Scalable! Rotatable! Warpable! Pinchable! Groupable! Snappable!项目地址https://gitcode.com/gh_mirrors/mo/moveable点击查看免费下载导读本文以preact-moveable包的 groupable.md 为核心系统讲解 Groupable 能力——让多个目标元素以组为单位在 draggable、resizable、scalable、rotatable、pinchable 之间联动操作。你将掌握多目标targets数组的传入方式、五类 Group 事件的完整参数与回调写法、keepRatio、edge、throttle*等关键配置项的作用以及 Group 事件与单目标事件在源码层的触发与派发差异。阅读本文后你可以直接在 Preact 应用中落地一个可框选、可整体拖拽、缩放、旋转的多元素编辑器场景。一、Groupable 是什么1.1 概念定义Groupable表示多个目标元素targets能否作为一个整体在 draggable拖拽、resizable缩放尺寸、scalable缩放变换、rotatable旋转等能力下被成组移动。它本身不是一个独立交互能力而是对上述单目标能力的组模式叠加。从仓库源码看Groupable 被实现为一个独立的 Able能力模块packages/react-moveable/src/ables/Groupable.tsx 中导出的 able 声明了groupable、groupableProps、targetGroups、defaultGroupRotate、defaultGroupOrigin、hideChildMoveableDefaultLines等 props并在渲染阶段为props.targets中的每个目标创建对应的子MoveableManager// packages/react-moveable/src/ables/Groupable.tsx节选 return [ ...targets.map((target, i) { return MoveableManagerGroupableProps key{moveable i} ref{refs(moveable, moveables, i)} target{target} origin{false} parentMoveable{moveable} ... /; }), ... ];也就是说Preact 中的Moveable target{[...]}最终会渲染出一个MoveableGroupMoveableGroup.tsx它内部再为每个子目标挂一个子 Moveable用于采集每个目标的几何信息并派发每个目标各自的独立事件。1.2 与单目标模式的差异在单目标模式下事件名为dragStart / drag / dragEnd、resizeStart / resize / resizeEnd等切换为组模式后事件名统一加上Group后缀dragGroupStart / dragGroup / dragGroupEnd、resizeGroupStart / resizeGroup / resizeGroupEnd、scaleGroupStart / scaleGroup / scaleGroupEnd、rotateGroupStart / rotateGroup / rotateGroupEnd、pinchGroupStart / pinchGroup / pinchGroupEnd。这点在 handbook/handbook.md 的 How to use Group 一节有明确说明When using group, event name changes. (ex: dragStart dragGroupStart, pinchStart pinchGroupStart) The drag event always occurs with the group event. (Resizable, Scalable, Rotatable)最后一句非常重要在组模式下每次 resize / scale / rotate 的同时都会伴随一次 drag 事件。这是理解组事件回调参数的基础详见下文ev.drag的用法。1.3 支持范围组模式支持 draggable、resizable、scalable、rotatable、pinchable、snappablewarpable 在组模式下不可用组模式可以继续配合 Snappable 使用吸附与边界约束行为与单目标一致。二、快速上手最小可用示例安装依赖$ npm i preact-moveable在 Preact 组件中渲染一个多目标 Moveableimport Moveable from preact-moveable; render() { return ( Moveable /* multiple targets */ target{[].slice.call(document.querySelectorAll(.target))} container{null} origin{true} draggable{true} throttleDrag{0} / ); }要点target传入数组ArrayHTMLElement | SVGElement而不是单个元素。原文档示例用[].slice.call(document.querySelectorAll(.target))把 NodeList 转为数组这是最常用的写法container{null}表示控制框挂载到 body 下避免被相对定位的父容器裁剪origin{true}显示旋转/缩放的原点draggable{true}开启组拖拽。也可以给每个目标元素添加classtarget然后在componentDidMount之后再初始化 Moveable确保 DOM 已存在。三、目标数组与目标组进阶配置3.1 基础 targets组件内部通过ChildrenDifferMoveableGroup.tsx 中的this.differ对props.targets做增删改差量检测。当added / removed不为空或transformOrigin变化、targetGroups变化时会触发updateRect()重新计算组的外包围矩形。因此运行时动态增删目标也是被支持的——只要更新target数组即可框架会自动同步子 Moveable 与组矩形。3.2 嵌套目标组 targetGroupstargetGroups允许把多个目标进一步划分为嵌套子组嵌套结构下子组作为一个整体参与父组的变换且父组变换会按嵌套层级递归下发。源码中的findMoveableGroupsMoveableGroup.tsx递归匹配targetGroups与子 Moveable返回值是SelfGroupArrayMoveableManager | null | SelfGroup用于构建嵌套的组矩形。3.3 组级 props 一览prop默认值说明targets[]组目标数组groupabletrue是否启用组模式MoveableGroup 默认开启targetGroups[]嵌套目标组配置dragAreatrue组模式下默认在组矩形上渲染拖拽区域keepRatiotrueresize/scale 时保持宽高比defaultGroupRotate0组初始旋转角度degdefaultGroupOrigin50% 50%组变换原点hideChildMoveableDefaultLinesfalse是否隐藏子目标对应的默认参考线以上默认值来自 MoveableGroup.tsx 的defaultProps。其中defaultGroupRotate/defaultGroupOrigin/hideChildMoveableDefaultLines的详细语义可见同一文件底部的 JSDoc 注释。组矩形的计算逻辑位于 MoveableGroup.tsx 的getGroupRect当组旋转角不是 90° 的整数倍时通过两组斜线与旋转角同向/垂直的直线簇求交得到最小外接矩形否则退化为普通的 min/max 包围盒。四、Draggable组拖拽4.1 事件与参数组拖拽三个事件onDragGroupStart({ targets })onDragGroup({ targets, events })onDragGroupEnd({ targets, isDrag, clientX, clientY })从类型定义packages/react-moveable/src/types.ts可见targets参与拖拽的目标数组events每个目标各自的drag事件参数数组长度与targets一致。onDragGroup的回调中events.forEach(...)遍历每个目标的拖拽事件ev上携带该目标的left/top、dist累计位移、transform等。典型应用是直接把ev.transform写回对应目标onDragGroup{({ targets, events }) { console.log(onDragGroup, targets); events.forEach(ev { // drag event console.log(onDrag left, top, ev.left, ev.top); // 方案一left/top 布局 // ev.target!.style.left ${ev.left}px; // ev.target!.style.top ${ev.top}px; // 方案二transform 布局 console.log(onDrag translate, ev.dist); ev.target!.style.transform ev.transform; }); }}注意组模式下强烈建议使用ev.transform而非 left/top。因为组可能处于旋转、缩放状态transform 会由框架基于矩阵计算给出能够准确叠加组级变换。4.2 源码级的派发链路组拖拽事件的底层实现位于 packages/react-moveable/src/ables/Draggable.tsxdragGroupStart先调用自身的dragStart再通过triggerChildGesto(moveable, this, dragStart, ...)把开始事件下发给每个子 Moveable收集eventParams后与targets一起组成OnDragGroupStart触发dragGroup同样先执行自身drag使用parentThrottleDrag继承组级节流再下发passDelta本次增量给子目标组装OnDragGroupdragGroupEnd依次结束自身与子目标的 drag。事件分发的公共函数是 groupUtils.ts 的triggerChildGesto——它遍历moveable.moveables为每个子 Moveable 以相同的事件类型、相同的增量/参数触发一次子事件从而得到events数组。这也解释了为什么events的顺序与targets一一对应。4.3 节流配置throttleDrag默认0拖拽位移按像素节流throttleDragRotate默认0拖拽方向按角度节流startDragRotatethrottleDragRotate的起始角度如 45 时配合 90 的节流可固定吸附到 45/135/225/315。在组模式下throttleDrag由父组统一下发给子目标见 Draggable.tsx 的throttleDrag isGroup ? 0 : ...与parentThrottleDrag避免子目标各自节流导致错位。五、Resizable组缩放尺寸5.1 事件与参数onResizeGroupStart({ targets, clientX, clientY })onResizeGroup({ targets, direction, events })onResizeGroupEnd({ targets, isDrag, clientX, clientY })onResizeGroup中direction是 8 方向数组如[-1, 1]表示从左上角拖拽events是每个目标的独立resize事件数组。每个ev携带ev.delta本次宽高增量ev.width / ev.height当前宽高ev.drag该目标伴随发生的 drag 事件组 resize 时目标位置也会联动变化。原文档示例给出了完整的位置修正逻辑——从左上角拉大时需要补偿负向增量导致的位移onResizeGroup{({ targets, direction }) { console.log(onResizeGroup, targets); e.events.forEach(ev { const offset [ direction[0] 0 ? -ev.delta[0] : 0, direction[1] 0 ? -ev.delta[1] : 0, ]; // ev.drag is a drag event that occurs when the group resize. const left offset[0] ev.drag.beforeDist[0]; const top offset[1] ev.drag.beforeDist[1]; const width ev.width; const top ev.top; }); }}该示例仅演示取值方式实际写回时需自行执行样式赋值如target.style.left left px等并可配合keepRatio保持宽高比。5.2 关键配置resizable{true}开启组 resizethrottleResize默认0宽高增量节流keepRatio{true}resize 或 scale 时保持宽高比。MoveableGroup 的defaultProps中keepRatio默认即为trueedge{false}是否在元素边缘显示可拖拽的边线控制点。互斥约束resizable、scalable、warpable三者在同一 Moveable 实例上只能启用其一组模式下 warpable 不可用因此实际是 resizable 与 scalable 二选一。六、Scalable组缩放变换6.1 事件与参数onScaleGroupStart({ targets, clientX, clientY })onScaleGroup({ targets, events })onScaleGroupEnd({ targets, isDrag, clientX, clientY })events中每个ev携带ev.scale[scaleX, scaleY]累计缩放比ev.drag伴随的 drag 事件ev.drag.beforeDist缩放过程中目标的累计位移ev.target当前目标元素。onScaleGroup{({ targets, events }) { console.log(onScaleGroup, targets); events.forEach(ev { const target ev.target; // ev.drag is a drag event that occurs when the group scale. const left ev.drag.beforeDist[0]; const top ev.drag.beforeDist[1]; const scaleX ev.scale[0]; const scaleY ev.scale[1]; // 写回样式示例 // target.style.transform translate(${left}px, ${top}px) scale(${scaleX}, ${scaleY}); }); }}OnScaleGroup的类型定义见 types.tstargets: ArrayHTMLElement | SVGElement、events: OnScale[]。6.2 关键配置scalable{true}开启组 scalethrottleScale默认0缩放比例节流keepRatio{true}保持宽高比。与 Resizable 相比Scalable 修改的是 CSStransform的 scale不改变元素的 layout 宽高对于 SVG 元素HandBook 也建议优先使用 scalable 而不是 resizable。七、Rotatable组旋转7.1 事件与参数onRotateGroupStart({ targets, clientX, clientY })onRotateGroup({ targets, events, delta, dist })onRotateGroupEnd({ targets, isDrag, clientX, clientY })onRotateGroup中delta / dist本次/累计旋转角度events每个目标的独立rotate事件数组每个ev携带ev.drag.beforeDist旋转联动位移、ev.beforeDist该目标累计旋转角。onRotateGroup{({ targets, events, delta, dist }) { e.events.forEach(ev { const target ev.target; // ev.drag is a drag event that occurs when the group rotate. const left ev.drag.beforeDist[0]; const top ev.drag.beforeDist[1]; const deg ev.beforeDist; // 写回示例target.style.transform translate(${left}px, ${top}px) rotate(${deg}deg); }); }}此外OnRotateGroup类型还提供了setGroupRotation(rotation)方法set已标记deprecated见 types.ts可以在事件中程序化设定组的旋转角。7.2 关键配置rotatable{true}开启组旋转throttleRotate默认0旋转角度节流defaultGroupRotate默认0组初始旋转角。当组内所有目标旋转角一致差值小于 0.1°且未设置useDefaultGroupRotate时组矩形会自动采用该角度作为基准见 MoveableGroup.tsx 的getMoveableGroupRect逻辑defaultGroupOrigin默认50% 50%组变换原点可设为20% 40%等任意 CSS 位置origin{true}显示原点控制点允许拖动原点。八、Pinchable组捏合触屏多指8.1 事件时序开启pinchable{true}后触屏双指操作会同时驱动draggable、resizable、scalable、rotatable 四类能力。原文档明确标注了事件的先后次序onPinchGroupStart发生在dragGroupStart / rotateGroupStart / scaleGroupStart / resizeGroupStart之前onPinchGroup发生在dragGroup / rotateGroup / scaleGroup / resizeGroup之前onPinchGroupEnd发生在dragGroupEnd / rotateGroupEnd / scaleGroupEnd / resizeGroupEnd之前。pinchable{true} onPinchGroupStart{({ targets, clientX, clientY, datas }) { // pinchGroupStart event occur before dragGroupStart, rotateGroupStart, scaleGroupStart, resizeGroupStart console.log(onPinchGroupStart); }} onPinchGroup{({ targets, clientX, clientY, datas }) { // pinchGroup event occur before dragGroup, rotateGroup, scaleGroup, resizeGroup console.log(onPinchGroup); }} onPinchGroupEnd{({ isDrag, targets, clientX, clientY, datas }) { // pinchGroupEnd event occur before dragGroupEnd, rotateGroupEnd, scaleGroupEnd, resizeGroupEnd console.log(onPinchGroupEnd); }}参数中datas用于在同一轮 pinch 的 Start / 进行中 / End 之间共享数据targets为参与捏合的目标数组。8.2 使用建议移动端编辑器建议同时开启draggable、resizable或scalable、rotatable与pinchable让双指手势与单指/手柄操作体验一致捏合事件内部会复用四类能力各自的子事件派发链路因此不必在 pinch 回调里重复写样式——只需在对应的dragGroup / resizeGroup / scaleGroup / rotateGroup中处理写回即可。九、事件序列与通用参数9.1 组事件序列与单目标一致组事件的完整触发序列同样包裹在beforeRender*与render*之间详见 handbook/handbook.mdstartbeforeRenderGroupStart resizeGroupStart或其他 GroupStart renderGroupStartmovebeforeRenderGroup beforeResizeGroup resizeGroup或其他 Group renderGroupendbeforeRenderGroupEnd resizeGroupEnd或其他 GroupEnd renderGroupEnd9.2 通用事件参数所有 Moveable 事件含 Group 系列默认携带参数含义currentTarget触发事件的 Moveable 实例target事件发生时命中的目标元素clientX / clientY事件发生的坐标datas同一事件链Start/进行中/End间共享的数据inputEvent原始 MouseEvent 或 TouchEvent组事件在此基础上额外提供targets目标数组与events每个目标各自的子事件数组。十、事件驱动模式补充拖拽区域与动态换目标10.1 dragArea把拖拽区挂到组矩形上组模式下dragArea默认开启MoveableGroup.defaultProps.dragArea true框架会在组矩形上渲染一个透明的拖拽区域见 DragArea.tsx并配套四个区域碎片area-pieces来精确判定点击是否落在某个子目标内部。这样即使目标之间有间隙也能在组矩形内空白处整体拖拽组。10.2 动态更换目标组模式下同样可以动态改变target并配合dragStart(e)手动启动拖拽。Preact 写法与 React 一致参见 handbook/handbook.md 的 React, Preact ExampleMoveable ref{e { this.moveable e; }} target{this.state.target} / onMouseDown(e) { // 使用 nativeEventPreact/React 合成事件需取原生事件 const nativeEvent e.nativeEvent; this.setState({ target: nativeEvent.target, }, () { this.moveable.dragStart(nativeEvent); }); }十一、完整示例五合一 Group 编辑器片段将以上能力整合为一个同时支持拖拽、等比缩放、缩放、旋转与捏合的组import Moveable from preact-moveable; render() { return ( Moveable target{[].slice.call(document.querySelectorAll(.target))} container{null} origin{true} edge{false} /* draggable */ draggable{true} throttleDrag{0} onDragGroupStart{({ targets }) console.log(onDragGroupStart, targets)} onDragGroup{({ targets, events }) { events.forEach(ev { ev.target!.style.transform ev.transform; }); }} onDragGroupEnd{({ targets, isDrag }) console.log(onDragGroupEnd, targets, isDrag)} /* resize 与 scale 互斥此处启用 resizable */ keepRatio{true} resizable{true} throttleResize{0} onResizeGroupStart{({ targets }) console.log(onResizeGroupStart, targets)} onResizeGroup{({ targets, direction, events }) { events.forEach(ev { const offset [ direction[0] 0 ? -ev.delta[0] : 0, direction[1] 0 ? -ev.delta[1] : 0, ]; const left offset[0] ev.drag.beforeDist[0]; const top offset[1] ev.drag.beforeDist[1]; ev.target!.style.left ${left}px; ev.target!.style.top ${top}px; ev.target!.style.width ${ev.width}px; ev.target!.style.height ${ev.height}px; }); }} onResizeGroupEnd{({ targets, isDrag }) console.log(onResizeGroupEnd, targets, isDrag)} /* rotatable */ rotatable{true} throttleRotate{0} onRotateGroupStart{({ targets }) console.log(onRotateGroupStart, targets)} onRotateGroup{({ targets, events }) { events.forEach(ev { const left ev.drag.beforeDist[0]; const top ev.drag.beforeDist[1]; const deg ev.beforeDist; ev.target!.style.transform translate(${left}px, ${top}px) rotate(${deg}deg); }); }} onRotateGroupEnd{({ targets, isDrag }) console.log(onRotateGroupEnd, targets, isDrag)} /* pinchable触屏双指同时驱动以上能力 */ pinchable{true} onPinchGroupStart{({ targets }) console.log(onPinchGroupStart)} onPinchGroup{({ targets }) console.log(onPinchGroup)} onPinchGroupEnd{({ targets }) console.log(onPinchGroupEnd)} / ); }提示若需要在组模式下同时使用 scale将resizable相关配置替换为scalable{true}与throttleScale{0}并把写回逻辑改为ev.target!.style.transform ...基于ev.scale与ev.drag.beforeDist同时保持keepRatio{true}。十二、常见问题与注意事项transform 优先组处于旋转/缩放状态时用 left/top 定位可能与 transform 叠加产生偏移推荐统一使用ev.transform或显式计算translate rotate scale组合。互斥能力同一实例上resizable、scalable、warpable只能启用一个组模式下 warpable 不可用。事件写回自担Group 事件只负责给出计算好的参数不自动修改目标样式与单目标模式一致需在回调中显式写回。events与targets一一对应子事件数组顺序与目标数组一致可用索引安全关联源码保证见 groupUtils.ts 的triggerChildGesto。动态增删目标直接更新target数组即可内部ChildrenDiffer会自动差量同步。SSR 与容器container{null}时控制框挂载在 body若页面有缩放zoomprop或变换容器请参考 Storybook 中 ReactZoomApp 等示例处理坐标换算。结语Groupable 是 Moveable 家族中面向多对象编辑场景的核心能力。本文从 packages/preact-moveable/groupable.md 出发结合 Groupable.tsx、MoveableGroup.tsx、Draggable.tsx、types.ts 等源码讲清了五类 Group 事件的使用姿势、参数语义与底层派发原理。若需深入了解组矩形包围盒算法、嵌套 targetGroups 或 persistData 组状态持久化可继续研读 MoveableGroup.tsx 与仓库中的 3-Group 系列 Storybook 示例。赞分享前端UI组件【免费下载链接】moveableMoveable! Draggable! Resizable! Scalable! Rotatable! Warpable! Pinchable! Groupable! Snappable!项目地址https://gitcode.com/gh_mirrors/mo/moveable点击查看免费下载相关推荐React Moveable 完全指南在 React 中实现拖拽、缩放、旋转、捏合与吸附React Moveable 完全指南在 React 中实现拖拽、缩放、旋转、捏合与吸附 导读本文以 react moveable 为对象系统讲解如何在一前端UI组件ngx-moveable 完全指南在 Angular 中接入 Moveable 拖拽、缩放、旋转、扭曲与捏合能力ngx moveable 完全指南在 Angular 中接入 Moveable 拖拽、缩放、旋转、扭曲与捏合能力 ngx moveable 是 Moveabl前端UI组件Preact Moveable 实战指南用 Preact 组件实现拖拽、缩放、旋转、扭曲与编组变换Preact Moveable 实战指南用 Preact 组件实现拖拽、缩放、旋转、扭曲与编组变换 本文基于开源仓库中的 preact moveable 包前端UI组件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →