feat(minimap): support touch operation (#381)

* fix(core): touch end clear hover

* feat(minimap): support touch operation

* fix(demo): touch create comment should not trigger drag

* feat(demo): comment node support touch resize
This commit is contained in:
Louis Young 2025-06-16 19:42:38 +08:00 committed by GitHub
parent 83ae052705
commit fd423d9cb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 80 additions and 39 deletions

View File

@ -1,6 +1,6 @@
import { CSSProperties, type FC } from 'react'; import { CSSProperties, type FC } from 'react';
import { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor'; import { MouseTouchEvent, useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';
import type { CommentEditorModel } from '../model'; import type { CommentEditorModel } from '../model';
@ -26,23 +26,27 @@ export const ResizeArea: FC<IResizeArea> = (props) => {
const { selectNode } = useNodeRender(); const { selectNode } = useNodeRender();
const handleMouseDown = (mouseDownEvent: React.MouseEvent) => { const handleResizeStart = (
mouseDownEvent.preventDefault(); startResizeEvent: React.MouseEvent | React.TouchEvent | MouseEvent
mouseDownEvent.stopPropagation(); ) => {
MouseTouchEvent.preventDefault(startResizeEvent);
startResizeEvent.stopPropagation();
if (!onResize) { if (!onResize) {
return; return;
} }
const { resizing, resizeEnd } = onResize(); const { resizing, resizeEnd } = onResize();
model.setFocus(false); model.setFocus(false);
selectNode(mouseDownEvent); selectNode(startResizeEvent as React.MouseEvent);
playground.node.focus(); // 防止节点无法被删除 playground.node.focus(); // 防止节点无法被删除
const startX = mouseDownEvent.clientX; const { clientX: startX, clientY: startY } = MouseTouchEvent.getEventCoord(
const startY = mouseDownEvent.clientY; startResizeEvent as MouseEvent
);
const handleMouseMove = (mouseMoveEvent: MouseEvent) => { const handleResizing = (mouseMoveEvent: MouseEvent | TouchEvent) => {
const deltaX = mouseMoveEvent.clientX - startX; const { clientX: moveX, clientY: moveY } = MouseTouchEvent.getEventCoord(mouseMoveEvent);
const deltaY = mouseMoveEvent.clientY - startY; const deltaX = moveX - startX;
const deltaY = moveY - startY;
const delta = getDelta?.({ x: deltaX, y: deltaY }); const delta = getDelta?.({ x: deltaX, y: deltaY });
if (!delta || !resizing) { if (!delta || !resizing) {
return; return;
@ -50,16 +54,22 @@ export const ResizeArea: FC<IResizeArea> = (props) => {
resizing(delta); resizing(delta);
}; };
const handleMouseUp = () => { const handleResizeEnd = () => {
resizeEnd(); resizeEnd();
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleResizing);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleResizeEnd);
document.removeEventListener('click', handleMouseUp); document.removeEventListener('click', handleResizeEnd);
document.removeEventListener('touchmove', handleResizing);
document.removeEventListener('touchend', handleResizeEnd);
document.removeEventListener('touchcancel', handleResizeEnd);
}; };
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleResizing);
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleResizeEnd);
document.addEventListener('click', handleMouseUp); document.addEventListener('click', handleResizeEnd);
document.addEventListener('touchmove', handleResizing, { passive: false });
document.addEventListener('touchend', handleResizeEnd);
document.addEventListener('touchcancel', handleResizeEnd);
}; };
return ( return (
@ -67,7 +77,8 @@ export const ResizeArea: FC<IResizeArea> = (props) => {
className="workflow-comment-resize-area" className="workflow-comment-resize-area"
style={style} style={style}
data-flow-editor-selectable="false" data-flow-editor-selectable="false"
onMouseDown={handleMouseDown} onMouseDown={handleResizeStart}
onTouchStart={handleResizeStart}
/> />
); );
}; };

View File

@ -36,14 +36,17 @@ export const Comment = () => {
async (mouseEvent: React.MouseEvent<HTMLButtonElement>) => { async (mouseEvent: React.MouseEvent<HTMLButtonElement>) => {
setTooltipVisible(false); setTooltipVisible(false);
const canvasPosition = calcNodePosition(mouseEvent); const canvasPosition = calcNodePosition(mouseEvent);
// 创建节点 // create comment node - 创建节点
const node = document.createWorkflowNodeByType(WorkflowNodeType.Comment, canvasPosition); const node = document.createWorkflowNodeByType(WorkflowNodeType.Comment, canvasPosition);
// 等待节点渲染 // wait comment node render - 等待节点渲染
await delay(16); await delay(16);
// 选中节点 // select comment node - 选中节点
selectService.selectNode(node); selectService.selectNode(node);
// 开始拖拽 // maybe touch event - 可能是触摸事件
dragService.startDragSelectedNodes(mouseEvent); if (mouseEvent.detail !== 0) {
// start drag -开始拖拽
dragService.startDragSelectedNodes(mouseEvent);
}
}, },
[selectService, calcNodePosition, document, dragService] [selectService, calcNodePosition, document, dragService]
); );

