Bind rendered editing callbacks to captured element lifetimes

This commit is contained in:
2026-11-09 19:26:00 +08:00
parent 28c792d640
commit 9efe6e7c9f
14 changed files with 103 additions and 45 deletions
+4 -3
View File
@@ -5,18 +5,19 @@ import { FoldVertical, UnfoldVertical, ExternalLink } from "lucide-react";
import { Handle, NodeToolbar, Position, useViewport, type NodeProps } from "@xyflow/react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type { CanvasNode } from "@/lib/canvas";
import { NodeEditing, TransformableNode, ResizeToggle } from "./transformable-node";
import { TransformableNode, ResizeToggle } from "./transformable-node";
import { FrameFeedback } from "./frame-feedback";
import { FrameLabelMenu } from "./frame-label-menu";
import { ContentHandles } from "./content-handles";
import {useCanvasOverlay} from "./canvas-overlays";
import {useScopedNodeEditing} from "./node-editing";
import styles from "./canvas.module.css";
export function FrameNode(props: NodeProps<CanvasNode> & { directAnchors?: boolean }) {
const { directAnchors = false, ...nodeProps } = props;
const { id, data, selected } = nodeProps;
const overlay=useCanvasOverlay(id,"frame-settings");
const editing = useContext(NodeEditing);
const editing = useScopedNodeEditing(id,data.__headlessTarget);
const feedback = useContext(FrameFeedback);
const incoming = feedback.drops.filter((drop) => drop.to === id && drop.from !== id).length;
const outgoing = feedback.drops.filter((drop) => drop.from === id && drop.to !== id).length;
@@ -39,7 +40,7 @@ export function FrameNode(props: NodeProps<CanvasNode> & { directAnchors?: boole
<input className={styles.frameNameInput} aria-label="分区名称" value={title} maxLength={300} onChange={(event) => setTitle(event.target.value)} onBlur={() => { if (cancelBlur.current) cancelBlur.current = false; else saveTitle(); }}
onKeyDown={(event) => { if (event.key === "Enter" && !event.nativeEvent.isComposing) { event.currentTarget.blur(); } if (event.key === "Escape") { cancelBlur.current = true; setTitle(data.title); event.currentTarget.blur(); } }} />
<FrameLabelMenu value={mode} change={(labelMode) => editing.frame(id, { labelMode })} />
<ResizeToggle id={id} />
<ResizeToggle id={id} renderedTarget={data.__headlessTarget} />
{data.subgraph && <><button type="button" aria-label={data.presentation?.collapsed ? "展开子画布" : "收起子画布"} title={data.presentation?.collapsed ? "展开子画布" : "收起子画布"} onClick={() => {
const collapsed = !data.presentation?.collapsed;
const currentSize = { width: (props.width || 640) / (data.viewScale || 1), height: (props.height || 400) / (data.viewScale || 1) };