From 4207a52b2d096aea539e31e73f15a3e62afc011d Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:23:47 +0300 Subject: [PATCH] B: Tentative fix for longpress on iOS --- .../context-menu/use-context-menu.ts | 13 +--- .../oss-page/editor-oss-graph/oss-flow.tsx | 75 +++++++++++++++++-- .../editor-oss-graph/use-dragging.tsx | 1 + rsconcept/frontend/src/utils/constants.ts | 1 + rsconcept/frontend/src/utils/utils.ts | 13 ++++ 5 files changed, 86 insertions(+), 17 deletions(-) diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/use-context-menu.ts b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/use-context-menu.ts index b07a5b95..cb62ff79 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/use-context-menu.ts +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/use-context-menu.ts @@ -19,18 +19,13 @@ export function useContextMenu() { const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem); const { addSelectedNodes } = useStoreApi().getState(); - function handleContextMenu(event: React.MouseEvent, node: OssNode) { - event.preventDefault(); - event.stopPropagation(); - + function openContextMenu(node: OssNode, clientX: number, clientY: number) { addSelectedNodes([node.id]); - setMenuProps({ item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null, - cursorX: event.clientX, - cursorY: event.clientY + cursorX: clientX, + cursorY: clientY }); - setIsOpen(true); setHoverOperation(null); } @@ -42,7 +37,7 @@ export function useContextMenu() { return { isOpen, menuProps, - handleContextMenu, + openContextMenu, hideContextMenu }; } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx index 925b4095..841b8a34 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import clsx from 'clsx'; import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow'; @@ -8,6 +8,7 @@ import { useMainHeight } from '@/stores/app-layout'; import { useDialogsStore } from '@/stores/dialogs'; import { PARAMETER } from '@/utils/constants'; import { promptText } from '@/utils/labels'; +import { isIOS } from '@/utils/utils'; import { useDeleteBlock } from '../../../backend/use-delete-block'; import { useMutatingOss } from '../../../backend/use-mutating-oss'; @@ -24,7 +25,7 @@ import { OssNodeTypes } from './graph/oss-node-types'; import { CoordinateDisplay } from './coordinate-display'; import { useOssFlow } from './oss-flow-context'; import { ToolbarOssGraph } from './toolbar-oss-graph'; -import { useDragging } from './use-dragging'; +// import { useDragging } from './use-dragging'; import { useGetLayout } from './use-get-layout'; export const flowOptions = { @@ -64,8 +65,11 @@ export function OssFlow() { const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation); const showEditBlock = useDialogsStore(state => state.showEditBlock); - const { isOpen: isContextMenuOpen, menuProps, handleContextMenu, hideContextMenu } = useContextMenu(); - const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu }); + const { isOpen: isContextMenuOpen, menuProps, openContextMenu, hideContextMenu } = useContextMenu(); + // const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu }); + + const longPressTimeout = useRef(null); + const longPressTarget = useRef(null); function handleSavePositions() { void updateLayout({ itemID: schema.id, data: getLayout() }); @@ -123,6 +127,7 @@ export function OssFlow() { } function handleNodeDoubleClick(event: React.MouseEvent, node: OssNode) { + console.log('handleNodeDoubleClick', event, node); event.preventDefault(); event.stopPropagation(); @@ -183,6 +188,56 @@ export function OssFlow() { setMouseCoords(targetPosition); } + function handleNodeContextMenu(event: React.MouseEvent, node: OssNode) { + event.preventDefault(); + event.stopPropagation(); + openContextMenu(node, event.clientX, event.clientY); + } + + function handleTouchStart(event: React.TouchEvent) { + console.log('handleTouchStart', event); + if (!isIOS() || event.touches.length !== 1) { + return; + } + + // Long-press support for iOS/iPadOS + const touch = event.touches[0]; + longPressTarget.current = touch.target; + longPressTimeout.current = setTimeout(() => { + let targetID = null; + let element = touch.target as HTMLElement | null; + while (element) { + if (element?.getAttribute?.('data-id')) { + targetID = element.getAttribute('data-id'); + break; + } + element = element.parentElement; + } + if (targetID) { + const targetNode = nodes.find(node => node.id === targetID); + if (targetNode) { + openContextMenu(targetNode, touch.clientX, touch.clientY); + } + } + }, PARAMETER.ossContextMenuDuration); + } + + function handleTouchEnd() { + if (!isIOS()) return; + if (longPressTimeout.current) { + clearTimeout(longPressTimeout.current); + longPressTimeout.current = null; + } + } + + function handleTouchMove() { + if (!isIOS()) return; + if (longPressTimeout.current) { + clearTimeout(longPressTimeout.current); + longPressTimeout.current = null; + } + } + return (
{showCoordinates ? : null} @@ -209,16 +264,20 @@ export function OssFlow() { showGrid={showGrid} onClick={hideContextMenu} onNodeDoubleClick={handleNodeDoubleClick} - onNodeContextMenu={handleContextMenu} + onNodeContextMenu={handleNodeContextMenu} onContextMenu={hideContextMenu} - onNodeDragStart={handleDragStart} - onNodeDrag={handleDrag} - onNodeDragStop={handleDragStop} + onTouchStart={handleTouchStart} + onTouchEnd={handleTouchEnd} + onTouchMove={handleTouchMove} />
); } +// onNodeDragStart={handleDragStart} +// onNodeDrag={handleDrag} +// onNodeDragStop={handleDragStop} + // -------- Internals -------- function extractBlockParent(selectedItems: IOssItem[]): number | null { if (selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK) { diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-dragging.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-dragging.tsx index 2d680279..77ebfb07 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-dragging.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-dragging.tsx @@ -42,6 +42,7 @@ export function useDragging({ hideContextMenu }: DraggingProps) { } function handleDragStart(event: React.MouseEvent, target: Node) { + console.log('handleDragStart', event); if (event.shiftKey) { setContainMovement(true); applyContainMovement([target.id, ...selected], true); diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index 16b543d0..09871961 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -11,6 +11,7 @@ export const PARAMETER = { notificationDelay: 300, // milliseconds delay for notifications zoomDuration: 500, // milliseconds animation duration navigationPopupDelay: 300, // milliseconds delay for navigation popup + ossContextMenuDuration: 500, // milliseconds - duration of long-press to trigger context menu on iOS/iPadOS moveDuration: 500, // milliseconds - duration of move animation diff --git a/rsconcept/frontend/src/utils/utils.ts b/rsconcept/frontend/src/utils/utils.ts index 41884195..fe11fe15 100644 --- a/rsconcept/frontend/src/utils/utils.ts +++ b/rsconcept/frontend/src/utils/utils.ts @@ -192,3 +192,16 @@ export function removeTags(target?: string): string { export function prepareTooltip(text: string, hotkey?: string) { return hotkey ? `[${hotkey}]
${text}` : text; } + +/** + * Utility to detect iOS/iPadOS. + */ +export function isIOS() { + if (typeof navigator === 'undefined') { + return false; + } + return ( + /iPad|iPhone|iPod/.test(navigator.userAgent) || + (navigator.userAgent.includes('Macintosh') && 'ontouchend' in document) + ); +}