修复预览节点松开选择手势
按下阶段仅记录节点手势与命中目标 在干净松开时选择并处理拖动取消失焦边界 阻止拖动后的嵌套 click 重新选择
This commit is contained in:
@@ -87,15 +87,19 @@ export function PreviewWorkspace({
|
||||
logicalSize,
|
||||
spaceHeld,
|
||||
tree,
|
||||
previewRef: viewportElementRef,
|
||||
viewportRef,
|
||||
});
|
||||
const {
|
||||
onNodePointerDown,
|
||||
onNodePointerMove,
|
||||
onNodePointerUp,
|
||||
onNodePointerCancel,
|
||||
onNodeResizePointerDown,
|
||||
onNodeResizePointerMove,
|
||||
onNodeResizePointerUp,
|
||||
onNodeResizePointerCancel,
|
||||
consumeNodeClick,
|
||||
} = nodeInteractions;
|
||||
|
||||
const setViewport = useCallback((next: CanvasViewport) => {
|
||||
@@ -389,13 +393,16 @@ export function PreviewWorkspace({
|
||||
fontFaces: canvas.fontFaces,
|
||||
}}
|
||||
onSelectNode={canvas.selectNode}
|
||||
consumeNodeClick={consumeNodeClick}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
onNodePointerDown={onNodePointerDown}
|
||||
onNodePointerMove={onNodePointerMove}
|
||||
onNodePointerUp={onNodePointerUp}
|
||||
onNodePointerCancel={onNodePointerCancel}
|
||||
onNodeResizePointerDown={onNodeResizePointerDown}
|
||||
onNodeResizePointerMove={onNodeResizePointerMove}
|
||||
onNodeResizePointerUp={onNodeResizePointerUp}
|
||||
onNodeResizePointerCancel={onNodeResizePointerCancel}
|
||||
onSelectExclusiveChild={canvas.selectExclusiveChild}
|
||||
viewportScale={viewport.scale}
|
||||
/>
|
||||
|
||||
+26
-6
@@ -35,6 +35,7 @@ type UiTreeRendererProps = {
|
||||
selectedNodeId: NodeId | null;
|
||||
resources: PreviewComponentResources;
|
||||
onSelectNode: (id: NodeId) => void;
|
||||
consumeNodeClick?: () => boolean;
|
||||
onNodeContextMenu: (
|
||||
event: ReactMouseEvent<HTMLDivElement>,
|
||||
node: UiNode,
|
||||
@@ -43,6 +44,7 @@ type UiTreeRendererProps = {
|
||||
onNodePointerDown: NodePointerDown;
|
||||
onNodePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onNodePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onNodePointerCancel?: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onNodeResizePointerDown: (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
node: UiNode,
|
||||
@@ -50,6 +52,9 @@ type UiTreeRendererProps = {
|
||||
) => void;
|
||||
onNodeResizePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onNodeResizePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onNodeResizePointerCancel?: (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
onSelectExclusiveChild: (nodeId: NodeId) => void;
|
||||
viewportScale: number;
|
||||
};
|
||||
@@ -80,13 +85,16 @@ function RenderNode({
|
||||
selectedNodeId,
|
||||
resources,
|
||||
onSelectNode,
|
||||
consumeNodeClick = () => false,
|
||||
onNodeContextMenu,
|
||||
onNodePointerDown,
|
||||
onNodePointerMove,
|
||||
onNodePointerUp,
|
||||
onNodePointerCancel = onNodePointerUp,
|
||||
onNodeResizePointerDown,
|
||||
onNodeResizePointerMove,
|
||||
onNodeResizePointerUp,
|
||||
onNodeResizePointerCancel = onNodeResizePointerUp,
|
||||
onSelectExclusiveChild,
|
||||
viewportScale,
|
||||
}: Omit<UiTreeRendererProps, 'tree'> & {
|
||||
@@ -109,6 +117,7 @@ function RenderNode({
|
||||
|
||||
const isEditorOverlay = renderMode === 'editor-overlay';
|
||||
const isFrameVisible = isEditorOverlay || showFrame;
|
||||
const hasDirectPointerGesture = !isRoot && !parentContainer;
|
||||
const exclusiveVisibleChildId =
|
||||
node.children_display_mode === 'Exclusive'
|
||||
? resolveExclusiveVisibleChildId(node.children, hiddenNodeIds)
|
||||
@@ -137,10 +146,18 @@ function RenderNode({
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSelectNode(node.id);
|
||||
}}
|
||||
onClick={
|
||||
hasDirectPointerGesture
|
||||
? (event) => {
|
||||
event.stopPropagation();
|
||||
consumeNodeClick();
|
||||
}
|
||||
: (event) => {
|
||||
event.stopPropagation();
|
||||
if (consumeNodeClick()) return;
|
||||
onSelectNode(node.id);
|
||||
}
|
||||
}
|
||||
onContextMenu={(event) => {
|
||||
if (!isEditorOverlay) return;
|
||||
event.preventDefault();
|
||||
@@ -153,7 +170,7 @@ function RenderNode({
|
||||
}
|
||||
onPointerMove={parentContainer ? undefined : onNodePointerMove}
|
||||
onPointerUp={parentContainer ? undefined : onNodePointerUp}
|
||||
onPointerCancel={parentContainer ? undefined : onNodePointerUp}
|
||||
onPointerCancel={parentContainer ? undefined : onNodePointerCancel}
|
||||
title={isFrameVisible ? node.metadata.name || undefined : undefined}
|
||||
>
|
||||
{isFrameVisible && node.metadata.name ? (
|
||||
@@ -192,13 +209,16 @@ function RenderNode({
|
||||
selectedNodeId={selectedNodeId}
|
||||
resources={resources}
|
||||
onSelectNode={onSelectNode}
|
||||
consumeNodeClick={consumeNodeClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
onNodePointerDown={onNodePointerDown}
|
||||
onNodePointerMove={onNodePointerMove}
|
||||
onNodePointerUp={onNodePointerUp}
|
||||
onNodePointerCancel={onNodePointerCancel}
|
||||
onNodeResizePointerDown={onNodeResizePointerDown}
|
||||
onNodeResizePointerMove={onNodeResizePointerMove}
|
||||
onNodeResizePointerUp={onNodeResizePointerUp}
|
||||
onNodeResizePointerCancel={onNodeResizePointerCancel}
|
||||
onSelectExclusiveChild={onSelectExclusiveChild}
|
||||
viewportScale={viewportScale}
|
||||
/>
|
||||
@@ -228,7 +248,7 @@ function RenderNode({
|
||||
}
|
||||
onPointerMove={onNodeResizePointerMove}
|
||||
onPointerUp={onNodeResizePointerUp}
|
||||
onPointerCancel={onNodeResizePointerUp}
|
||||
onPointerCancel={onNodeResizePointerCancel}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
|
||||
+70
-6
@@ -28,6 +28,7 @@ type GestureBase = {
|
||||
startClientX: number;
|
||||
startClientY: number;
|
||||
startTransform: UiNode['layout']['transform'];
|
||||
hitNodeId: string;
|
||||
hasMoved: boolean;
|
||||
};
|
||||
|
||||
@@ -78,6 +79,32 @@ function releasePointer(target: HTMLDivElement, pointerId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveHitNodeId(
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
fallback: string,
|
||||
) {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return fallback;
|
||||
return (
|
||||
target.closest<HTMLElement>('[data-node-id]')?.dataset.nodeId ?? fallback
|
||||
);
|
||||
}
|
||||
|
||||
function isInsidePreview(
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
previewRef: RefObject<HTMLElement | null> | undefined,
|
||||
) {
|
||||
const preview = previewRef?.current;
|
||||
if (!preview) return true;
|
||||
const rect = preview.getBoundingClientRect();
|
||||
return (
|
||||
event.clientX >= rect.left &&
|
||||
event.clientX <= rect.right &&
|
||||
event.clientY >= rect.top &&
|
||||
event.clientY <= rect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
export function useNodeTransformInteraction({
|
||||
activeImageId,
|
||||
canvas,
|
||||
@@ -85,6 +112,7 @@ export function useNodeTransformInteraction({
|
||||
spaceHeld,
|
||||
tree,
|
||||
viewportRef,
|
||||
previewRef,
|
||||
}: {
|
||||
activeImageId: UiEditorCanvasProjection['activeImageId'];
|
||||
canvas: Pick<UiEditorCanvasProjection, 'selectNode' | 'updateNodeTransform'>;
|
||||
@@ -92,8 +120,10 @@ export function useNodeTransformInteraction({
|
||||
spaceHeld: boolean;
|
||||
tree: UITree | null;
|
||||
viewportRef: RefObject<ViewportScale>;
|
||||
previewRef?: RefObject<HTMLElement | null>;
|
||||
}) {
|
||||
const activeGestureRef = useRef<ActiveGesture | null>(null);
|
||||
const suppressNextNodeClickRef = useRef(false);
|
||||
|
||||
const cancelGesture = useCallback(() => {
|
||||
const gesture = activeGestureRef.current;
|
||||
@@ -103,6 +133,12 @@ export function useNodeTransformInteraction({
|
||||
|
||||
useEffect(() => cancelGesture, [cancelGesture]);
|
||||
|
||||
useEffect(() => {
|
||||
const cancelOnWindowBlur = () => cancelGesture();
|
||||
window.addEventListener('blur', cancelOnWindowBlur);
|
||||
return () => window.removeEventListener('blur', cancelOnWindowBlur);
|
||||
}, [cancelGesture]);
|
||||
|
||||
useEffect(() => {
|
||||
const gesture = activeGestureRef.current;
|
||||
if (
|
||||
@@ -140,7 +176,7 @@ export function useNodeTransformInteraction({
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
canvas.selectNode(node.id);
|
||||
suppressNextNodeClickRef.current = false;
|
||||
activeGestureRef.current = {
|
||||
kind: 'drag',
|
||||
treeId: activeImageId,
|
||||
@@ -150,10 +186,11 @@ export function useNodeTransformInteraction({
|
||||
startClientX: event.clientX,
|
||||
startClientY: event.clientY,
|
||||
startTransform: structuredClone(node.layout.transform),
|
||||
hitNodeId: resolveHitNodeId(event, node.id),
|
||||
hasMoved: false,
|
||||
};
|
||||
},
|
||||
[activeImageId, canvas, spaceHeld, tree?.root.id],
|
||||
[activeImageId, spaceHeld, tree?.root.id],
|
||||
);
|
||||
|
||||
const onNodePointerMove = useCallback(
|
||||
@@ -164,6 +201,8 @@ export function useNodeTransformInteraction({
|
||||
}
|
||||
event.stopPropagation();
|
||||
if (!gesture.hasMoved && !passedDragThreshold(gesture, event)) return;
|
||||
gesture.hasMoved = true;
|
||||
suppressNextNodeClickRef.current = true;
|
||||
const scale = viewportRef.current?.scale;
|
||||
if (!Number.isFinite(scale) || scale <= 0) {
|
||||
cancelGesture();
|
||||
@@ -175,7 +214,6 @@ export function useNodeTransformInteraction({
|
||||
cancelGesture();
|
||||
return;
|
||||
}
|
||||
gesture.hasMoved = true;
|
||||
const nextTransform = structuredClone(gesture.startTransform);
|
||||
nextTransform.offset_min[0] += logicalDeltaX;
|
||||
nextTransform.offset_min[1] += logicalDeltaY;
|
||||
@@ -191,6 +229,21 @@ export function useNodeTransformInteraction({
|
||||
);
|
||||
|
||||
const onNodePointerUp = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
const gesture = activeGestureRef.current;
|
||||
if (!gesture || !acceptsGestureEvent(event)) return;
|
||||
event.stopPropagation();
|
||||
if (gesture.hasMoved) suppressNextNodeClickRef.current = true;
|
||||
if (!gesture.hasMoved && isInsidePreview(event, previewRef)) {
|
||||
canvas.selectNode(gesture.hitNodeId);
|
||||
suppressNextNodeClickRef.current = true;
|
||||
}
|
||||
cancelGesture();
|
||||
},
|
||||
[acceptsGestureEvent, cancelGesture, canvas, previewRef],
|
||||
);
|
||||
|
||||
const onNodePointerCancel = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!acceptsGestureEvent(event)) return;
|
||||
event.stopPropagation();
|
||||
@@ -236,7 +289,7 @@ export function useNodeTransformInteraction({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
canvas.selectNode(node.id);
|
||||
suppressNextNodeClickRef.current = false;
|
||||
activeGestureRef.current = {
|
||||
kind: 'resize',
|
||||
treeId: activeImageId,
|
||||
@@ -247,13 +300,14 @@ export function useNodeTransformInteraction({
|
||||
startClientX: event.clientX,
|
||||
startClientY: event.clientY,
|
||||
startTransform: structuredClone(node.layout.transform),
|
||||
hitNodeId: node.id,
|
||||
startRect: context.rect,
|
||||
parentRect: context.parentRect,
|
||||
ratioAxis: null,
|
||||
hasMoved: false,
|
||||
};
|
||||
},
|
||||
[activeImageId, canvas, logicalSize, spaceHeld, tree],
|
||||
[activeImageId, logicalSize, spaceHeld, tree],
|
||||
);
|
||||
|
||||
const onNodeResizePointerMove = useCallback(
|
||||
@@ -268,6 +322,8 @@ export function useNodeTransformInteraction({
|
||||
}
|
||||
event.stopPropagation();
|
||||
if (!gesture.hasMoved && !passedDragThreshold(gesture, event)) return;
|
||||
gesture.hasMoved = true;
|
||||
suppressNextNodeClickRef.current = true;
|
||||
const scale = viewportRef.current?.scale;
|
||||
if (!Number.isFinite(scale) || scale <= 0) {
|
||||
cancelGesture();
|
||||
@@ -309,18 +365,26 @@ export function useNodeTransformInteraction({
|
||||
cancelGesture();
|
||||
return;
|
||||
}
|
||||
gesture.hasMoved = true;
|
||||
canvas.updateNodeTransform(gesture.treeId, gesture.nodeId, nextTransform);
|
||||
},
|
||||
[acceptsGestureEvent, cancelGesture, canvas, viewportRef],
|
||||
);
|
||||
|
||||
const consumeNodeClick = useCallback(() => {
|
||||
if (!suppressNextNodeClickRef.current) return false;
|
||||
suppressNextNodeClickRef.current = false;
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
onNodePointerDown,
|
||||
onNodePointerMove,
|
||||
onNodePointerUp,
|
||||
onNodePointerCancel,
|
||||
onNodeResizePointerDown,
|
||||
onNodeResizePointerMove,
|
||||
onNodeResizePointerUp: onNodePointerUp,
|
||||
onNodeResizePointerCancel: onNodePointerCancel,
|
||||
consumeNodeClick,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user