返回 presentation-ai
EditablePlate.tsx
根目录 / src / components / notebook / presentation / editor / components / EditablePlate.tsx
1 "use client";
2
3 import { type Value } from "platejs";
4 import { Plate, type PlateEditor } from "platejs/react";
5 import { useCallback, useMemo, useRef, type CSSProperties } from "react";
6
7 import { Editor } from "@/components/plate/ui/editor";
8 import ImageGenerationModel from "@/components/plate/ui/image-generation-model";
9 import { cn } from "@/lib/utils";
10 import { type PlateSlide } from "../../utils/parser";
11 import RootImage from "../custom-elements/root-image";
12 import LayoutImageDrop from "../dnd/components/LayoutImageDrop";
13 import { useEditorSurfaceDrop } from "../dnd/hooks/useEditorSurfaceDrop";
14 import { usePresentModeEditorScale } from "../hooks/usePresentModeEditorScale";
15 import { useRootImageHeight } from "../hooks/useRootImageHeight";
16 import { getAlignmentClass, getAlignmentStyle } from "../utils/alignment-utils";
17
18 interface EditablePlateProps {
19 editor: PlateEditor;
20 className?: string;
21 id?: string;
22 readOnly: boolean;
23 isPreview: boolean;
24 isGenerating: boolean;
25 isPresenting: boolean;
26 initialContent?: PlateSlide;
27 onFocusSlide: (slideId: string) => void;
28 onDebouncedChange: (value: Value) => void;
29 }
30
31 export function EditablePlate({
32 editor,
33 className,
34 id,
35 readOnly,
36 isPreview,
37 isGenerating,
38 isPresenting,
39 initialContent,
40 onFocusSlide,
41 onDebouncedChange,
42 }: EditablePlateProps) {
43 const isReadOnlyMode = readOnly || isPresenting;
44 const editorRegionRef = useRef<HTMLDivElement | null>(null);
45 const { isEditorSurfaceDropActive, setEditorSurfaceDropRef } =
46 useEditorSurfaceDrop({
47 disabled: isReadOnlyMode || isPreview || isGenerating,
48 editor,
49 });
50 // Use extracted hook for root image height calculations
51 const {
52 editorRef,
53 shouldCapRootImage,
54 maxRootImageHeight,
55 presentingRootImageHeight,
56 presentingMaxRootImageHeight,
57 } = useRootImageHeight({
58 isPresenting,
59 initialContent,
60 });
61
62 // Use extracted utility for alignment style
63 const alignmentStyle = useMemo(
64 () =>
65 getAlignmentStyle(
66 isPresenting,
67 initialContent?.alignment,
68 initialContent?.layoutType,
69 ),
70 [isPresenting, initialContent?.alignment, initialContent?.layoutType],
71 );
72 const shouldUsePresentModeEditorScale =
73 isPresenting &&
74 (initialContent?.formatCategory ?? "presentation") !== "social";
75 const presentModeEditorScale = usePresentModeEditorScale({
76 isPresenting: shouldUsePresentModeEditorScale,
77 initialContent,
78 editorRef,
79 regionRef: editorRegionRef,
80 });
81
82 const setEditorRegionRefs = useCallback(
83 (node: HTMLDivElement | null) => {
84 editorRegionRef.current = node;
85 setEditorSurfaceDropRef(node);
86 },
87 [setEditorSurfaceDropRef],
88 );
89
90 const presentModeStageStyle: CSSProperties | undefined =
91 shouldUsePresentModeEditorScale
92 ? {
93 height: presentModeEditorScale.shouldScroll
94 ? `${presentModeEditorScale.scaledContentHeight}px`
95 : "100%",
96 minHeight: presentModeEditorScale.shouldScroll ? undefined : "100%",
97 position: "relative",
98 width: "100%",
99 }
100 : undefined;
101
102 const presentModeEditorStyle: CSSProperties | undefined =
103 shouldUsePresentModeEditorScale
104 ? {
105 left: `${presentModeEditorScale.leftOffset}px`,
106 position: "absolute",
107 top: `${presentModeEditorScale.topOffset}px`,
108 transform: `scale(${presentModeEditorScale.contentScale})`,
109 transformOrigin: "top left",
110 width: presentModeEditorScale.logicalWidth
111 ? `${presentModeEditorScale.logicalWidth}px`
112 : "100%",
113 }
114 : undefined;
115
116 return (
117 <Plate
118 editor={editor}
119 onValueChange={({ value }) => {
120 if (isReadOnlyMode || isGenerating) return;
121 onDebouncedChange(value);
122 }}
123 readOnly={isGenerating || isReadOnlyMode}
124 >
125 {!isReadOnlyMode && <LayoutImageDrop slideId={initialContent?.id ?? ""} />}
126 <div
127 ref={setEditorRegionRefs}
128 data-presentation-editor-region="true"
129 className={cn(
130 "flex flex-1 flex-col transition-shadow",
131 shouldUsePresentModeEditorScale ? "min-h-0" : "min-h-max",
132 shouldUsePresentModeEditorScale &&
133 (presentModeEditorScale.shouldScroll
134 ? "overflow-visible"
135 : "overflow-clip"),
136 isEditorSurfaceDropActive && "ring-2 ring-primary/60 ring-inset",
137 )}
138 >
139 <div
140 className={cn(
141 !shouldUsePresentModeEditorScale &&
142 "flex min-h-max flex-1 flex-col",
143 )}
144 style={presentModeStageStyle}
145 >
146 <Editor
147 ref={editorRef}
148 className={cn(
149 className,
150 "@container/presentation-slide-content flex flex-1 flex-col overflow-clip border-none bg-transparent! py-8 outline-hidden",
151 isReadOnlyMode && "px-4 md:px-8",
152 isPresenting && shouldCapRootImage && "self-start",
153 getAlignmentClass(initialContent?.alignment),
154 )}
155 id={id}
156 variant="ghost"
157 readOnly={isPreview || isGenerating || isReadOnlyMode}
158 style={{
159 ...alignmentStyle,
160 ...presentModeEditorStyle,
161 }}
162 onFocus={() =>
163 initialContent?.id && onFocusSlide(initialContent.id)
164 }
165 />
166 </div>
167 </div>
168
169 {initialContent?.rootImage &&
170 initialContent.layoutType !== undefined &&
171 initialContent.layoutType !== "background" &&
172 initialContent.layoutType !== "none" && (
173 <RootImage
174 image={initialContent.rootImage}
175 layoutType={initialContent.layoutType}
176 slideId={initialContent.id}
177 maxHeightPx={
178 shouldCapRootImage
179 ? maxRootImageHeight
180 : presentingMaxRootImageHeight
181 }
182 heightPx={presentingRootImageHeight}
183 />
184 )}
185 {!isReadOnlyMode && <ImageGenerationModel />}
186 </Plate>
187 );
188 }
189
189 lines Plain Text