返回 oh-my-ppt
page-shell.ts
根目录 / src / main / presentation / html / page-shell.ts
1 import { requireSlideSize, type SlideSizePreset } from '@shared/slide-size'
2
3 export const buildBasePageStyleTag = (input: SlideSizePreset): string => {
4 const slideSize = requireSlideSize(input)
5 return `<style id="ppt-page-guard-style">
6 :root {
7 --ppt-page-bg: #ffffff;
8 --ppt-system-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
9 --ppt-slide-width: ${slideSize.width}px;
10 --ppt-slide-height: ${slideSize.height}px;
11 }
12 html, body {
13 margin: 0;
14 width: var(--ppt-slide-width);
15 height: var(--ppt-slide-height);
16 overflow: hidden;
17 font-family: var(--ppt-master-body-font, var(--ppt-body-font, var(--ppt-system-sans)));
18 background: var(--ppt-page-bg);
19 color: #0f172a;
20 }
21 *, *::before, *::after { box-sizing: border-box; }
22 .ppt-page-root[data-ppt-guard-root="1"] {
23 position: relative;
24 width: var(--ppt-slide-width);
25 height: var(--ppt-slide-height);
26 overflow: hidden;
27 isolation: isolate;
28 background: var(--ppt-page-bg);
29 }
30 .ppt-page-root.p-2,
31 .ppt-page-root.p-8,
32 .ppt-page-root.p-12 {
33 padding: 0;
34 }
35 .ppt-page-root[data-ppt-guard-root="1"]:not(.p-2):not(.p-8):not(.p-12) {
36 padding: 0;
37 }
38 body > .ppt-page-root:not([data-ppt-guard-root="1"]):not(.p-2):not(.p-8):not(.p-12) {
39 padding: 0;
40 }
41 .ppt-page-fit-scope {
42 position: relative;
43 width: 100%;
44 height: 100%;
45 transform-origin: top left;
46 overflow: hidden;
47 }
48 .ppt-page-content {
49 width: 100%;
50 height: 100%;
51 min-height: 100%;
52 flex: 1;
53 position: relative;
54 display: flex;
55 flex-direction: column;
56 justify-content: flex-start;
57 align-items: stretch;
58 overflow: hidden;
59 font-size: 16px;
60 font-family: var(--ppt-master-body-font, var(--ppt-body-font, var(--ppt-system-sans)));
61 }
62 .ppt-page-content [data-ppt-readable-fonts="1"] {
63 font-size: 18px;
64 }
65 .ppt-page-content [data-ppt-readable-fonts="1"] [data-ppt-density="high"] {
66 font-size: 16px;
67 }
68 .ppt-page-content h1,
69 .ppt-page-content h2,
70 .ppt-page-content h3,
71 .ppt-page-content h4,
72 .ppt-page-content h5,
73 .ppt-page-content h6,
74 .ppt-page-content [data-role="title"],
75 .ppt-page-content [data-block-id="title"] {
76 font-family: var(--ppt-master-title-font, var(--ppt-title-font, var(--ppt-body-font, var(--ppt-system-sans))));
77 }
78 .ppt-page-content > [data-page-scaffold="1"] {
79 width: 100%;
80 min-height: 100%;
81 height: 100%;
82 }
83 .ppt-page-content canvas {
84 display: block;
85 width: 100%;
86 height: 100%;
87 max-width: 100% !important;
88 max-height: 100% !important;
89 }
90 .ppt-page-content .ppt-chart-frame {
91 position: relative;
92 min-width: 0;
93 overflow: hidden;
94 }
95 .ppt-page-content .ppt-chart-frame > canvas {
96 width: 100% !important;
97 height: 100% !important;
98 }
99 .ppt-page-content [data-block-id*="chart"],
100 .ppt-page-content [data-block-id*="graph"],
101 .ppt-page-content [data-block-id*="plot"] {
102 min-width: 0;
103 }
104 [data-role="title"] h1,
105 header[data-block-id="title"] h1 {
106 font-size: 48px !important;
107 line-height: 1.2 !important;
108 }
109 [data-role="title"] h1.text-5xl,
110 header[data-block-id="title"] h1.text-5xl {
111 font-size: 48px !important;
112 }
113 </style>`
114 }
115
116 export const buildFitScript = (input: SlideSizePreset): string => {
117 const slideSize = requireSlideSize(input)
118 const heightScale = slideSize.height / 900
119 const legacyMinFont = Math.round(14 * heightScale)
120 const auxiliaryMinFont = Math.round(12 * heightScale)
121 const bodyMinFont = Math.round(18 * heightScale)
122 const denseBodyMinFont = Math.round(16 * heightScale)
123 const headingMinFont = Math.round(24 * heightScale)
124 return `<script id="ppt-page-fit">
125 (() => {
126 const WIDTH = ${slideSize.width};
127 const HEIGHT = ${slideSize.height};
128 const LEGACY_MIN_FONT = ${legacyMinFont};
129 const AUXILIARY_MIN_FONT = ${auxiliaryMinFont};
130 const BODY_MIN_FONT = ${bodyMinFont};
131 const DENSE_BODY_MIN_FONT = ${denseBodyMinFont};
132 const HEADING_MIN_FONT = ${headingMinFont};
133 const AUXILIARY_TEXT_SELECTOR = [
134 "footer",
135 "small",
136 "figcaption",
137 '[data-ppt-text-role="auxiliary"]',
138 '[data-role="footer"]',
139 '[data-role="footnote"]',
140 '[data-role="source"]',
141 '[data-role="annotation"]',
142 '[data-role="page-number"]'
143 ].join(",");
144 const search = new URLSearchParams(window.location.search);
145 const disableFit = search.get("fit") === "off";
146 const findRoot = () =>
147 document.querySelector('.ppt-page-root[data-ppt-guard-root="1"]') ||
148 document.querySelector(".ppt-page-root");
149 const collectTextElements = (root) => {
150 const elements = new Set();
151 const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
152 let textNode = walker.nextNode();
153 while (textNode) {
154 const parent = textNode.parentElement;
155 if (
156 textNode.textContent.trim() &&
157 parent &&
158 !parent.closest("script, style, svg, canvas, .katex")
159 ) {
160 elements.add(parent);
161 }
162 textNode = walker.nextNode();
163 }
164 return Array.from(elements);
165 };
166 const isAuxiliaryText = (node) => Boolean(node.closest(AUXILIARY_TEXT_SELECTOR));
167 const isHighDensityText = (node) => Boolean(node.closest('[data-ppt-density="high"]'));
168 const resolveMinimumFontSize = (node, readableRoot) => {
169 if (!readableRoot || !readableRoot.contains(node)) return LEGACY_MIN_FONT;
170 if (isAuxiliaryText(node)) return AUXILIARY_MIN_FONT;
171 if (
172 node.matches("h1, h2, h3, h4, h5, h6") ||
173 node.closest(
174 'h1, h2, h3, h4, h5, h6, [data-role="title"], [data-block-id="title"]'
175 )
176 ) {
177 return HEADING_MIN_FONT;
178 }
179 return isHighDensityText(node) ? DENSE_BODY_MIN_FONT : BODY_MIN_FONT;
180 };
181
182 function fitPage() {
183 const root = findRoot();
184 if (!root) return;
185
186 let scope = root.querySelector(":scope > .ppt-page-fit-scope");
187 let content = null;
188 if (scope) {
189 content =
190 scope.querySelector(":scope > .ppt-page-content") ||
191 scope.querySelector(".ppt-page-content") ||
192 scope;
193 }
194
195 if (!scope) {
196 const directElementChildren = Array.from(root.children);
197 const singleContentChild =
198 directElementChildren.length === 1 &&
199 directElementChildren[0].classList.contains("ppt-page-content")
200 ? directElementChildren[0]
201 : null;
202
203 if (singleContentChild) {
204 content = singleContentChild;
205 } else {
206 const container = document.createElement("div");
207 container.className = "ppt-page-content";
208 container.style.cssText = "white-space:normal;word-wrap:normal;";
209 while (root.firstChild) {
210 container.appendChild(root.firstChild);
211 }
212 content = container;
213 }
214
215 const scopeEl = document.createElement("div");
216 scopeEl.className = "ppt-page-fit-scope";
217 scopeEl.appendChild(content);
218 root.appendChild(scopeEl);
219 scope = scopeEl;
220 }
221
222 scope.style.transform = "scale(1)";
223 const measuredContent = content || scope;
224 const readableRoot = measuredContent.querySelector('[data-ppt-readable-fonts="1"]');
225 const textEntries = collectTextElements(measuredContent).map((node) => ({
226 node,
227 minimum: resolveMinimumFontSize(node, readableRoot),
228 size: Number.parseFloat(getComputedStyle(node).fontSize || "16")
229 }));
230 if (readableRoot) {
231 textEntries.forEach((entry) => {
232 if (entry.minimum <= 0) return;
233 if (Number.isFinite(entry.size) && entry.size < entry.minimum) {
234 entry.size = entry.minimum;
235 entry.node.style.setProperty("font-size", entry.minimum + "px", "important");
236 }
237 });
238 }
239 if (disableFit) {
240 return;
241 }
242 const targetWidth = Math.max(1, Math.floor(scope.clientWidth || root.clientWidth || WIDTH));
243 const targetHeight = Math.max(1, Math.floor(scope.clientHeight || root.clientHeight || HEIGHT));
244 let guard = 0;
245 while ((measuredContent.scrollWidth > targetWidth || measuredContent.scrollHeight > targetHeight) && guard < 12) {
246 let changed = false;
247 textEntries.forEach((entry) => {
248 if (Number.isFinite(entry.size) && entry.size > entry.minimum) {
249 entry.size = Math.max(entry.minimum, Math.floor(entry.size * 0.94));
250 entry.node.style.setProperty("font-size", entry.size + "px", "important");
251 changed = true;
252 }
253 });
254 if (!changed) break;
255 guard += 1;
256 }
257
258 const scale = Math.min(
259 1,
260 targetWidth / Math.max(measuredContent.scrollWidth, 1),
261 targetHeight / Math.max(measuredContent.scrollHeight, 1)
262 );
263 scope.style.transform = "scale(" + scale.toFixed(4) + ")";
264 }
265
266 window.addEventListener("load", () => requestAnimationFrame(fitPage), { once: true });
267 window.addEventListener("resize", fitPage);
268 })();
269 </script>`
270 }
271
272 export const VIDEO_INTERACTION_SCRIPT = `<script id="ppt-video-interaction">
273 (() => {
274 const prepareVideos = () => {
275 document.querySelectorAll("video").forEach((video) => {
276 video.playsInline = true;
277 if (!video.hasAttribute("preload")) {
278 video.preload = "metadata";
279 }
280 });
281 };
282 if (document.readyState === "loading") {
283 document.addEventListener("DOMContentLoaded", prepareVideos, { once: true });
284 } else {
285 prepareVideos();
286 }
287 window.addEventListener("pageshow", prepareVideos);
288 })();
289 </script>`
290
291 export const DEFAULT_MOTION_SCRIPT = `<script id="ppt-default-motion">
292 (() => {
293 const search = new URLSearchParams(window.location.search);
294 if (search.get("print") === "1" || search.get("export") === "1") {
295 document.documentElement.dataset.pptExportStatic = "1";
296 return;
297 }
298
299 function revealFallback(root) {
300 const hiddenTargets = Array.from(root.querySelectorAll("*"))
301 .filter((el) => {
302 const style = getComputedStyle(el);
303 return style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity) === 0;
304 })
305 .slice(0, 120);
306 hiddenTargets.forEach((el, i) => {
307 const node = el;
308 node.style.transition = "opacity 300ms ease, transform 300ms ease";
309 if (!node.style.transform || node.style.transform === "none") {
310 node.style.transform = "translateY(0)";
311 }
312 window.setTimeout(() => {
313 node.style.opacity = "1";
314 }, i * 8);
315 });
316 }
317
318 function runDataAnimMotion(root) {
319 var pptApi = globalThis.PPT;
320 if (!pptApi || typeof pptApi.scanDataAnim !== "function") return false;
321 var config = pptApi.scanDataAnim(root);
322 if (!config || (!config.load.length && !config.click.length)) return false;
323
324 if (config.load.length > 0 && typeof pptApi.executeDataAnim === "function") {
325 pptApi.executeDataAnim(config.load);
326 }
327
328 if (config.click.length > 0 && pptApi.clicks && typeof pptApi.clicks.on === "function") {
329 var clickSteps = Array.isArray(config.clickSteps) && config.clickSteps.length > 0
330 ? config.clickSteps
331 : config.click.map(function (animDef) { return [animDef]; });
332 clickSteps.forEach(function (stepDefs, idx) {
333 var clickNum = idx + 1;
334 pptApi.clicks.on(clickNum, function () {
335 if (typeof pptApi.executeDataAnim === "function") {
336 pptApi.executeDataAnim(stepDefs);
337 } else {
338 stepDefs.forEach(function (animDef) {
339 pptApi.animate(animDef.targets, {
340 opacity: [0, 1],
341 translateY: [20, 0],
342 duration: animDef.duration,
343 easing: animDef.easing
344 });
345 });
346 }
347 });
348 });
349 }
350
351 return true;
352 }
353
354 function runLegacyMotion(root) {
355 var targets = Array.from(
356 root.querySelectorAll(".opacity-0, [data-anime], [data-animate], h1, h2, h3, p, li, .card, .panel, .text-section, .diagram-section, .timeline-node, section, section > *")
357 ).slice(0, 16);
358 if (targets.length === 0) {
359 revealFallback(root);
360 return;
361 }
362 var pptApi = globalThis.PPT;
363 if (pptApi && typeof pptApi.animate === "function") {
364 try {
365 pptApi.animate(targets, {
366 opacity: [0, 1],
367 translateY: [20, 0],
368 easing: "easeOutCubic",
369 duration: 560,
370 delay: function (_el, i) { return i * 45; },
371 });
372 window.setTimeout(function () { revealFallback(root); }, 720);
373 return;
374 } catch (_err) {
375 revealFallback(root);
376 return;
377 }
378 }
379 targets.forEach(function (el, i) {
380 var node = el;
381 node.style.opacity = "0";
382 node.style.transform = "translateY(14px)";
383 node.style.transition = "opacity 420ms ease, transform 420ms ease";
384 window.setTimeout(function () {
385 node.style.opacity = "1";
386 node.style.transform = "translateY(0)";
387 }, i * 40);
388 });
389 revealFallback(root);
390 }
391
392 function runMotion() {
393 var root = document.querySelector(".ppt-page-root");
394 if (!root) return;
395 if (!runDataAnimMotion(root)) {
396 runLegacyMotion(root);
397 }
398 }
399
400 if (document.readyState === "loading") {
401 document.addEventListener("DOMContentLoaded", runMotion, { once: true });
402 } else {
403 runMotion();
404 }
405 })();
406 </script>`
407
407 lines TYPESCRIPT