Add native task metadata, working-day calendar settings, dependency scheduling and rich note editing. Support complete-sheet PNG export with resolution and transparency controls, preserve XMind extensions and update browser regressions.
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
"use client";
|
|
import {
|
|
type PlaitBoard,
|
|
type PlaitElement,
|
|
toHostPointFromViewBoxPoint,
|
|
toScreenPointFromHostPoint,
|
|
} from "@plait/core";
|
|
import { getRectangleByNode, MindElement } from "@plait/mind";
|
|
import { MoreHorizontal, X } from "lucide-react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
getNoteDocument,
|
|
type NoteDocument,
|
|
} from "@/lib/flow-board/topic-notes";
|
|
import { getTopicTitle } from "@/lib/flow-board/topic-details";
|
|
import { XmindNotesEditor } from "./xmind-notes-editor";
|
|
export function XmindNotesPopover({
|
|
board,
|
|
topic,
|
|
viewport,
|
|
onChange,
|
|
onDetails,
|
|
onClose,
|
|
}: {
|
|
board: PlaitBoard | null;
|
|
topic: PlaitElement | null;
|
|
viewport: unknown;
|
|
onChange: (doc: NoteDocument) => void;
|
|
onDetails: () => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const ref = useRef<HTMLElement>(null);
|
|
const [position, setPosition] = useState({ left: 400, top: 300 });
|
|
useEffect(() => {
|
|
if (!board || !topic) return;
|
|
const update = () => {
|
|
const node = MindElement.getNode(topic as MindElement);
|
|
if (!node) return;
|
|
const rect = getRectangleByNode(node),
|
|
point = toScreenPointFromHostPoint(
|
|
board,
|
|
toHostPointFromViewBoxPoint(board, [
|
|
rect.x + rect.width / 2,
|
|
rect.y + rect.height,
|
|
])
|
|
);
|
|
const host = ref.current?.parentElement?.getBoundingClientRect();
|
|
const width = Math.min(360, window.innerWidth - 32);
|
|
const right = window.innerWidth > 800 ? 240 : 0;
|
|
const left = Math.max(
|
|
12,
|
|
Math.min(window.innerWidth - right - width - 16, point[0] - width / 2)
|
|
);
|
|
const top = Math.max(
|
|
78,
|
|
Math.min(window.innerHeight - 340, point[1] + 12)
|
|
);
|
|
setPosition({ left: left - (host?.x || 0), top: top - (host?.y || 0) });
|
|
};
|
|
const frame = requestAnimationFrame(update);
|
|
window.addEventListener("resize", update);
|
|
return () => {
|
|
cancelAnimationFrame(frame);
|
|
window.removeEventListener("resize", update);
|
|
};
|
|
}, [board, topic?.id, topic?.data, viewport]);
|
|
return (
|
|
<aside
|
|
ref={ref}
|
|
className="xmind-notes-popover"
|
|
aria-label="主题笔记"
|
|
style={position}
|
|
data-flow-board-shell-control="true"
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onClose();
|
|
}
|
|
}}
|
|
>
|
|
<header>
|
|
<span title={topic ? getTopicTitle(topic) : "笔记"}>
|
|
{topic ? getTopicTitle(topic) : "笔记"}
|
|
</span>
|
|
<div>
|
|
<button
|
|
type="button"
|
|
aria-label="主题详情"
|
|
title="主题详情"
|
|
onClick={onDetails}
|
|
>
|
|
<MoreHorizontal size={15} />
|
|
</button>
|
|
<button type="button" aria-label="关闭笔记" onClick={onClose}>
|
|
<X size={15} />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
{topic ? (
|
|
<XmindNotesEditor
|
|
key={topic.id}
|
|
value={getNoteDocument(topic)}
|
|
onChange={onChange}
|
|
/>
|
|
) : (
|
|
<p className="xmind-note-empty">选择一个主题以编辑笔记。</p>
|
|
)}
|
|
</aside>
|
|
);
|
|
}
|