返回 presentation-ai
useRootImageHeight.ts
根目录 / src / components / notebook / presentation / editor / hooks / useRootImageHeight.ts
1 "use client";
2
3 import { useEffect, useMemo, useRef, useState } from "react";
4
5 import { BASE_HEIGHT } from "@/hooks/presentation/useRootImageActions";
6 import { type PlateSlide } from "../../utils/parser";
7
8 const PRESENTING_ROOT_IMAGE_MAX_VIEWPORT_RATIO_DEFAULT = 0.5;
9 const PRESENTING_ROOT_IMAGE_MAX_VIEWPORT_RATIO_SMALL = 0.35;
10 const SMALL_VIEWPORT_HEIGHT_THRESHOLD = 700;
11
12 function getPresentingRootImageMaxViewportRatio(
13 viewportHeight: number,
14 ): number {
15 if (viewportHeight <= SMALL_VIEWPORT_HEIGHT_THRESHOLD) {
16 return PRESENTING_ROOT_IMAGE_MAX_VIEWPORT_RATIO_SMALL;
17 }
18 return PRESENTING_ROOT_IMAGE_MAX_VIEWPORT_RATIO_DEFAULT;
19 }
20
21 interface UseRootImageHeightOptions {
22 isPresenting: boolean;
23 initialContent?: PlateSlide;
24 }
25
26 interface UseRootImageHeightReturn {
27 editorRef: React.RefObject<HTMLDivElement | null>;
28 shouldCapRootImage: boolean;
29 maxRootImageHeight: number | undefined;
30 presentingRootImageHeight: number | undefined;
31 presentingMaxRootImageHeight: number | undefined;
32 }
33
34 /**
35 * Hook to size vertical root images for edit and present modes.
36 * In edit mode, the image is capped to the editor height so it never exceeds
37 * half of the slide. In present mode, the current ratio-based sizing is
38 * preserved but the final computed height is capped to 50vh.
39 */
40 export function useRootImageHeight({
41 isPresenting,
42 initialContent,
43 }: UseRootImageHeightOptions): UseRootImageHeightReturn {
44 const editorRef = useRef<HTMLDivElement | null>(null);
45 const [editorHeightPx, setEditorHeightPx] = useState<number | undefined>(
46 undefined,
47 );
48 const [viewportHeightPx, setViewportHeightPx] = useState<number | undefined>(
49 undefined,
50 );
51
52 const isVerticalRootImageLayout = useMemo(
53 () =>
54 Boolean(
55 initialContent?.rootImage && initialContent.layoutType === "vertical",
56 ),
57 [initialContent?.layoutType, initialContent?.rootImage],
58 );
59
60 const shouldCapRootImage = useMemo(
61 () => isVerticalRootImageLayout && !isPresenting,
62 [isPresenting, isVerticalRootImageLayout],
63 );
64
65 const maxRootImageHeight = useMemo(() => {
66 if (!shouldCapRootImage) return undefined;
67 return editorHeightPx;
68 }, [editorHeightPx, shouldCapRootImage]);
69
70 const rootImageHeightRatio = useMemo(() => {
71 if (!isVerticalRootImageLayout) return undefined;
72
73 const rootImageHeight = initialContent?.rootImage?.size?.h ?? BASE_HEIGHT;
74 if (rootImageHeight <= 0 || !editorHeightPx || editorHeightPx <= 0) {
75 return undefined;
76 }
77
78 const totalSlideHeight = rootImageHeight + editorHeightPx;
79 if (totalSlideHeight <= 0) return undefined;
80
81 return Math.min(rootImageHeight / totalSlideHeight, 1);
82 }, [
83 editorHeightPx,
84 initialContent?.rootImage?.size?.h,
85 isVerticalRootImageLayout,
86 ]);
87
88 const presentingMaxRootImageHeight = useMemo(() => {
89 if (!isPresenting || !isVerticalRootImageLayout || !viewportHeightPx) {
90 return undefined;
91 }
92
93 const ratio = getPresentingRootImageMaxViewportRatio(viewportHeightPx);
94 return Math.round(viewportHeightPx * ratio);
95 }, [isPresenting, isVerticalRootImageLayout, viewportHeightPx]);
96
97 const presentingRootImageHeight = useMemo(() => {
98 if (
99 !isPresenting ||
100 !isVerticalRootImageLayout ||
101 rootImageHeightRatio === undefined ||
102 !viewportHeightPx
103 ) {
104 return undefined;
105 }
106
107 const calculatedHeight = Math.round(
108 rootImageHeightRatio * viewportHeightPx,
109 );
110
111 if (!presentingMaxRootImageHeight) {
112 return calculatedHeight;
113 }
114
115 return Math.min(calculatedHeight, presentingMaxRootImageHeight);
116 }, [
117 isPresenting,
118 isVerticalRootImageLayout,
119 rootImageHeightRatio,
120 presentingMaxRootImageHeight,
121 viewportHeightPx,
122 ]);
123
124 // Observe editor height changes
125 useEffect(() => {
126 if (!isVerticalRootImageLayout) {
127 setEditorHeightPx(undefined);
128 return;
129 }
130
131 const element = editorRef.current;
132 if (!element || typeof ResizeObserver === "undefined") return;
133
134 let rafId = 0;
135 const updateHeight = () => {
136 const nextHeight = Math.round(element.getBoundingClientRect().height);
137 setEditorHeightPx((prev) => (prev === nextHeight ? prev : nextHeight));
138 };
139
140 updateHeight();
141 const observer = new ResizeObserver(() => {
142 cancelAnimationFrame(rafId);
143 rafId = requestAnimationFrame(updateHeight);
144 });
145 observer.observe(element);
146
147 return () => {
148 cancelAnimationFrame(rafId);
149 observer.disconnect();
150 };
151 }, [
152 isVerticalRootImageLayout,
153 initialContent?.id,
154 initialContent?.layoutType,
155 ]);
156
157 useEffect(() => {
158 if (!isPresenting || !isVerticalRootImageLayout) {
159 setViewportHeightPx(undefined);
160 return;
161 }
162
163 const updateViewportHeight = () => {
164 const element = editorRef.current;
165 const slideRoot = element?.closest<HTMLElement>(
166 '[data-slide-content="true"]',
167 );
168 const rootHeight = slideRoot?.clientHeight;
169 const nextHeight =
170 rootHeight && rootHeight > 0
171 ? rootHeight
172 : Math.max(
173 window.innerHeight,
174 Math.round(window.visualViewport?.height ?? 0),
175 );
176 setViewportHeightPx((prev) => (prev === nextHeight ? prev : nextHeight));
177 };
178
179 updateViewportHeight();
180 window.addEventListener("resize", updateViewportHeight, {
181 passive: true,
182 });
183 window.visualViewport?.addEventListener("resize", updateViewportHeight);
184
185 return () => {
186 window.removeEventListener("resize", updateViewportHeight);
187 window.visualViewport?.removeEventListener(
188 "resize",
189 updateViewportHeight,
190 );
191 };
192 }, [isPresenting, isVerticalRootImageLayout]);
193
194 return {
195 editorRef,
196 shouldCapRootImage,
197 maxRootImageHeight,
198 presentingRootImageHeight,
199 presentingMaxRootImageHeight,
200 };
201 }
202
202 lines TYPESCRIPT