0.1.12全屏编辑修复
This commit is contained in:
@@ -332,6 +332,11 @@ const MindmapBlockView = ({
|
||||
const persistDataRef = useRef<((data: unknown) => void) | null>(null);
|
||||
const recentNodeDblclickRef = useRef(false);
|
||||
const deletingRef = useRef(false);
|
||||
const textEditOpenRef = useRef(false);
|
||||
const suppressTextEditRefocusUntilRef = useRef(0);
|
||||
const lastTextEditRefocusAtRef = useRef(0);
|
||||
const pendingInitActivateRootRef = useRef(false);
|
||||
const pendingRenderEndActivateRootRef = useRef(false);
|
||||
|
||||
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
|
||||
const instance = mm ?? mindmap;
|
||||
@@ -464,7 +469,6 @@ const MindmapBlockView = ({
|
||||
useEffect(() => {
|
||||
const wrapper = wrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
if (effectiveFullscreen) return;
|
||||
|
||||
const stopBubble = (e: Event) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
@@ -488,19 +492,22 @@ const MindmapBlockView = ({
|
||||
hotkeyScopeRef.current = true;
|
||||
lastInteractionAtRef.current = Date.now();
|
||||
|
||||
// 同步/异步都尝试一次,把焦点拉到 wrapper,保证快捷键稳定
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
// 内嵌视图:同步/异步都尝试一次,把焦点拉到 wrapper,保证快捷键稳定。
|
||||
// 全屏(Portal)视图:不能强制 focus wrapper,否则会打断节点双击编辑输入框的焦点。
|
||||
if (!effectiveFullscreen) {
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 0);
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
wrapper.addEventListener("mousedown", stopBubble);
|
||||
@@ -526,6 +533,137 @@ const MindmapBlockView = ({
|
||||
};
|
||||
}, [mindmap]);
|
||||
|
||||
// 记录“是否处于节点文本编辑态”,并在全屏时做一次“焦点保护”:避免外部组件/浏览器机制
|
||||
// 在延迟(例如 1s)后抢走焦点,导致编辑框/光标消失(用户反馈)。
|
||||
useEffect(() => {
|
||||
if (!mindmap) return;
|
||||
const onShow = () => {
|
||||
textEditOpenRef.current = true;
|
||||
// 刚进入编辑态时允许立即 refocus(不抑制)
|
||||
suppressTextEditRefocusUntilRef.current = 0;
|
||||
// 兜底:部分环境下焦点会在进入编辑态后稍后被抢走,这里做一次 1s 的检查
|
||||
window.setTimeout(() => {
|
||||
if (!effectiveFullscreen) return;
|
||||
if (!textEditOpenRef.current) return;
|
||||
const wrap = document.querySelector(
|
||||
".smm-node-edit-wrap, .smm-richtext-node-edit-wrap",
|
||||
) as HTMLElement | null;
|
||||
if (!wrap) return;
|
||||
const style = window.getComputedStyle(wrap);
|
||||
if (style.display === "none" || style.visibility === "hidden") return;
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (active && wrap.contains(active)) return;
|
||||
const editorEl =
|
||||
(wrap.querySelector(".ql-editor") as HTMLElement | null) ?? wrap;
|
||||
try {
|
||||
editorEl.focus({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 1100);
|
||||
};
|
||||
const onHide = () => {
|
||||
textEditOpenRef.current = false;
|
||||
// 用户反馈:全屏下双击节点进入编辑后,约 1s 后光标/输入框会消失。
|
||||
// 根因通常是初始化兜底激活(或 render_end 回调)在编辑中途清空 active 列表/重新激活根节点,导致编辑被打断。
|
||||
// 这里把这类“激活根节点”的动作延后到结束编辑后再执行。
|
||||
if (pendingInitActivateRootRef.current || pendingRenderEndActivateRootRef.current) {
|
||||
pendingInitActivateRootRef.current = false;
|
||||
pendingRenderEndActivateRootRef.current = false;
|
||||
window.setTimeout(() => {
|
||||
const mm = mindmapRef.current;
|
||||
const renderer = mm?.renderer;
|
||||
const root =
|
||||
renderer?.root ?? renderer?.renderTree?._node ?? null;
|
||||
if (!mm || !renderer || !root) return;
|
||||
try {
|
||||
renderer?.clearActiveNodeList?.();
|
||||
renderer?.addNodeToActiveList?.(root, true);
|
||||
renderer?.emitNodeActiveEvent?.(root);
|
||||
setActiveNodes([root as MindMapNode]);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
mindmap.on?.("before_show_text_edit", onShow);
|
||||
mindmap.on?.("hide_text_edit", onHide);
|
||||
return () => {
|
||||
mindmap.off?.("before_show_text_edit", onShow);
|
||||
mindmap.off?.("hide_text_edit", onHide);
|
||||
};
|
||||
}, [mindmap, effectiveFullscreen]);
|
||||
|
||||
// 全屏时:如果编辑框仍在显示但焦点被外部抢走,则自动把焦点拉回编辑框,保证“有光标”的体验。
|
||||
// 同时尊重用户的“点击外部结束编辑”:当检测到 pointerdown 发生在编辑框之外时,短暂抑制 refocus,
|
||||
// 让 simple-mind-map 正常触发 hide_text_edit。
|
||||
useEffect(() => {
|
||||
if (!effectiveFullscreen) return;
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const getEditWrap = () =>
|
||||
document.querySelector(
|
||||
".smm-node-edit-wrap, .smm-richtext-node-edit-wrap",
|
||||
) as HTMLElement | null;
|
||||
|
||||
const onPointerDownCapture = (e: Event) => {
|
||||
if (!textEditOpenRef.current) return;
|
||||
const wrap = getEditWrap();
|
||||
if (!wrap) return;
|
||||
const target = e.target as Node | null;
|
||||
if (target && wrap.contains(target)) return;
|
||||
// 用户点击了编辑框外部:允许结束编辑(暂时不强制拉回焦点)
|
||||
suppressTextEditRefocusUntilRef.current = Date.now() + 800;
|
||||
};
|
||||
|
||||
const onFocusInCapture = (e: Event) => {
|
||||
if (!textEditOpenRef.current) return;
|
||||
const now = Date.now();
|
||||
if (now < suppressTextEditRefocusUntilRef.current) return;
|
||||
const wrap = getEditWrap();
|
||||
if (!wrap) return;
|
||||
const style = window.getComputedStyle(wrap);
|
||||
if (style.display === "none" || style.visibility === "hidden") return;
|
||||
const target = e.target as Node | null;
|
||||
if (target && wrap.contains(target)) return;
|
||||
|
||||
// 节流:避免 focusin -> refocus -> focusin 的循环
|
||||
if (now - lastTextEditRefocusAtRef.current < 120) return;
|
||||
lastTextEditRefocusAtRef.current = now;
|
||||
|
||||
const editorEl =
|
||||
(wrap.querySelector(".ql-editor") as HTMLElement | null) ?? wrap;
|
||||
window.setTimeout(() => {
|
||||
if (!textEditOpenRef.current) return;
|
||||
if (Date.now() < suppressTextEditRefocusUntilRef.current) return;
|
||||
const curWrap = getEditWrap();
|
||||
if (!curWrap) return;
|
||||
const curStyle = window.getComputedStyle(curWrap);
|
||||
if (curStyle.display === "none" || curStyle.visibility === "hidden")
|
||||
return;
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (active && curWrap.contains(active)) return;
|
||||
const focusTarget =
|
||||
(curWrap.querySelector(".ql-editor") as HTMLElement | null) ?? curWrap;
|
||||
try {
|
||||
focusTarget.focus({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
document.addEventListener("pointerdown", onPointerDownCapture, true);
|
||||
document.addEventListener("mousedown", onPointerDownCapture, true);
|
||||
document.addEventListener("focusin", onFocusInCapture, true);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", onPointerDownCapture, true);
|
||||
document.removeEventListener("mousedown", onPointerDownCapture, true);
|
||||
document.removeEventListener("focusin", onFocusInCapture, true);
|
||||
};
|
||||
}, [effectiveFullscreen]);
|
||||
|
||||
const exitLocalFullscreen = useCallback(() => {
|
||||
setActiveSidebar(null);
|
||||
if (fullscreen) {
|
||||
@@ -954,8 +1092,15 @@ const MindmapBlockView = ({
|
||||
// 否则切换视图时会用旧数据,表现为“看起来没保存”
|
||||
const safe = canonicalizeMindmapData(data);
|
||||
initialDataRef.current = safe;
|
||||
window.localStorage.setItem(autosaveKey, JSON.stringify(safe));
|
||||
editor.updateBlock(block, { props: { ...block.props, data: safe } });
|
||||
window.localStorage.setItem(autosaveKey, JSON.stringify(safe));
|
||||
// 注意:全屏(Portal 覆盖层)下如果调用 updateBlock,BlockNote/ProseMirror
|
||||
// 可能会在防抖保存时(例如 ~800ms)抢回焦点,导致节点双击编辑的输入框
|
||||
// 被迫退出(表现为“有光标但过一会儿就消失”)。
|
||||
// 因此全屏态只做 localStorage + 后端同步,等退出全屏(实例重建/卸载)
|
||||
// 时再统一把最新数据写回 block。
|
||||
if (!effectiveFullscreen) {
|
||||
editor.updateBlock(block, { props: { ...block.props, data: safe } });
|
||||
}
|
||||
if (docId) {
|
||||
// 同步到本地文件 + Supabase(弱依赖)
|
||||
fetch(`/api/mindmap/${docId}/${mindmapId}`, {
|
||||
@@ -978,7 +1123,7 @@ const MindmapBlockView = ({
|
||||
.catch((err) => console.warn("思维导图同步失败", err));
|
||||
}
|
||||
},
|
||||
[autosaveKey, block, docId, editor, mindmapId],
|
||||
[autosaveKey, block, docId, editor, mindmapId, effectiveFullscreen],
|
||||
);
|
||||
useEffect(() => {
|
||||
persistDataRef.current = persistData;
|
||||
@@ -1288,8 +1433,21 @@ const MindmapBlockView = ({
|
||||
useLeftKeySelectionRightKeyDrag: boolean;
|
||||
createNewNodeBehavior: string;
|
||||
iconList: unknown[];
|
||||
customInnerElsAppendTo?: HTMLElement;
|
||||
nodeTextEditZIndex?: number;
|
||||
}) => MindMapInstance;
|
||||
const MindMapCtor = MindMap as unknown as MindMapConstructor;
|
||||
const MindMapCtor = MindMap as unknown as MindMapConstructor;
|
||||
|
||||
// Portal 全屏时,simple-mind-map 的节点文本编辑框会 append 到 body,并受 z-index 影响:
|
||||
// 若 z-index 低于我们的全屏覆盖层,会出现“进入编辑态但看不到独立输入框/光标”的问题。
|
||||
// 这里显式提高节点编辑框 z-index,并把内部浮层挂到 wrapper 上,确保全屏下可见且不会被遮挡。
|
||||
const fullscreenTextEditOptions = effectiveFullscreen
|
||||
? {
|
||||
// 让编辑框仍 append 到 body(库默认行为),只提高 z-index,避免被全屏覆盖层挡住。
|
||||
// 注意:如果把编辑框 append 到 wrapper(overflow-hidden),可能会影响 box-shadow/可见性。
|
||||
nodeTextEditZIndex: 100000,
|
||||
}
|
||||
: null;
|
||||
|
||||
const instance = new MindMapCtor({
|
||||
el: hostEl,
|
||||
@@ -1308,6 +1466,7 @@ const MindmapBlockView = ({
|
||||
...nodeIconList,
|
||||
...(iconConfig as unknown[]),
|
||||
]),
|
||||
...(fullscreenTextEditOptions ?? {}),
|
||||
});
|
||||
createdInstance = instance;
|
||||
mindmapRef.current = instance;
|
||||
@@ -1433,6 +1592,12 @@ const MindmapBlockView = ({
|
||||
const renderer = instance.renderer;
|
||||
const root = getRootNode(instance);
|
||||
if (root) {
|
||||
// 用户正在编辑节点文本时,不要清空 active 列表/切回根节点,避免打断编辑(光标/输入框消失)。
|
||||
if (textEditOpenRef.current) {
|
||||
pendingRenderEndActivateRootRef.current = true;
|
||||
mindmapReadyRef.current = true;
|
||||
return;
|
||||
}
|
||||
if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
|
||||
if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(root, true);
|
||||
if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(root);
|
||||
@@ -1530,6 +1695,11 @@ const MindmapBlockView = ({
|
||||
}
|
||||
// 若首次渲染时 root 仍未就绪,设置兜底延迟激活
|
||||
window.setTimeout(() => {
|
||||
// 全屏下快速进入节点编辑态时,兜底激活会打断编辑;延后到 hide_text_edit 再处理。
|
||||
if (textEditOpenRef.current) {
|
||||
pendingInitActivateRootRef.current = true;
|
||||
return;
|
||||
}
|
||||
const root = getRootNode(instance);
|
||||
if (!root) return;
|
||||
if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList();
|
||||
|
||||
Reference in New Issue
Block a user