docs(canvas): explain node editing scopes and display interactions
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* 分区节点:展示名称、边框、成员拖入/移出反馈,以及子画布折叠与概览设置。
|
||||
* 局部状态只存名称输入草稿;保存与分区属性修改都交给作用域编辑接口,成员归属不由本组件直接变更。
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
@@ -13,25 +17,37 @@ import {useCanvasOverlay} from "./canvas-overlays";
|
||||
import {useScopedNodeEditing} from "./node-editing";
|
||||
import styles from "./canvas.module.css";
|
||||
|
||||
/** directAnchors用于新版分区四向连接点;子画布ports则按输入/输出方向单独布局。
|
||||
* 分区使用自己的设置浮层,因此隐藏TransformableNode的通用样式栏,仍保留公共变换行为。 */
|
||||
export function FrameNode(props: NodeProps<CanvasNode> & { directAnchors?: boolean }) {
|
||||
const { directAnchors = false, ...nodeProps } = props;
|
||||
const { id, data, selected } = nodeProps;
|
||||
// 设置浮层注册到当前视图的overlay索引,让Portal中的控件也能正确参与焦点与输入路由。
|
||||
const overlay=useCanvasOverlay(id,"frame-settings");
|
||||
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;
|
||||
const { zoom } = useViewport();
|
||||
const reduce = useReducedMotion();
|
||||
// 名称是受控输入草稿;外部data.title变化时同步覆盖,实际提交发生在blur。
|
||||
const [title, setTitle] = useState(data.title);
|
||||
useEffect(() => setTitle(data.title), [data.title]);
|
||||
// Escape恢复模型标题并标记跳过随后的blur保存;Enter排除输入法组合态后通过blur统一提交。
|
||||
const cancelBlur = useRef(false);
|
||||
const mode = data.labelMode || "fixed";
|
||||
/** 去除首尾空白,空名称回退“未命名分区”;只有与模型不同才派发修改,再归一化输入显示。 */
|
||||
function saveTitle() {
|
||||
const name = title.trim() || "未命名分区";
|
||||
if (name !== data.title) editing.frame(id, { title: name });
|
||||
setTitle(name);
|
||||
}
|
||||
// 选中时显示设置栏,动画尊重减少动态效果偏好;栏内事件停止冒泡,避免触发画布快捷键或拖动。
|
||||
// 折叠切换前按viewScale还原当前尺寸,存入原状态对应的expandedSize/collapsedSize供后续恢复。
|
||||
// 概览比例输入限定5%—400%,只更新presentation.contentScale;链接单独打开子画布。
|
||||
// fixed名称与拖放提示用1/(zoom*viewScale)抵消缩放,scaled跟随画布,hidden不绘制名称。
|
||||
// 四侧边框提供拖动命中区域;ports按同方向均匀分布,输出在右侧可起连线,输入在左侧可收连线。
|
||||
return <>
|
||||
<AnimatePresence>{selected && <NodeToolbar style={{ zIndex: 100, pointerEvents: "auto" }} isVisible={true} position={Position.Top} offset={45}>
|
||||
<motion.div ref={overlay} className={`${styles.styleToolbar} nodrag nopan nowheel`} role="toolbar" aria-label="分区设置"
|
||||
@@ -59,6 +75,7 @@ export function FrameNode(props: NodeProps<CanvasNode> & { directAnchors?: boole
|
||||
{(["top", "right", "bottom", "left"] as const).map((side) => <div key={side} className={`${styles.frameBorderHit} ${styles["frameBorder_" + side]}`} title="拖动分区,已有成员一起移动" />)}
|
||||
{directAnchors && <ContentHandles selected={!!selected} />}
|
||||
{data.ports?.map((port) => {
|
||||
// 只在同方向端口之间均分高度,index+1与length+1让首尾都留出边距。
|
||||
const peers = data.ports!.filter((item) => item.direction === port.direction);
|
||||
return <Handle key={port.id} id={`port:${port.id}`} type="source" position={port.direction === "input" ? Position.Left : Position.Right} className={styles.framePort} style={{ top: `${(peers.indexOf(port) + 1) / (peers.length + 1) * 100}%` }} isConnectableStart={selected && port.direction === "output"} isConnectableEnd={port.direction === "input"} title={port.direction === "input" ? "子画布输入" : "子画布输出"} />;
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user