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