View File

@ -145,6 +145,12 @@ export class PlaygroundLayer extends Layer<PlaygroundLayerOptions> {
// 这里必须监听 NORMAL_LAYER该图层最先触发 // 这里必须监听 NORMAL_LAYER该图层最先触发
PipelineLayerPriority.NORMAL_LAYER PipelineLayerPriority.NORMAL_LAYER
), ),
this.listenPlaygroundEvent('touchend', (e: TouchEvent) => {
this.options.hoverService?.clearHovered();
}),
this.listenPlaygroundEvent('touchcancel', (e: TouchEvent) => {
this.options.hoverService?.clearHovered();
}),
this.listenPlaygroundEvent( this.listenPlaygroundEvent(
'mousedown', 'mousedown',
(e: MouseEvent) => { (e: MouseEvent) => {

View File

@ -84,6 +84,12 @@ export const MinimapRender: React.FC<MinimapProps> = (props) => {
onMouseLeave={() => { onMouseLeave={() => {
service.setActivate(false); service.setActivate(false);
}} }}
onTouchStartCapture={() => {
service.setActivate(true);
}}
onTouchEndCapture={() => {
service.setActivate(false);
}}
></div> ></div>
</div> </div>
); );

View File

@ -4,7 +4,7 @@ import { Disposable, DisposableCollection, IPoint, Rectangle } from '@flowgram.a
import { FlowNodeEntity, FlowNodeTransformData } from '@flowgram.ai/document'; import { FlowNodeEntity, FlowNodeTransformData } from '@flowgram.ai/document';
import { FlowNodeBaseType } from '@flowgram.ai/document'; import { FlowNodeBaseType } from '@flowgram.ai/document';
import { FlowDocument } from '@flowgram.ai/document'; import { FlowDocument } from '@flowgram.ai/document';
import { EntityManager, PlaygroundConfigEntity } from '@flowgram.ai/core'; import { EntityManager, MouseTouchEvent, PlaygroundConfigEntity } from '@flowgram.ai/core';
import type { MinimapRenderContext, MinimapServiceOptions, MinimapCanvasStyle } from './type'; import type { MinimapRenderContext, MinimapServiceOptions, MinimapCanvasStyle } from './type';
import { MinimapDraw } from './draw'; import { MinimapDraw } from './draw';
@ -312,26 +312,29 @@ export class FlowMinimapService {
private addEventListeners(): void { private addEventListeners(): void {
this.canvas.addEventListener('wheel', this.handleWheel); this.canvas.addEventListener('wheel', this.handleWheel);
this.canvas.addEventListener('mousedown', this.handleStartDrag); this.canvas.addEventListener('mousedown', this.handleStartDrag);
this.canvas.addEventListener('touchstart', this.handleStartDrag, { passive: false });
this.canvas.addEventListener('mousemove', this.handleCursor); this.canvas.addEventListener('mousemove', this.handleCursor);
} }
private removeEventListeners(): void { private removeEventListeners(): void {
this.canvas.removeEventListener('wheel', this.handleWheel); this.canvas.removeEventListener('wheel', this.handleWheel);
this.canvas.removeEventListener('mousedown', this.handleStartDrag); this.canvas.removeEventListener('mousedown', this.handleStartDrag);
this.canvas.removeEventListener('touchstart', this.handleStartDrag);
this.canvas.removeEventListener('mousemove', this.handleCursor); this.canvas.removeEventListener('mousemove', this.handleCursor);
} }
private handleWheel = (event: WheelEvent): void => {}; private handleWheel = (event: WheelEvent): void => {};
private handleStartDrag = (event: MouseEvent): void => { private handleStartDrag = (event: MouseEvent | TouchEvent): void => {
event.preventDefault(); MouseTouchEvent.preventDefault(event);
event.stopPropagation(); event.stopPropagation();
const renderContext = this.createRenderContext(); const renderContext = this.createRenderContext();
const { viewRect, scale, offset } = renderContext; const { viewRect, scale, offset } = renderContext;
const canvasRect = this.canvas.getBoundingClientRect(); const canvasRect = this.canvas.getBoundingClientRect();
const { clientX, clientY } = MouseTouchEvent.getEventCoord(event);
const mousePoint: IPoint = { const mousePoint: IPoint = {
x: event.clientX - canvasRect.left, x: clientX - canvasRect.left,
y: event.clientY - canvasRect.top, y: clientY - canvasRect.top,
}; };
const viewRectOnCanvas = this.rectOnCanvas({ const viewRectOnCanvas = this.rectOnCanvas({
@ -344,20 +347,26 @@ export class FlowMinimapService {
} }
this.isDragging = true; this.isDragging = true;
this.dragStart = mousePoint; this.dragStart = mousePoint;
// click
document.addEventListener('mousemove', this.handleDragging); document.addEventListener('mousemove', this.handleDragging);
document.addEventListener('mouseup', this.handleEndDrag); document.addEventListener('mouseup', this.handleEndDrag);
// touch
document.addEventListener('touchmove', this.handleDragging, { passive: false });
document.addEventListener('touchend', this.handleEndDrag);
document.addEventListener('touchcancel', this.handleEndDrag);
}; };
private handleDragging = (event: MouseEvent): void => { private handleDragging = (event: MouseEvent | TouchEvent): void => {
if (!this.isDragging || !this.dragStart) return; if (!this.isDragging || !this.dragStart) return;
event.preventDefault(); MouseTouchEvent.preventDefault(event);
event.stopPropagation(); event.stopPropagation();
const renderContext = this.createRenderContext(); const renderContext = this.createRenderContext();
const { scale } = renderContext; const { scale } = renderContext;
const canvasRect = this.canvas.getBoundingClientRect(); const canvasRect = this.canvas.getBoundingClientRect();
const mouseX = event.clientX - canvasRect.left; const { clientX, clientY } = MouseTouchEvent.getEventCoord(event);
const mouseY = event.clientY - canvasRect.top; const mouseX = clientX - canvasRect.left;
const mouseY = clientY - canvasRect.top;
const deltaX = (mouseX - this.dragStart.x) / scale; const deltaX = (mouseX - this.dragStart.x) / scale;
const deltaY = (mouseY - this.dragStart.y) / scale; const deltaY = (mouseY - this.dragStart.y) / scale;
@ -368,11 +377,16 @@ export class FlowMinimapService {
this.render(); this.render();
}; };
private handleEndDrag = (event: MouseEvent): void => { private handleEndDrag = (event: MouseEvent | TouchEvent): void => {
event.preventDefault(); MouseTouchEvent.preventDefault(event);
event.stopPropagation(); event.stopPropagation();
// click
document.removeEventListener('mousemove', this.handleDragging); document.removeEventListener('mousemove', this.handleDragging);
document.removeEventListener('mouseup', this.handleEndDrag); document.removeEventListener('mouseup', this.handleEndDrag);
// touch
document.removeEventListener('touchmove', this.handleDragging);
document.removeEventListener('touchend', this.handleEndDrag);
document.removeEventListener('touchcancel', this.handleEndDrag);
this.isDragging = false; this.isDragging = false;
this.dragStart = undefined; this.dragStart = undefined;
this.setActivate(this.isMouseInCanvas(event)); this.setActivate(this.isMouseInCanvas(event));
@ -404,13 +418,14 @@ export class FlowMinimapService {
} }
}; };
private isMouseInCanvas(event: MouseEvent): boolean { private isMouseInCanvas(event: MouseEvent | TouchEvent): boolean {
const canvasRect = this.canvas.getBoundingClientRect(); const canvasRect = this.canvas.getBoundingClientRect();
const { clientX, clientY } = MouseTouchEvent.getEventCoord(event);
return ( return (
event.clientX >= canvasRect.left && clientX >= canvasRect.left &&
event.clientX <= canvasRect.right && clientX <= canvasRect.right &&
event.clientY >= canvasRect.top && clientY >= canvasRect.top &&
event.clientY <= canvasRect.bottom clientY <= canvasRect.bottom
); );
} }