B: Tentative fix for longpress on iOS
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
Frontend CI / notify-failure (push) Blocked by required conditions

This commit is contained in:
Ivan 2025-07-01 12:23:47 +03:00
parent 91186b0e2e
commit 4207a52b2d
5 changed files with 86 additions and 17 deletions

View File

@ -19,18 +19,13 @@ export function useContextMenu() {
const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem); const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem);
const { addSelectedNodes } = useStoreApi().getState(); const { addSelectedNodes } = useStoreApi().getState();
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) { function openContextMenu(node: OssNode, clientX: number, clientY: number) {
event.preventDefault();
event.stopPropagation();
addSelectedNodes([node.id]); addSelectedNodes([node.id]);
setMenuProps({ setMenuProps({
item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null, item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null,
cursorX: event.clientX, cursorX: clientX,
cursorY: event.clientY cursorY: clientY
}); });
setIsOpen(true); setIsOpen(true);
setHoverOperation(null); setHoverOperation(null);
} }
@ -42,7 +37,7 @@ export function useContextMenu() {
return { return {
isOpen, isOpen,
menuProps, menuProps,
handleContextMenu, openContextMenu,
hideContextMenu hideContextMenu
}; };
} }

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState } from 'react'; import { useRef, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow'; 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 { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { promptText } from '@/utils/labels'; import { promptText } from '@/utils/labels';
import { isIOS } from '@/utils/utils';
import { useDeleteBlock } from '../../../backend/use-delete-block'; import { useDeleteBlock } from '../../../backend/use-delete-block';
import { useMutatingOss } from '../../../backend/use-mutating-oss'; import { useMutatingOss } from '../../../backend/use-mutating-oss';
@ -24,7 +25,7 @@ import { OssNodeTypes } from './graph/oss-node-types';
import { CoordinateDisplay } from './coordinate-display'; import { CoordinateDisplay } from './coordinate-display';
import { useOssFlow } from './oss-flow-context'; import { useOssFlow } from './oss-flow-context';
import { ToolbarOssGraph } from './toolbar-oss-graph'; import { ToolbarOssGraph } from './toolbar-oss-graph';
import { useDragging } from './use-dragging'; // import { useDragging } from './use-dragging';
import { useGetLayout } from './use-get-layout'; import { useGetLayout } from './use-get-layout';
export const flowOptions = { export const flowOptions = {
@ -64,8 +65,11 @@ export function OssFlow() {
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation); const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const showEditBlock = useDialogsStore(state => state.showEditBlock); const showEditBlock = useDialogsStore(state => state.showEditBlock);
const { isOpen: isContextMenuOpen, menuProps, handleContextMenu, hideContextMenu } = useContextMenu(); const { isOpen: isContextMenuOpen, menuProps, openContextMenu, hideContextMenu } = useContextMenu();
const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu }); // const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu });
const longPressTimeout = useRef<NodeJS.Timeout | null>(null);
const longPressTarget = useRef<EventTarget | null>(null);
function handleSavePositions() { function handleSavePositions() {
void updateLayout({ itemID: schema.id, data: getLayout() }); void updateLayout({ itemID: schema.id, data: getLayout() });
@ -123,6 +127,7 @@ export function OssFlow() {
} }
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) { function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
console.log('handleNodeDoubleClick', event, node);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -183,6 +188,56 @@ export function OssFlow() {
setMouseCoords(targetPosition); setMouseCoords(targetPosition);
} }
function handleNodeContextMenu(event: React.MouseEvent<Element>, 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 ( return (
<div tabIndex={-1} className='relative' onMouseMove={showCoordinates ? handleMouseMove : undefined}> <div tabIndex={-1} className='relative' onMouseMove={showCoordinates ? handleMouseMove : undefined}>
{showCoordinates ? <CoordinateDisplay mouseCoords={mouseCoords} className='absolute top-1 right-2' /> : null} {showCoordinates ? <CoordinateDisplay mouseCoords={mouseCoords} className='absolute top-1 right-2' /> : null}
@ -209,16 +264,20 @@ export function OssFlow() {
showGrid={showGrid} showGrid={showGrid}
onClick={hideContextMenu} onClick={hideContextMenu}
onNodeDoubleClick={handleNodeDoubleClick} onNodeDoubleClick={handleNodeDoubleClick}
onNodeContextMenu={handleContextMenu} onNodeContextMenu={handleNodeContextMenu}
onContextMenu={hideContextMenu} onContextMenu={hideContextMenu}
onNodeDragStart={handleDragStart} onTouchStart={handleTouchStart}
onNodeDrag={handleDrag} onTouchEnd={handleTouchEnd}
onNodeDragStop={handleDragStop} onTouchMove={handleTouchMove}
/> />
</div> </div>
); );
} }
// onNodeDragStart={handleDragStart}
// onNodeDrag={handleDrag}
// onNodeDragStop={handleDragStop}
// -------- Internals -------- // -------- Internals --------
function extractBlockParent(selectedItems: IOssItem[]): number | null { function extractBlockParent(selectedItems: IOssItem[]): number | null {
if (selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK) { if (selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK) {

View File

@ -42,6 +42,7 @@ export function useDragging({ hideContextMenu }: DraggingProps) {
} }
function handleDragStart(event: React.MouseEvent, target: Node) { function handleDragStart(event: React.MouseEvent, target: Node) {
console.log('handleDragStart', event);
if (event.shiftKey) { if (event.shiftKey) {
setContainMovement(true); setContainMovement(true);
applyContainMovement([target.id, ...selected], true); applyContainMovement([target.id, ...selected], true);

View File

@ -11,6 +11,7 @@ export const PARAMETER = {
notificationDelay: 300, // milliseconds delay for notifications notificationDelay: 300, // milliseconds delay for notifications
zoomDuration: 500, // milliseconds animation duration zoomDuration: 500, // milliseconds animation duration
navigationPopupDelay: 300, // milliseconds delay for navigation popup 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 moveDuration: 500, // milliseconds - duration of move animation

View File

@ -192,3 +192,16 @@ export function removeTags(target?: string): string {
export function prepareTooltip(text: string, hotkey?: string) { export function prepareTooltip(text: string, hotkey?: string) {
return hotkey ? `<kbd>[${hotkey}]</kbd><br/>${text}` : text; return hotkey ? `<kbd>[${hotkey}]</kbd><br/>${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)
);
}