-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: 移动端适配 #2317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: 移动端适配 #2317
Conversation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements mobile device adaptation by migrating event handling from MouseEvent to PointerEvent API, enabling unified input handling across touch, mouse, and pen devices. The changes introduce touch-specific features including long-press context menus (500ms), two-finger pinch-to-zoom gestures, and proper touch action configurations.
Key changes:
- Replaced MouseEvent with PointerEvent across core drag interactions, node/edge handlers, and plugin components
- Added mobile gesture support: long-press context menus and two-finger pinch zoom in CanvasOverlay
- Configured
touchAction: 'none'on interactive elements to prevent default touch behaviors
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/extension/src/tools/label/Label.tsx | Updated label drag handlers to use PointerEvent |
| packages/extension/src/components/selection-select/index.ts | Migrated selection box events to PointerEvent with touch action configuration |
| packages/extension/src/components/mini-map/index.ts | Converted mini-map viewport drag to PointerEvent |
| packages/core/src/view/text/BaseText.tsx | Changed text mouse handlers to pointer events |
| packages/core/src/view/overlay/CanvasOverlay.tsx | Added pinch zoom, long-press menu, and pointer event handling for canvas |
| packages/core/src/view/overlay/BezierAdjustOverlay.tsx | Updated Bezier anchor adjustment to PointerEvent |
| packages/core/src/view/node/BaseNode.tsx | Added long-press context menu for touch and migrated to PointerEvent |
| packages/core/src/view/edge/PolylineEdge.tsx | Updated polyline drag start to PointerEvent |
| packages/core/src/view/edge/BaseEdge.tsx | Added long-press menu and pointer event handling with touch styles |
| packages/core/src/view/edge/AdjustPoint.tsx | Converted adjust point handlers to PointerEvent |
| packages/core/src/view/Rotate.tsx | Updated rotation control to PointerEvent |
| packages/core/src/view/Control.tsx | Changed resize control handlers to PointerEvent |
| packages/core/src/view/Anchor.tsx | Migrated anchor handlers to PointerEvent |
| packages/core/src/util/drag.ts | Core drag utility updated with pointer capture, preventDefault, and PointerEvent |
| packages/core/src/tool/MultipleSelectTool.tsx | Updated multiple selection tool to PointerEvent |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 点拖拽进画布没有触发mousedown事件,没有startTime,用这个值做区分 | ||
| const isDragging = this.mouseUpDrag === false | ||
| const curTime = new Date().getTime() | ||
| console.log('6666', e) |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug console.log statement should be removed before merging to production.
| console.log('6666', e) |
packages/core/src/util/drag.ts
Outdated
| e.preventDefault() | ||
| const target = e.target as any | ||
| if (target && typeof target.setPointerCapture === 'function') { | ||
| target.setPointerCapture((e as PointerEvent).pointerId) |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant type casting. The parameter e is already typed as PointerEvent, so (e as PointerEvent).pointerId can be simplified to e.pointerId.
| target.setPointerCapture((e as PointerEvent).pointerId) | |
| target.setPointerCapture(e.pointerId) |
packages/core/src/util/drag.ts
Outdated
| if (this.isStopPropagation) e.stopPropagation() | ||
| const target = e.target as any | ||
| if (target && typeof target.releasePointerCapture === 'function') { | ||
| target.releasePointerCapture((e as PointerEvent).pointerId) |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant type casting. The parameter e is already typed as PointerEvent, so (e as PointerEvent).pointerId can be simplified to e.pointerId.
| target.releasePointerCapture((e as PointerEvent).pointerId) | |
| target.releasePointerCapture(e.pointerId) |
| eventCenter.emit(EventType.BLANK_MOUSEDOWN, { e: ev }) | ||
| } | ||
| // 为了处理画布移动的时候,编辑和菜单仍然存在的问题。 | ||
| this.clickHandler(ev) |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type mismatch: clickHandler expects MouseEvent but is being called with PointerEvent. The method signature should be updated to accept MouseEvent | PointerEvent to maintain consistency with the new pointer event handling.
| onMouseDown={this.handleMouseDown} | ||
| onPointerDown={this.handleMouseDown} | ||
| onContextMenu={this.handleContextMenu} | ||
| onWheel={this.handleWheelEvent} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mark 确认一下这里的onContextMenu和onWheel要不要处理?
packages/core/src/util/drag.ts
Outdated
|
|
||
| DOC.removeEventListener('mousemove', this.handleMouseMove, false) | ||
| DOC.removeEventListener('mouseup', this.handleMouseUp, false) | ||
| DOC.removeEventListener('pointermove', this.handleMouseMove as any, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any改成指定类型
| nodeConfig: OnDragNodeConfig | null = null | ||
| lf: LogicFlow | ||
| fakeNode: BaseNodeModel | null = null | ||
| docPointerMove?: (e: PointerEvent) => void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check docPointerMove和docPointerUp的必要性
| e.clientX, | ||
| e.clientY, | ||
| ) as HTMLElement | null | ||
| const inside = topEl === overlay || (topEl && overlay?.contains(topEl)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inside这个变量名需要改一下,现在看不是很语义化
| } | ||
| this.docPointerUp = (e: PointerEvent) => { | ||
| if (!this.nodeConfig) return | ||
| const overlay = this.lf.graphModel.rootEl.querySelector( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里的inside获取逻辑和上面PointerMove的逻辑是一样的,可以抽成一个通用的方法,减少重复代码
| onMouseUp: this.onDrop, | ||
| } | ||
| } | ||
| // eventMap() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check这里的eventMap还有没有在使用的地方 + 去掉会有什么影响
| } = graphModel | ||
| model.isDragging = true | ||
| const { clientX, clientY } = event! | ||
| const { clientX, clientY } = event as PointerEvent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里写as有点太临时了,是不是可以直接改IDragParams里的参数类型
| } = this.props | ||
| this.pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY }) | ||
| if (this.longPressTimer) { | ||
| clearTimeout(this.longPressTimer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
如果这个timeout是只有touch才触发的话,这个clearTimeout(this.longPressTimer)应该放下面if (ev.pointerType === 'touch') 里,否则mouse和pen也会走这个逻辑
| this.pinchStartDistance = Math.hypot(dx, dy) | ||
| this.pinchStartScale = transformModel.SCALE_X | ||
| // 双指操作下取消画布拖拽,避免与捏合缩放冲突 | ||
| this.stepDrag.cancelDrag() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔cancelDrag是不是应该放前面?先取消拖拽 再记录位置
需要check一下会不会出现画布先动一点点然后再固定缩放的情况
| name="canvas-overlay" | ||
| onWheel={this.zoomHandler} | ||
| onMouseDown={this.mouseDownHandler} | ||
| onPointerDown={this.mouseDownHandler} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
同上的问题,check方法名要不要统一
| if (shapeItem.callback) { | ||
| shapeItem.callback(this.lf, this.domContainer) | ||
| } | ||
| // try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check注释能不能删
| this.__container = container | ||
| this.__currentData = null // 当前展示的菜单所属元素的model数据 | ||
|
|
||
| lf.updateEditConfig({ stopZoomGraph: false }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
多余改动?
|
|
||
| startDrag(nodeConfig: OnDragNodeConfig) { | ||
| const { editConfigModel } = this.lf.graphModel | ||
| if (!editConfigModel?.isSilentMode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
如果这个if里逻辑太多的话,就改成反向判断吧
即if(editConfigModel?.isSilentMode) { ...do something; return; }
| window.document.removeEventListener('mouseup', this.stopDrag) | ||
| if (this.docPointerMove) { | ||
| window.document.removeEventListener('pointermove', this.docPointerMove) | ||
| this.docPointerMove = undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里为什么要置undefined?
| this.nodeConfig = nodeConfig | ||
| window.document.addEventListener('mouseup', this.stopDrag) | ||
| // 指针移动:根据命中结果判断是否在画布覆盖层上,驱动假节点创建/移动或清理 | ||
| this.docPointerMove = (e: PointerEvent) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这里是不是可以直接提到外面做单独的方法?
输入层选型:
选择PointerEvent来统一输入层,它是W3C推出的新一代指针事件,旨在统一所有 “指针输入设备” 的事件处理,包括触摸(手指),光标(鼠标),触控笔等。所以多端中相同功能只需写一套逻辑,不需要专门针对移动端写一套逻辑造成代码冗余。而且PointerEvent独特的指针特性使得处理起多指操作更为高效便捷。
解决方案:
1.移动端功能的逻辑和pc端功能的逻辑相同的话,就将原本的MouseEvent换成PointerEvent,直接复用B端函数即可以实现移动端适配(绝大部分功能可以正常实现,有些功能可能涉及到浏览器对PointerEvent的隐式捕获,导致指针事件指向的元素与预想不同,这就需要对函数进行改动)
2.当多端功能逻辑不同时(例如移动端长按出现菜单,pc端则是右键;移动端需要双指控制缩放,移动端则是滚轮实现),将事件类型换为PointerEvent只是第一步,具体改动则要根据具体实现逻辑实现。
3.当需要对B端函数进行改动时,要尽可能减少代码改动量,最大限度的复用B端函数
改动后移动端的效果:
1.支持节点的拖拽、双击修改文本,单击节点出现轮廓支持调整节点尺寸和旋转
2.支持边的连接、拖动调整折线中段和端点、贝塞尔曲线可以调整
3.支持画布的拖拽、双指缩放功能
4.支持插件中拖拽小地图来移动画布的功能、框选功能、长按节点/边/画布显示对应菜单