- 前端
- UI组件
【免费下载链接】joint
A proven SVG-based JavaScript diagramming library powering exceptional UIs
本文基于官方示例
resize-control-tool-js(示例 README)展开,讲解如何在 JointJS 中为元素挂载一个可拖拽的缩放手柄(Resize Control Tool)。文章既给出可直接复制的完整代码与运行命令,也深入@joint/core源码(Control.mjs)剖析 Control 工具的参数契约与鼠标交互事件流,帮助读者理解并自定义属于自己的元素编辑工具。
一、示例项目是什么
resize-control-tool-js是 JointJS monorepo(仓库根目录见 package.json)中的一个纯 JavaScript 示例,位于 examples/resize-control-tool-js。它的目标是回答一个高频需求:如何让用户通过拖拽手柄方便地缩放画布中的元素?
示例的运行效果是:画布中有一个 100×100 的标准矩形,矩形的右下角带有一个缩放图标手柄;按住手柄拖拽时,矩形实时改变尺寸,同时手柄周围出现一个虚线外框提示目标范围;松开鼠标后虚线框消失,缩放完成。
examples/resize-control-tool-js/ ├── assets/ │ ├── icons/ │ │ └── resize.svg # 缩放手柄图标(紫色箭头图标) │ └── jointjs-logo-black.svg ├── src/ │ ├── main.js # 全部示例逻辑 │ └── styles.css # 画布容器样式 ├── index.html # 页面骨架,挂载 #paper-container ├── README.md # 安装 / 开发 / 构建 / 预览说明 └── package.json # 名为 @joint/demo-resize-control-tool-js 的 Vite 工程工程通过package.json以"@joint/core": "workspace:^"的方式引用 monorepo 内的核心库,使用 Vite 作为开发/构建工具,源码为 ESM 模块。
二、安装、开发与构建
示例 README 提供了完整的四步工作流,均需在仓库环境内执行:
1. 安装与构建(在 monorepo 根目录执行)
yarn install yarn run build第一步安装整个仓库(含@joint/core工作区依赖),第二步完成核心包的构建。
2. 开发模式(在示例目录内执行)
yarn dev启动 Vite 开发服务器,然后在终端打印的地址访问页面,通常为http://localhost:5173。
3. 生产构建
yarn build产物会生成在dist/目录(由 package.json 中tsc && vite build脚本驱动)。
4. 本地预览生产构建
yarn preview该命令启动一个静态服务器预览dist/中的产物。
三、核心实现逐行拆解
示例的全部逻辑都在 src/main.js(约 75 行),分为三个部分:创建画布、定义缩放工具、给元素挂载工具。
3.1 创建 Graph 与 Paper
import { dia, shapes, elementTools } from '@joint/core'; import './styles.css'; import resizeIcon from '../assets/icons/resize.svg'; const graph = new dia.Graph({}, { cellNamespace: shapes }); const paper = new dia.Paper({ model: graph, cellViewNamespace: shapes, width: '100%', height: '100%', gridSize: 20, async: true, sorting: dia.Paper.sorting.APPROX, background: { color: '#F3F7F6' } }); paperContainer.appendChild(paper.el); paper.setGrid('mesh');几个值得注意的配置项:
gridSize: 20:设置 20 像素的逻辑网格,后续拖拽缩放的坐标会被吸附到网格上(clientToLocalPoint在 Paper.mjs 中结合snapToGrid实现)。这样元素尺寸会以 20 为粒度变化,避免"毛边"。async: true:开启异步渲染,适合交互频繁的场景。sorting: dia.Paper.sorting.APPROX:使用近似排序算法(Paper.mjs 附近可以看到drawGrid相关初始化),在大图中性能更好。background: { color: '#F3F7F6' }:设置浅色背景。paper.setGrid('mesh'):显示网格线,让缩放结果在视觉上有参照。
页面骨架由 index.html 提供,样式见 styles.css,核心就是让#paper-container铺满视口并支持滚动。
3.2 定义 ResizeTool:扩展 elementTools.Control
这是示例的核心——一个自定义的缩放手柄工具:
const ResizeTool = elementTools.Control.extend({ children: [ { tagName: 'image', selector: 'handle', attributes: { cursor: 'pointer', width: 20, height: 20, 'xlink:href': resizeIcon } }, { tagName: 'rect', selector: 'extras', attributes: { 'pointer-events': 'none', fill: 'none', stroke: '#33334F', 'stroke-dasharray': '2,4', rx: 5, ry: 5 } } ], getPosition: function(view) { const model = view.model; const { width, height } = model.size(); return { x: width, y: height }; }, setPosition: function(view, coordinates) { const model = view.model; model.resize( Math.max(coordinates.x - 10, 1), Math.max(coordinates.y - 10, 1) ); } });这段代码蕴含了 Control 工具的完整契约:
(1)children:自定义工具的 SVG 标记(markup)
Control默认自带两个子节点(见 Control.mjs):圆形handle和虚线矩形extras。示例用 20×20 的resize.svg图片(图标文件)替换了默认圆形手柄,保留了相同语义的extras虚线框。
(2)getPosition(view):告诉工具手柄该放在哪里
返回手柄在元素相对坐标系中的位置。示例直接返回元素当前尺寸{ x: width, y: height },即元素右下角——手柄始终贴住右下角,元素变大手柄跟着走。
(3)setPosition(view, coordinates):拖拽时手柄坐标如何写回模型
拖拽过程中框架会持续调用setPosition传入相对坐标,示例将其换算为新的宽高并写入模型:
coordinates.x - 10:手柄图标本身 20×20,中心偏移 10,减去后图标中心与指针对齐,视觉上"握住"了手柄;Math.max(..., 1):防止宽高被拖成 0 或负数。
3.3 把工具挂到元素上
const rectangle = new shapes.standard.Rectangle(); rectangle.resize(100, 100); rectangle.position(100, 100); rectangle.addTo(graph); rectangle.findView(paper).addTools( new dia.ToolsView({ tools: [ new ResizeTool({ selector: 'body' }) ] }) );创建标准矩形并加入图之后,通过findView(paper)拿到它的视图,调用addTools传入一个dia.ToolsView,其中tools数组包含new ResizeTool({ selector: 'body' })。
这里的selector: 'body'是 Control 的关键选项:工具内部的虚线extras矩形需要知道"参考哪个子元素"来计算包围盒,body正是standard.Rectangle中矩形主体的 selector。框架通过relatedView.findNode(selector)找到该节点并计算其 bbox(见 Control.mjs),于是虚线框始终与矩形主体轮廓一致。
四、源码级原理:Control 工具如何工作
理解 cellTools/Control.mjs 有助于把这个示例推广到任意缩放/旋转/拖拽工具。
4.1 完整选项表
Control 继承自ToolView,其options默认为:
| 选项 | 默认值 | 作用 |
|---|---|---|
handleAttributes | null | 附加到handle节点上的额外 SVG 属性(如cursor、pointer-events),在updateHandle中逐个setAttribute写入 |
selector | 'root' | 计算extras虚线框包围盒时参考的元素子节点;示例传'body' |
padding | 6 | 虚线框相对参考节点 bbox 的向外扩展量;传NaN会被置为 0 |
scale | null | 手柄自身的缩放比例,getHandleTransformString会在translate后追加scale(...),适合应对 paper 缩放 |
重要约束:Control 的 markup 必须包含selector: 'handle'的节点,否则update()会抛出Control: markup selector 'handle' is required(Control.mjs)。
4.2 事件驱动的完整交互链路
Control 绑定了两类事件(Control.mjs):
- 自身事件:
mousedown/touchstart→onPointerDown;dblclick/dbltap→onPointerDblClick; - document 级事件:
mousemove/touchmove→onPointerMove;mouseup/touchend/touchcancel→onPointerUp。
一次完整拖拽的时序如下:
onPointerDown:先guard(evt)判断是否命中忽略区域;stopPropagation+preventDefault阻断画布自身的拖拽;调用paper.undelegateEvents()临时接管全局事件并delegateDocumentEvents();toggleExtras(true)显示虚线框;model.startBatch('control-move')开启批处理,让多次resize合并为一次事务(例如只触发一次 change 事件);onPointerMove(document 事件):把指针的客户端坐标依次转换为paper.clientToLocalPoint(clientX, clientY)的本地坐标、再通过model.getRelativePointFromAbsolute(coords)转换为元素相对坐标,随后调用可覆盖的this.setPosition(relatedView, relativeCoords, evt)并this.update()重算手柄与虚线框位置。示例正是在这里把相对坐标换算成model.resize(width, height);onPointerUp:paper.delegateEvents()归还事件、undelegateDocumentEvents()、blur()、toggleExtras(false)隐藏虚线框、stopBatch('control-move')结束批处理;onPointerDblClick:调用可覆盖的resetPosition(relatedView, evt)。示例未覆盖它(基类为空实现),因此双击无动作;若你的工具支持"双击复位",在此实现即可。
这套"down 时接管 document 事件、up 时归还"的机制,保证了即使指针拖出画布或浏览器窗口,拖拽也不会丢失。
4.3 getPosition 与坐标系统
getHandleTransformString(Control.mjs)决定手柄的实际渲染位置:
const relativePos = this.getPosition(relatedView, this); const translate = this.isOverlay() // 工具渲染在 paper 坐标系中 ? model.getAbsolutePointFromRelative(relativePos) // 工具渲染在 relatedView 的坐标系中 : relativePos;也就是说,Control 要求getPosition返回元素相对坐标,是否转换由工具处于 overlay 模式还是随元素变换决定。示例的手柄随元素一起平移旋转,所以直接使用相对坐标{ x: width, y: height }。
4.4 ToolsView 如何承载工具
ToolsView.mjs 是工具的容器:
configure(options):遍历tools数组,为每个ToolView实例调用tool.configure(relatedView, this)并挂到<g>容器(ToolsView.mjs);update():按顺序updateVisibility(),对可见工具首次render(),后续update()更新位置与外观;focusTool/blurTool:实现"聚焦一个工具、隐藏其他工具"的常见交互。
示例中rectangle.findView(paper).addTools(new dia.ToolsView({...}))即完成"视图 ↔ 工具容器 ↔ 工具实例"三者绑定,之后工具随元素视图的更新请求(TOOLSflag)自动刷新。
4.5 底层:Element.resize
拖拽最终落到 Element.mjs 的resize(width, height, opt)。它改变元素的size()结果并触发相关视图重绘——示例中getPosition恰好读取model.size()作为手柄位置,因此 resize 与手柄形成闭环:拖拽 → 写回尺寸 → 手柄位置更新 → 下一帧拖拽以新位置为基准。
五、扩展思路与注意事项
基于上面这套机制,可以很容易派生更多工具:
- 多方向缩放:将
getPosition改为返回四个边角/边中点(如{ x: 0, y: height }表示左下角),并给每个方向各建一个Control实例放进ToolsView的tools数组; - 同时显示辅助虚线框:复用
extras+selector机制,把参考节点换成其他子元素(如标签、图标),即可实现"包围盒预览"; - 双击复位:覆盖
resetPosition(view, evt),在onPointerDblClick时恢复元素初始尺寸; - 配合 paper 缩放:当调用
paper.scale()后,可通过scale选项让手柄大小不随画布缩放,避免手柄过大或过小。
实现时留意两点:一是setPosition中务必对最小尺寸做防护(示例用Math.max(x, 1));二是若手柄贴边布局,记得像示例一样扣除手柄自身尺寸的一半作为偏移,否则拖拽点与图标中心错位。
六、小结
resize-control-tool-js用约 75 行代码演示了 JointJS 元素缩放工具的完整范式:elementTools.Control.extend()覆写children(markup)、getPosition(手柄定位)与setPosition(坐标写回模型),再通过dia.ToolsView挂载到元素视图上。结合 Control.mjs 源码可见,其背后是完整的事件委托、坐标转换(clientToLocalPoint→getRelativePointFromAbsolute)、批处理(startBatch/stopBatch)与包围盒计算体系。
读者可以把这个示例当作模板:复制其结构、替换children中的图标与getPosition/setPosition逻辑,即可在几分钟内实现自定义的拖拽、旋转或其他编辑工具;想要深入了解,可继续阅读核心库中的 dia/ToolsView.mjs、dia/ToolView.mjs 与 dia/Element.mjs。
- 前端
- UI组件
【免费下载链接】joint
A proven SVG-based JavaScript diagramming library powering exceptional UIs
相关推荐
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考