0.1.11 ai修复与全屏
This commit is contained in:
@@ -296,6 +296,7 @@ const MindmapBlockView = ({
|
||||
block,
|
||||
editor,
|
||||
fullscreen = false,
|
||||
onExitFullscreen,
|
||||
}: {
|
||||
block: SpecificBlock<
|
||||
CustomBlockSchema,
|
||||
@@ -305,6 +306,7 @@ const MindmapBlockView = ({
|
||||
>;
|
||||
editor: BlockNoteEditor<CustomBlockSchema>;
|
||||
fullscreen?: boolean;
|
||||
onExitFullscreen?: () => void;
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -327,9 +329,8 @@ const MindmapBlockView = ({
|
||||
const hotkeyScopeRef = useRef(false);
|
||||
const lastInteractionAtRef = useRef(0);
|
||||
const skipNextPasteRef = useRef(false);
|
||||
const persistDataRef = useRef<((data: unknown) => void) | null>(null);
|
||||
const persistDataRef = useRef<((data: unknown) => void) | null>(null);
|
||||
const recentNodeDblclickRef = useRef(false);
|
||||
const [fullscreenApiActive, setFullscreenApiActive] = useState(false);
|
||||
const deletingRef = useRef(false);
|
||||
|
||||
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
|
||||
@@ -441,6 +442,7 @@ const MindmapBlockView = ({
|
||||
|
||||
// 这里用 setTimeout(0) 而不是 microtask:BlockNote/ProseMirror 可能会在同一轮事件里重新抢回焦点,
|
||||
// 导致“选中节点后 Ctrl+V 把思维导图替换成纯文本”。延后一拍把焦点拉回 wrapper,保证快捷键/粘贴作用域稳定。
|
||||
if (effectiveFullscreen) return;
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
@@ -455,13 +457,14 @@ const MindmapBlockView = ({
|
||||
document.removeEventListener("pointerdown", onPointerDownCapture, true);
|
||||
document.removeEventListener("mousedown", onPointerDownCapture, true);
|
||||
};
|
||||
}, []);
|
||||
}, [effectiveFullscreen]);
|
||||
|
||||
// 关键:阻止鼠标事件冒泡到 BlockNote/ProseMirror(它们会在 contenteditable 上处理 mousedown,从而产生 NodeSelection)。
|
||||
// 不能用 React 的 onMouseDown(事件委托在 document,太晚了),必须用原生监听挂在 wrapper 上,确保在 bubble 链路中先于 editor DOM。
|
||||
useEffect(() => {
|
||||
const wrapper = wrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
if (effectiveFullscreen) return;
|
||||
|
||||
const stopBubble = (e: Event) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
@@ -525,7 +528,10 @@ const MindmapBlockView = ({
|
||||
|
||||
const exitLocalFullscreen = useCallback(() => {
|
||||
setActiveSidebar(null);
|
||||
if (fullscreen) return;
|
||||
if (fullscreen) {
|
||||
onExitFullscreen?.();
|
||||
return;
|
||||
}
|
||||
if (typeof document === "undefined") {
|
||||
setLocalFullscreen(false);
|
||||
return;
|
||||
@@ -540,29 +546,12 @@ const MindmapBlockView = ({
|
||||
return;
|
||||
}
|
||||
setLocalFullscreen(false);
|
||||
}, [fullscreen]);
|
||||
}, [fullscreen, onExitFullscreen]);
|
||||
|
||||
const enterLocalFullscreen = useCallback(() => {
|
||||
if (fullscreen) return;
|
||||
setLocalFullscreen(true);
|
||||
setActiveSidebar(null);
|
||||
if (typeof document === "undefined") return;
|
||||
if (!document.fullscreenEnabled) return;
|
||||
if (document.fullscreenElement) return;
|
||||
// 必须在“用户手势回调”中同步调用,避免 Fullscreen API 被浏览器拒绝
|
||||
try {
|
||||
const target = document.documentElement as unknown as {
|
||||
requestFullscreen?: () => Promise<void>;
|
||||
};
|
||||
const p = target.requestFullscreen?.();
|
||||
if (p && typeof p.catch === "function") {
|
||||
p.catch(() => {
|
||||
// ignore:失败则保持 Portal 伪全屏
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore:失败则保持 Portal 伪全屏
|
||||
}
|
||||
}, [fullscreen]);
|
||||
|
||||
// 沉浸式全屏:锁定页面滚动、支持 ESC 退出(仅对内嵌全屏生效)
|
||||
@@ -596,24 +585,6 @@ const MindmapBlockView = ({
|
||||
};
|
||||
}, [localFullscreen, fullscreen, exitLocalFullscreen]);
|
||||
|
||||
// “真全屏”:使用 Fullscreen API 隐藏浏览器/桌面窗口的外层 UI(Electron/Web 都可用)
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const onFsChange = () => {
|
||||
const active = Boolean(document.fullscreenElement);
|
||||
setFullscreenApiActive(active);
|
||||
// 注意:浏览器在打开原生对话框(例如 file picker / alert / confirm)时可能会自动退出
|
||||
// Fullscreen API。此时不应退出“沉浸式全屏”UI,否则会导致用户在全屏编辑中执行导入/新建
|
||||
// 等操作时被强制退出全屏。
|
||||
};
|
||||
|
||||
document.addEventListener("fullscreenchange", onFsChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", onFsChange);
|
||||
};
|
||||
}, [localFullscreen]);
|
||||
|
||||
// 让 Enter/Tab/复制/粘贴在“思维导图块”内生效(避免 BlockNote 抢走快捷键)
|
||||
useLayoutEffect(() => {
|
||||
const onKeyDownCapture = (e: KeyboardEvent) => {
|
||||
@@ -706,6 +677,21 @@ const MindmapBlockView = ({
|
||||
//(例如 Ctrl+C/Ctrl+V 作为文本复制粘贴)。
|
||||
if (isNodeTextEditing) return;
|
||||
|
||||
if ((key === "Delete" || key === "Backspace") && !e.shiftKey && !e.altKey && !isMod) {
|
||||
stop();
|
||||
const inst = ensureActiveBefore(mm);
|
||||
if (!inst) return;
|
||||
inst.execCommand?.("REMOVE_NODE");
|
||||
hasLocalEditsRef.current = true;
|
||||
try {
|
||||
const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
|
||||
if (snapshot) persistDataRef.current?.(snapshot);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMod && lower === "c") {
|
||||
stop();
|
||||
const inst = ensureActiveBefore(mm);
|
||||
@@ -1043,10 +1029,10 @@ const MindmapBlockView = ({
|
||||
);
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await fetch(`/api/mindmap/${docId}/${mindmapId}`, {
|
||||
const resp = await fetch(`/api/mindmap/${docId}/${mindmapId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ data }),
|
||||
body: JSON.stringify({ data, createOnly: true }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.warn(
|
||||
@@ -1426,9 +1412,14 @@ const MindmapBlockView = ({
|
||||
window.__mindmapInstance = instance;
|
||||
const w = window as unknown as {
|
||||
__mindmapInstancesById?: Record<string, MindMapInstance>;
|
||||
__mindmapPersistById?: Record<string, (data: unknown) => void>;
|
||||
};
|
||||
if (!w.__mindmapInstancesById) w.__mindmapInstancesById = {};
|
||||
w.__mindmapInstancesById[mindmapId] = instance;
|
||||
if (!w.__mindmapPersistById) w.__mindmapPersistById = {};
|
||||
w.__mindmapPersistById[mindmapId] = (data: unknown) => {
|
||||
persistDataRef.current?.(data);
|
||||
};
|
||||
}
|
||||
|
||||
setMindmap(instance);
|
||||
@@ -1471,13 +1462,17 @@ const MindmapBlockView = ({
|
||||
// 这里用内部事件标记“当前在思维导图作用域内”,确保 Ctrl+C/Ctrl+V 不会被 BlockNote 抢走。
|
||||
hotkeyScopeRef.current = true;
|
||||
lastInteractionAtRef.current = Date.now();
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 0);
|
||||
// 仅内嵌视图需要把焦点从编辑器拉回本块;全屏(Portal)下强制 focus
|
||||
// 可能会抢走节点文本编辑的输入焦点,导致双击编辑不出光标。
|
||||
if (!effectiveFullscreen) {
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
instance.on?.("node_click", (node: unknown) => {
|
||||
// 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致)
|
||||
@@ -1499,13 +1494,17 @@ const MindmapBlockView = ({
|
||||
);
|
||||
hotkeyScopeRef.current = true;
|
||||
lastInteractionAtRef.current = Date.now();
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 0);
|
||||
// 仅内嵌视图需要把焦点从编辑器拉回本块;全屏(Portal)下强制 focus
|
||||
// 可能会抢走节点文本编辑的输入焦点,导致双击编辑不出光标。
|
||||
if (!effectiveFullscreen) {
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
instance.on?.("painter_start", () => setPainterMode(true));
|
||||
instance.on?.("painter_end", () => setPainterMode(false));
|
||||
@@ -1601,10 +1600,16 @@ const MindmapBlockView = ({
|
||||
window.__mindmapInstance = null;
|
||||
}
|
||||
try {
|
||||
const w = window as unknown as { __mindmapInstancesById?: Record<string, MindMapInstance> };
|
||||
const w = window as unknown as {
|
||||
__mindmapInstancesById?: Record<string, MindMapInstance>;
|
||||
__mindmapPersistById?: Record<string, (data: unknown) => void>;
|
||||
};
|
||||
if (w.__mindmapInstancesById && w.__mindmapInstancesById[mindmapId] === createdInstance) {
|
||||
delete w.__mindmapInstancesById[mindmapId];
|
||||
}
|
||||
if (w.__mindmapPersistById && w.__mindmapPersistById[mindmapId]) {
|
||||
delete w.__mindmapPersistById[mindmapId];
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -2392,7 +2397,7 @@ const MindmapBlockView = ({
|
||||
>
|
||||
<div className="fixed left-0 right-0 top-0 z-30 flex h-12 items-center justify-between border-b border-gray-200 bg-white/90 px-4 backdrop-blur">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
思维导图编辑(全屏{fullscreenApiActive ? "·真" : ""})
|
||||
思维导图编辑(全屏)
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -2421,6 +2426,8 @@ const MindmapBlockView = ({
|
||||
onSelect={setActiveSidebar}
|
||||
/>
|
||||
<MindmapSidebar
|
||||
documentId={docId}
|
||||
mindmapId={mindmapId}
|
||||
mindmap={mindmap}
|
||||
activeNodes={activeNodes}
|
||||
activeTab={activeSidebar}
|
||||
@@ -2510,6 +2517,8 @@ const MindmapBlockView = ({
|
||||
onSelect={setActiveSidebar}
|
||||
/>
|
||||
<MindmapSidebar
|
||||
documentId={docId}
|
||||
mindmapId={mindmapId}
|
||||
mindmap={mindmap}
|
||||
activeNodes={activeNodes}
|
||||
activeTab={activeSidebar}
|
||||
|
||||
Reference in New Issue
Block a user