ソースを参照

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
Louis Young 6 ヶ月 前
コミット
fd423d9cb5

+ 29 - 18
apps/demo-free-layout/src/components/comment/components/resize-area.tsx

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

+ 8 - 5
apps/demo-free-layout/src/components/tools/comment.tsx

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

+ 6 - 0
packages/canvas-engine/core/src/core/layer/playground-layer.ts

@@ -145,6 +145,12 @@ export class PlaygroundLayer extends Layer<PlaygroundLayerOptions> {
         // 这里必须监听 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(
         'mousedown',
         (e: MouseEvent) => {

+ 6 - 0
packages/plugins/minimap-plugin/src/component.tsx

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

+ 31 - 16
packages/plugins/minimap-plugin/src/service.ts

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