天猫网站的建设,旅游景区网站源码,手机上怎么做网站创业,wordpress嵌入淘宝商品系列文章
Svg Flow Editor 原生svg流程图编辑器#xff08;一#xff09;
说明 这项目也是我第一次写TS代码哈#xff0c;现在还被绕在类型中头昏脑胀#xff0c;更新可能会慢点#xff0c;大家见谅~ 目前实现的功能#xff1a;1. 元件的创建、移动、形变#xff1b;2…系列文章
Svg Flow Editor 原生svg流程图编辑器一
说明 这项目也是我第一次写TS代码哈现在还被绕在类型中头昏脑胀更新可能会慢点大家见谅~ 目前实现的功能1. 元件的创建、移动、形变2. command API3. eventBus listener 事件监听4. register 自定义右键菜单 5. 多实例化 6. 文本创建与跟随。 实现形变锚点 形变锚点的添加思想与连接锚点类似但是是通过动态创建实现是在commonEvent中处理哈因为每一个创建的svg元组都需要实现该效果 // click 需要添加形变锚点public click(e: Event, graph: IGraph) {const nodeID graph.getID();// 1. 先看是否目前选中的就是当前节点是的话直接返回防止频繁点击元素 执行dom操作const selectedID this.getCurrentSelectedNodeID();if (selectedID selectedID nodeID) return;// 2. 创建形变锚点this.draw.createFormatAnchorPoint(e, graph);}
核心方法
const points [];/*** 顺序如下* 1 2 3* 8 4* 7 6 5*/points.push({ cursor: nwse-resize, x, y });points.push({ cursor: ns-resize, x: x width / 2, y: y });points.push({ cursor: nesw-resize, x: x width, y: y });points.push({ cursor: ew-resize, x: x width, y: y height / 2 });points.push({ cursor: nwse-resize, x: x width, y: y height });points.push({ cursor: ns-resize, x: x width / 2, y: y height });points.push({ cursor: nesw-resize, x: x, y: y height });points.push({ cursor: ew-resize, x: x, y: y height / 2 });// 循环创建 rectpoints.forEach(({ x, y, cursor }) {const rect document.createElementNS(xmlns, rect);rect.setAttribute(x, (x - 4).toString());rect.setAttribute(y, (y - 4).toString());rect.setAttribute(width, 8);rect.setAttribute(height, 8);rect.setAttribute(fill, red);// ts-ignorerect.style.cursor cursor;// 添加拖动事件rect.addEventListener(mousedown, () {console.log(形变锚点事件);}); 而形变事件则是通过创建的锚点事件实现 // 形变事件rect.addEventListener(mousedown, () this.handleFormatMousedown());rect.addEventListener(mouseup, () this.handleFormatMouseup());
元件太小拖动不流畅优化 正常情况下通过 mousedown、mousemove、mouseup 三个事件监听的移动拖拽事件会导致元件太小失焦从而不能实现流畅的拖拽因此不适用该思路实现 实现思路通过监听down 事件使得根元素监听move事件因为根元素的move是不会收到元件大小的影响可以实现流畅拖动。
// 形变事件处理private handleFormatMousedown(_e: Event, rect: Element, graph: IGraph) {const svg this.getSvg(this.getGraph().getSvgXmlns());const element graph.getElement();const nodeID graph.getID();const xmlns graph.getXmlns();const { offsetX, offsetY } _e as MouseEvent;const startX offsetX; // 初始位置const startY offsetY; // 初始位置var width 0; // 初始宽度var height 0; // 初始高度// 记录初始位置(这恶鬼也要根据targetName动态获取)switch (element.tagName) {case rect:width Number(element.getAttribute(width));height Number(element.getAttribute(height));break;case circle:width Number(element.getAttribute(r)) * 2;height width;break;case ellipse:width Number(element.getAttribute(rx)) * 2;height Number(element.getAttribute(ry)) * 2;break;default:break;}// ts-ignore pointer-events: none; 在拖动过程中使得 rect 不能响应事件才能往回托element.style[pointer-events] none;// 实现内部函数才能获取参数const handleMousedown (e: Event) {/*** 同时这个的宽高变化还要根据是从哪一个边拖拽进行不同的宽高变化*/const { offsetX, offsetY } e as MouseEvent;// 设置 element 的宽高const diffX offsetX - startX;const diffY offsetY - startY;// ts-ignore 获取变化方向const cursor rect.style.cursor;switch (cursor) {case ns-resize:// 只进行上下高度调整element.setAttribute(height, (height diffY).toString());break;case ew-resize:// 只进行左右宽度调整element.setAttribute(width, (width diffX).toString());break;default:// 其他四个方向宽高都调整element.setAttribute(width, (width diffX).toString());element.setAttribute(height, (height diffY).toString());break;}// 更新所有锚点this.updateFormatAnchorPoint();this.updateLinkAnchorPoint(nodeID, element, xmlns);e.preventDefault();e.stopPropagation();}; 临界值优化 // 临界值处理if (resultX MIN_WIDTH) width MIN_WIDTH;if (resultX MAX_WIDTH) width MAX_WIDTH;if (resultY MIN_HEIGHT) height MIN_HEIGHT;if (resultY MAX_HEIGHT) height MAX_HEIGHT;
反方向拖动优化 反向拖动的核心就是处理定位坐标及宽高的关系 还有圆形椭圆的圆心坐标目前没有想到好的实现思路如果大家有想法可以留言交流~ 实现旋转锚点 旋转这块还有些技术问题还没攻克哈特别是旋转了之后的移动点线的创建都是问题大家有思路可以留言讨论。
实现move移动 移动的核心就是 mousedown 记录点击位置在move中起始点移动了多少位置元件的中心页移动多少位置即可特别注意rect 的定位是左上角circle的定位是圆心因此不能直接将move的坐标直接赋给元件。【包括元件的移动太快也会导致失焦也可以考虑使用根元素move方法实现】 核心方法
// dowm 记录初始位置public mousedown(e: MouseEvent, graph: IGraph) {const { offsetX, offsetY } e;const { x, y } this.getElementPosition(graph.getElement());this.startX offsetX;this.startY offsetY;this.graphX x;this.graphY y;this.move true;}// 移动更新位置public mousemove(e: MouseEvent, graph: IGraph) {if (!this.move) return;// 这个是新的 offset直接与旧的 offset 进行运算即可得到差值与当前位置做计算即可const { offsetX, offsetY } e;// 计算差值const diffX offsetX - this.startX;const diffY offsetY - this.startY;graph.position.call(graph, this.graphX diffX, this.graphY diffY);}// 弹起重置参数public mouseup(e: Event, graph: IGraph) {this.resetDefault();} 实现文本 使用div创建contenteditable的元素
// 2. 当前位置创建 contentEditorabel divconst element graph.getElement();// 获取当前宽度 高度 位置坐标const width graph.getWidth();const height graph.getHeight();const x graph.getX();const y graph.getY();const left element.tagName rect ? x px : x - width / 2 px;const top element.tagName rect ? y px : y - height / 2 px;const div this.draw.getHTMLElement(div);div.classList.add(svg-flow-contenteditable);div.style.width width px;div.style.height height px;div.style.left left;div.style.top top;// 内部创建div实现编辑,才能实现const t this.draw.getHTMLElement(div);t.setAttribute(contenteditable, true);t.style.width width px;div.appendChild(t);// 添加到根元素this.draw.addTo(this.draw.getRootElement(), div);// 自动获取焦点t.focus(); 并且绑定失焦事件 // 失去焦点事件t.addEventListener(blur, () {// 获取用户输入const div document.querySelector(div[classsvg-flow-contenteditable]) as HTMLDivElement;const text div.innerText;// 将内容添加到 graph 元素上// 清空内容this.clearContenteditable();});// 添加enter事件t.addEventListener(keydown, (e: KeyboardEvent) {if (e.code ! Enter) return;// 执行 enter 结束t.blur();}); 跟随移动 // 重新渲染文本位置public updateTextPosition(graph: IGraph) {const element graph.getElement();const x graph.getX();const y graph.getY();// 获取文本节点const textNode element.parentNode?.parentNode?.querySelector(text);textNode?.setAttribute(x, x.toString());textNode?.setAttribute(y, (y 5).toString());} user-select: none;记得添加上这个属性哈不然在移动过程中会选中文字导致拖动卡顿异常pointer-events: none; 文本不响应鼠标事件不然有了文本后拖拽也会有问题。 右键菜单 在template 中定义好html结构使用innerHTML添加到div 中再将div添加到根元素上 // svg 右键事件public handleSvgContextmenu(e: Event) {const { offsetX, offsetY } e as PointerEvent;// 先清空右键菜单const menu this.getContextmenu();if (menu) {(menu as HTMLDivElement).style.left offsetX px;(menu as HTMLDivElement).style.top offsetY px;e.stopPropagation();e.preventDefault();return;}// 不存在则 创建svg右键菜单const div document.createElement(div);div.classList.add(contextmenu-box);div.style.left offsetX px;div.style.top offsetY px;div.innerHTML contextmenu;// 添加事件div.querySelectorAll(div[classsvg-flow-contextmenu-item]).forEach((i) {// 获取commandi.addEventListener(click, () this.handleContextmenu(i.getAttribute(command) as string));});// 右键的右键不影响事件div.addEventListener(contextmenu, (e) {e.stopPropagation();e.preventDefault();});setTimeout(() this.root.appendChild(div));e.stopPropagation();e.preventDefault();} 实现用户自定义右键 // 自定义右键菜单SFEditor.register.contextMenuList [{title: 测试右键菜单,callback: () {console.log(点击了自定义菜单);},},];
// 判断用户的自定义事件nextTick(() {const { contextMenuList } this.register;if (!contextMenuList.length) return;// 将用户的自定义事件添加到 菜单中contextMenuList.forEach(({ title, callback }) {const d document.createElement(div);d.classList.add(svg-flow-contextmenu-item);const spanIcon document.createElement(span);spanIcon.innerText title as string;d.appendChild(spanIcon);d.addEventListener(click, (e: Event) {callback callback(e);});div.querySelector(.svg-flow-contextmenu-svg)?.appendChild(d);});}); 矫正右键菜单位置
// 右键菜单唤起事件需要矫正位置private correctContextMenuPosition(div: HTMLDivElement, e: Event) {// 获取父元素的宽高 取 this.rootconst { clientHeight, clientWidth } this.root;// 获取自身的宽高const width div.clientWidth;const height div.clientHeight;const { offsetX, offsetY } e as PointerEvent;var left offsetX;var top offsetY;// 如果 offsetX width 超过父元素的宽度则令left offsetX-widthif (offsetX width clientWidth) left offsetX - width;if (offsetY height clientHeight) top offsetY - height;div.style.left left px;div.style.top top px;}
实现多实例化 多实例的核心是创建新对象 // 1. 一定要基于创建的 构建的实例对象进行操作const editor new SFEditor(.flow-box);Reflect.set(window, editor, editor); // 这个是外部调用的关键// 2. 创建yuanjianeditor.Rect(200, 200);const editor2 new SFEditor(.flow-demo2);// 3. 执行动作editor2.command.executeAddGraph({type: rect,width: 200,height: 200,}); 在每次创建实例时都会生成新的div根节点、svg根节点并且要求在操作dom时都需要加上限制不允许直接使用 document.querySelector 应该限制在当前节点下进行dom操作 防止多实例dom相互影响。 总结 目前已经可以进行元件的基本操作实现通过API调用实现响应功能、并且支持事件监听、用户事件注册等但是还是少了些东西。例如线条、旋转、辅助线等本来想一起放在本章节写的但是有些技术难点还是没有想到实现方式就留着下一节吧。 ts写起来确实要繁琐些在项目构建之初我将 svg 创建的元素都设置为 Element 类型后来在设置属性、进行事件响应的时候总是有问题后面又修改了属性类型为SVGSVGElement项目初期也没考虑多实例化后面又改动了项目index的结构同时也为了实现项目事件监听回调在多处进行事件埋点整体的工作量也是挺大的所以更新慢了些大家见谅哈~