/** * 卡片与纯文本共用的正文编辑交互。展示态用覆盖层接收选择/双击,编辑态让textarea接收原生文本操作。 * 正文直接来自data.text,每次输入交给actions.edit;本组件只保存是否正在编辑,不另存正文副本。 */ "use client"; import { useEffect, useRef, useState } from "react"; import type { CanvasNodeData } from "@/lib/canvas-elements"; import {useScopedNodeEditing} from "./node-editing"; import { textAppearance } from "./text-appearance"; import styles from "./canvas.module.css"; /** Single clicks select; native double-click timing controls entry into editing. */ /** plain选择纯文本样式、标签和占位文案;非plain按kind区分脚本/笔记占位。 * 对外动作经过渲染目标身份检查,具体文本事务与history分组由Provider实现。 */ export function CanvasTextEditor({ id, data, selected, plain = false }: { id: string; data: CanvasNodeData; selected: boolean; plain?: boolean }) { const actions = useScopedNodeEditing(id,data.__headlessTarget); const [editing, setEditing] = useState(false); const textarea = useRef(null); // 按下位置和移动标记只用于区分拖动与双击;移动超过4px后不因双击事件进入编辑。 const press = useRef({ x: 0, y: 0, moved: false }); // 失去选择退出本地编辑态;此effect不直接调用endEditing,正常输入框blur另走结束回调。 useEffect(() => { if (!selected) setEditing(false); }, [selected]); // 进入编辑后聚焦到文本末尾,并禁止浏览器自动滚动,以免画布视口跳动。 useEffect(() => { if (!editing) return; const element = textarea.current; element?.focus({ preventScroll: true }); element?.setSelectionRange(element.value.length, element.value.length); }, [editing]); /** 通知控制器开始文本编辑会话,再开启本地输入态;真正可写还要求节点保持选中。 */ function begin() { actions.begin(id); setEditing(true); } // 同时满足editing与selected才允许输入;展示时textarea只读且不进入Tab顺序。 const active = editing && selected; const label = plain ? "纯文本" : data.title; // 输入最多100000字符;onChange立即发动作,blur退出并通知endEditing。Escape只结束编辑,不回滚已输入文本。 // 非编辑态覆盖层不接管单击逻辑,让外层画布选择/拖动;双击使用浏览器原生时序并按移动标记过滤。 // 键盘Enter/F2仅在已选中时进入;双击未选中节点会先发select,再开始编辑。 return