feat(markdown): wire MarkdownOverlay into canvas lifecycle with React portals
This commit is contained in:
@@ -13,6 +13,7 @@ import { UploadManager } from '../stores/uploadManager';
|
|||||||
import { AnnotationStore } from '../stores/annotationStore';
|
import { AnnotationStore } from '../stores/annotationStore';
|
||||||
import { PinOverlay } from '../canvas/PinOverlay';
|
import { PinOverlay } from '../canvas/PinOverlay';
|
||||||
import { CropOverlay } from '../canvas/CropOverlay';
|
import { CropOverlay } from '../canvas/CropOverlay';
|
||||||
|
import { MarkdownOverlay } from '../canvas/MarkdownOverlay';
|
||||||
import { TextSprite } from '../canvas/sprites/TextSprite';
|
import { TextSprite } from '../canvas/sprites/TextSprite';
|
||||||
import { TextSharpnessManager } from '../canvas/textSharpness';
|
import { TextSharpnessManager } from '../canvas/textSharpness';
|
||||||
// PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit
|
// PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit
|
||||||
@@ -78,6 +79,7 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
const textEditorRef = useRef<TextEditor | null>(null);
|
const textEditorRef = useRef<TextEditor | null>(null);
|
||||||
if (!textEditorRef.current) textEditorRef.current = new TextEditor();
|
if (!textEditorRef.current) textEditorRef.current = new TextEditor();
|
||||||
const cropOverlayRef = useRef<CropOverlay | null>(null);
|
const cropOverlayRef = useRef<CropOverlay | null>(null);
|
||||||
|
const mdOverlayRef = useRef<MarkdownOverlay | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!boardData || !resolvedBoardId) return;
|
if (!boardData || !resolvedBoardId) return;
|
||||||
@@ -177,6 +179,13 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
sharpness.destroy();
|
sharpness.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Markdown overlay — DOM divs positioned over canvas for each markdown card ──
|
||||||
|
if (domContainer) {
|
||||||
|
const mdOverlay = new MarkdownOverlay(viewport, scene, domContainer);
|
||||||
|
mdOverlayRef.current = mdOverlay;
|
||||||
|
mdOverlay.refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
// Create InboxZone and add to viewport
|
// Create InboxZone and add to viewport
|
||||||
const inboxZone = new InboxZone(scene.textures, scene.springs);
|
const inboxZone = new InboxZone(scene.textures, scene.springs);
|
||||||
viewport.addChild(inboxZone);
|
viewport.addChild(inboxZone);
|
||||||
@@ -198,14 +207,19 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
selection.onItemsTransform = (items) => {
|
selection.onItemsTransform = (items) => {
|
||||||
syncRef.current?.broadcastTransform(items);
|
syncRef.current?.broadcastTransform(items);
|
||||||
pinOverlayRef.current?.updatePositions();
|
pinOverlayRef.current?.updatePositions();
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type === 'markdown') mdOverlayRef.current?.updateItem(item.id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
selection.onItemTransform = (item) => {
|
selection.onItemTransform = (item) => {
|
||||||
syncRef.current?.broadcastTransform(item);
|
syncRef.current?.broadcastTransform(item);
|
||||||
pinOverlayRef.current?.updatePositions();
|
pinOverlayRef.current?.updatePositions();
|
||||||
|
if (item.type === 'markdown') mdOverlayRef.current?.updateItem(item.id);
|
||||||
};
|
};
|
||||||
selection.transformBox.onItemTransform = (item) => {
|
selection.transformBox.onItemTransform = (item) => {
|
||||||
syncRef.current?.broadcastTransform(item);
|
syncRef.current?.broadcastTransform(item);
|
||||||
pinOverlayRef.current?.updatePositions();
|
pinOverlayRef.current?.updatePositions();
|
||||||
|
if (item.type === 'markdown') mdOverlayRef.current?.updateItem(item.id);
|
||||||
};
|
};
|
||||||
selection.onObjectDragEnd = (itemIds) => {
|
selection.onObjectDragEnd = (itemIds) => {
|
||||||
onCanvasChange(itemIds); // broadcasts elements + saves + undo + spatial refresh
|
onCanvasChange(itemIds); // broadcasts elements + saves + undo + spatial refresh
|
||||||
@@ -483,6 +497,10 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
dropCleanupRef.current?.();
|
dropCleanupRef.current?.();
|
||||||
pasteCleanupRef.current?.();
|
pasteCleanupRef.current?.();
|
||||||
textEditorRef.current?.stopEditing(false);
|
textEditorRef.current?.stopEditing(false);
|
||||||
|
if (mdOverlayRef.current) {
|
||||||
|
mdOverlayRef.current.destroy();
|
||||||
|
mdOverlayRef.current = null;
|
||||||
|
}
|
||||||
if (cropOverlayRef.current) {
|
if (cropOverlayRef.current) {
|
||||||
cropOverlayRef.current.destroy();
|
cropOverlayRef.current.destroy();
|
||||||
cropOverlayRef.current = null;
|
cropOverlayRef.current = null;
|
||||||
@@ -499,5 +517,5 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
|
|||||||
};
|
};
|
||||||
}, [boardData, resolvedBoardId, user, isPublicView, onCanvasChange, showToast, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, uploadManager, setOnlineUsers, setSelectedLayerIds]);
|
}, [boardData, resolvedBoardId, user, isPublicView, onCanvasChange, showToast, canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, uploadManager, setOnlineUsers, setSelectedLayerIds]);
|
||||||
|
|
||||||
return { annotationStore: annotationStoreRef.current, pinOverlay: pinOverlayRef.current, textEditor: textEditorRef.current, cropOverlayRef };
|
return { annotationStore: annotationStoreRef.current, pinOverlay: pinOverlayRef.current, textEditor: textEditorRef.current, cropOverlayRef, mdOverlay: mdOverlayRef.current };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ import { InboxZone } from '../canvas/InboxZone';
|
|||||||
import { getItemWorldBounds } from '../canvas/SceneManager';
|
import { getItemWorldBounds } from '../canvas/SceneManager';
|
||||||
import { getPointAnchorWorld } from '../canvas/reviewAnchors';
|
import { getPointAnchorWorld } from '../canvas/reviewAnchors';
|
||||||
import { resolveReviewTargetAtPoint } from '../canvas/reviewTargeting';
|
import { resolveReviewTargetAtPoint } from '../canvas/reviewTargeting';
|
||||||
import type { TextObject, StickyObject } from '../canvas/scene-format';
|
import type { TextObject, StickyObject, MarkdownObject } from '../canvas/scene-format';
|
||||||
import { VideoSprite } from '../canvas/sprites/VideoSprite';
|
import { VideoSprite } from '../canvas/sprites/VideoSprite';
|
||||||
import { TextSprite } from '../canvas/sprites/TextSprite';
|
import { TextSprite } from '../canvas/sprites/TextSprite';
|
||||||
import { StickySprite } from '../canvas/sprites/StickySprite';
|
import { StickySprite } from '../canvas/sprites/StickySprite';
|
||||||
import * as ops from '../canvas/operations';
|
import * as ops from '../canvas/operations';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import MarkdownReadView from '../components/MarkdownReadView';
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
import { useBoardLoader } from '../hooks/useBoardLoader';
|
import { useBoardLoader } from '../hooks/useBoardLoader';
|
||||||
@@ -197,12 +199,38 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Canvas setup (selection, undo, sync, socket, drag/drop, paste, inbox, annotations)
|
// Canvas setup (selection, undo, sync, socket, drag/drop, paste, inbox, annotations)
|
||||||
const { annotationStore, pinOverlay, textEditor, cropOverlayRef } = useCanvasSetup({
|
const { annotationStore, pinOverlay, textEditor, cropOverlayRef, mdOverlay } = useCanvasSetup({
|
||||||
boardData, resolvedBoardId, user, isPublicView,
|
boardData, resolvedBoardId, user, isPublicView,
|
||||||
canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef,
|
canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef,
|
||||||
uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds,
|
uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Markdown overlay — sync visible card IDs for React portal rendering ──
|
||||||
|
const [mdCardIds, setMdCardIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mdOverlay) return;
|
||||||
|
|
||||||
|
const syncIds = () => {
|
||||||
|
const scene = canvasRef.current?.getScene();
|
||||||
|
if (!scene) return;
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (const [id, item] of scene.items) {
|
||||||
|
if (item.type === 'markdown' && item.data.visible) {
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMdCardIds(prev => {
|
||||||
|
if (prev.length === ids.length && prev.every((v, i) => v === ids[i])) return prev;
|
||||||
|
return ids;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
mdOverlay.onChange = syncIds;
|
||||||
|
syncIds();
|
||||||
|
return () => { mdOverlay.onChange = null; };
|
||||||
|
}, [mdOverlay]);
|
||||||
|
|
||||||
// Build canvas objects map for FeedbackPanel (memoized on object count changes)
|
// Build canvas objects map for FeedbackPanel (memoized on object count changes)
|
||||||
const canvasObjectMap = React.useMemo(() => {
|
const canvasObjectMap = React.useMemo(() => {
|
||||||
const scene = canvasRef.current?.getScene();
|
const scene = canvasRef.current?.getScene();
|
||||||
@@ -1179,6 +1207,30 @@ export default function Editor({ isPublicView }: EditorProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Markdown card portals */}
|
||||||
|
{mdCardIds.map(id => {
|
||||||
|
const mountPoint = mdOverlay?.getMountPoint(id);
|
||||||
|
const item = canvasRef.current?.getScene()?.getById(id);
|
||||||
|
if (!mountPoint || !item || item.type !== 'markdown') return null;
|
||||||
|
const data = item.data as MarkdownObject;
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<MarkdownReadView
|
||||||
|
key={id}
|
||||||
|
content={data.content}
|
||||||
|
textColor={data.textColor}
|
||||||
|
accentColor={data.accentColor}
|
||||||
|
bgColor={data.bgColor}
|
||||||
|
padding={data.padding}
|
||||||
|
onCheckboxToggle={(newContent) => {
|
||||||
|
data.content = newContent;
|
||||||
|
onCanvasChange([id]);
|
||||||
|
mdOverlay?.measureHeight(id);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
mountPoint,
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Export dialog */}
|
{/* Export dialog */}
|
||||||
{showExport && (
|
{showExport && (
|
||||||
<ExportDialog
|
<ExportDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user