editor-ts 0.0.1 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,672 @@
1
+ import type { Page } from './Page';
2
+
3
+ export type IframeCanvasBuildOptions = {
4
+ title: string;
5
+ css: string;
6
+ htmlBody: string;
7
+ };
8
+
9
+ function buildWysiwygCss(): string {
10
+ // Pure CSS only.
11
+ return `
12
+ /* WYSIWYG editing styles */
13
+ .editorts-highlight {
14
+ outline: 2px dashed var(--color-editor-light-text, #212C3E) !important;
15
+ outline-offset: 2px;
16
+ cursor: pointer !important;
17
+ position: relative !important;
18
+ }
19
+ .editorts-highlight:hover {
20
+ outline: 2px solid var(--color-editor-light-text, #212C3E) !important;
21
+ }
22
+ .editorts-selected {
23
+ outline: 3px solid #10b981 !important;
24
+ }
25
+
26
+ .editorts-box-model {
27
+ position: absolute;
28
+ inset: 0;
29
+ pointer-events: none;
30
+ z-index: 9998;
31
+ box-sizing: border-box;
32
+ display: none;
33
+ }
34
+ .editorts-box-model [data-layer] {
35
+ position: absolute;
36
+ inset: 0;
37
+ box-sizing: border-box;
38
+ border-style: solid;
39
+ pointer-events: none;
40
+ }
41
+ .editorts-box-model [data-layer="margin"] {
42
+ border-color: rgba(251, 146, 60, 0.35);
43
+ }
44
+ .editorts-box-model [data-layer="padding"] {
45
+ border-color: rgba(59, 130, 246, 0.3);
46
+ }
47
+
48
+ .editorts-flash {
49
+ animation: editortsFlash 300ms cubic-bezier(0.2, 0.8, 0.2, 1) 1;
50
+ }
51
+
52
+ @keyframes editortsFlash {
53
+ 0% {
54
+ outline: 4px solid rgba(245, 158, 11, 0.95);
55
+ outline-offset: 4px;
56
+ }
57
+ 60% {
58
+ outline: 4px solid rgba(245, 158, 11, 0.65);
59
+ outline-offset: 3px;
60
+ }
61
+ 100% {
62
+ outline: 3px solid rgba(16, 185, 129, 0.85);
63
+ outline-offset: 2px;
64
+ }
65
+ }
66
+ .editorts-drag-over {
67
+ outline: 3px dashed #3b82f6 !important;
68
+ outline-offset: 2px;
69
+ }
70
+ .editorts-dragging {
71
+ opacity: 0.5 !important;
72
+ }
73
+ .editorts-context-toolbar {
74
+ position: absolute;
75
+ top: 0;
76
+ left: 0;
77
+ transform: translateY(calc(-100% - 8px));
78
+ z-index: 10000;
79
+ background: white;
80
+ border: 2px solid var(--color-editor-light-text, #212C3E);
81
+ border-radius: 6px;
82
+ padding: 0.4rem;
83
+ display: flex;
84
+ gap: 0.3rem;
85
+ box-shadow: 0 4px 20px rgba(0,0,0,0.25);
86
+ z-index: 9999;
87
+ }
88
+ .toolbar-action {
89
+ background: white;
90
+ border: 1px solid var(--color-primary-border, #e5e7eb);
91
+ padding: 0.5rem 0.75rem;
92
+ border-radius: 4px;
93
+ cursor: pointer;
94
+ font-size: 0.85rem;
95
+ white-space: nowrap;
96
+ font-family: var(--font-main);
97
+ transition: all 0.2s;
98
+ }
99
+ .toolbar-action:hover {
100
+ background: var(--color-editor-light-bg, #EDF0F5);
101
+ border-color: var(--color-editor-light-text, #212C3E);
102
+ }
103
+ .toolbar-action.danger:hover {
104
+ background: #fee;
105
+ border-color: #ef4444;
106
+ color: #ef4444;
107
+ }
108
+ /* Text editing mode */
109
+ .editorts-editing {
110
+ outline: 3px solid #3b82f6 !important;
111
+ background-color: rgba(59, 130, 246, 0.1) !important;
112
+ cursor: text !important;
113
+ min-height: 1em;
114
+ }
115
+ .editorts-editing:focus {
116
+ outline: 3px solid #2563eb !important;
117
+ background-color: rgba(59, 130, 246, 0.15) !important;
118
+ }
119
+ /* Image editing mode */
120
+ .editorts-image-editing {
121
+ outline: 3px solid #f59e0b !important;
122
+ background-color: rgba(245, 158, 11, 0.1) !important;
123
+ cursor: pointer !important;
124
+ }
125
+ /* Hidden file input */
126
+ #editorts-file-input {
127
+ display: none;
128
+ }
129
+ `;
130
+ }
131
+
132
+ // NOTE: This function is stringified and executed inside the iframe.
133
+ // Keep dependencies limited to browser globals.
134
+ function iframeWysiwygScript() {
135
+ let selectedElement: HTMLElement | null = null;
136
+ let editingElement: HTMLElement | null = null;
137
+ let originalContent = '';
138
+ let imageEditTarget: HTMLElement | null = null;
139
+ let fileInput: HTMLInputElement | null = null;
140
+
141
+ // Drag and drop state
142
+ let draggedElement: HTMLElement | null = null;
143
+ let draggedId: string | null = null;
144
+ const dropEdgeThreshold = 0.25;
145
+
146
+ const isComponentElement = (el: HTMLElement | null): el is HTMLElement => {
147
+ return !!el && !!el.id && !el.id.startsWith('editorts-');
148
+ };
149
+
150
+ const parsePixelValue = (value: string | null): number => {
151
+ if (!value) return 0;
152
+ const parsed = Number.parseFloat(value);
153
+ return Number.isFinite(parsed) ? parsed : 0;
154
+ };
155
+
156
+ const toPixelStyle = (value: number) => `${Math.max(0, value)}px`;
157
+
158
+ const getBoxModelMetrics = (el: HTMLElement) => {
159
+ const style = window.getComputedStyle(el);
160
+ return {
161
+ marginTop: parsePixelValue(style.marginTop),
162
+ marginRight: parsePixelValue(style.marginRight),
163
+ marginBottom: parsePixelValue(style.marginBottom),
164
+ marginLeft: parsePixelValue(style.marginLeft),
165
+ paddingTop: parsePixelValue(style.paddingTop),
166
+ paddingRight: parsePixelValue(style.paddingRight),
167
+ paddingBottom: parsePixelValue(style.paddingBottom),
168
+ paddingLeft: parsePixelValue(style.paddingLeft),
169
+ };
170
+ };
171
+
172
+ const ensureBoxModelOverlay = (el: HTMLElement) => {
173
+ let overlay = el.querySelector('.editorts-box-model') as HTMLDivElement | null;
174
+ if (!overlay) {
175
+ overlay = document.createElement('div');
176
+ overlay.className = 'editorts-box-model';
177
+
178
+ const marginLayer = document.createElement('div');
179
+ marginLayer.dataset.layer = 'margin';
180
+ const paddingLayer = document.createElement('div');
181
+ paddingLayer.dataset.layer = 'padding';
182
+
183
+ overlay.appendChild(marginLayer);
184
+ overlay.appendChild(paddingLayer);
185
+ el.appendChild(overlay);
186
+ }
187
+
188
+ return overlay;
189
+ };
190
+
191
+ const updateBoxModelOverlay = (el: HTMLElement) => {
192
+ const overlay = ensureBoxModelOverlay(el);
193
+ const marginLayer = overlay.querySelector('[data-layer="margin"]') as HTMLDivElement | null;
194
+ const paddingLayer = overlay.querySelector('[data-layer="padding"]') as HTMLDivElement | null;
195
+ if (!marginLayer || !paddingLayer) return;
196
+
197
+ const metrics = getBoxModelMetrics(el);
198
+
199
+ marginLayer.style.position = 'absolute';
200
+ marginLayer.style.inset = `-${metrics.marginTop}px -${metrics.marginRight}px -${metrics.marginBottom}px -${metrics.marginLeft}px`;
201
+ marginLayer.style.borderWidth = `${metrics.marginTop}px ${metrics.marginRight}px ${metrics.marginBottom}px ${metrics.marginLeft}px`;
202
+ marginLayer.style.background = 'transparent';
203
+
204
+ paddingLayer.style.position = 'absolute';
205
+ paddingLayer.style.inset = '0';
206
+ paddingLayer.style.borderWidth = `${metrics.paddingTop}px ${metrics.paddingRight}px ${metrics.paddingBottom}px ${metrics.paddingLeft}px`;
207
+ paddingLayer.style.background = 'transparent';
208
+ };
209
+
210
+ const showBoxModelOverlay = (el: HTMLElement) => {
211
+ const overlay = ensureBoxModelOverlay(el);
212
+ updateBoxModelOverlay(el);
213
+ overlay.style.display = 'block';
214
+ };
215
+
216
+ const hideBoxModelOverlay = (el: HTMLElement) => {
217
+ const overlay = el.querySelector('.editorts-box-model') as HTMLDivElement | null;
218
+ if (overlay) overlay.style.display = 'none';
219
+ };
220
+
221
+ const getParentComponent = (el: HTMLElement): HTMLElement | null => {
222
+ let parent = el.parentElement;
223
+ while (parent) {
224
+ const candidate = parent as HTMLElement;
225
+ if (isComponentElement(candidate)) return candidate;
226
+ parent = parent.parentElement;
227
+ }
228
+ return null;
229
+ };
230
+
231
+ const getChildComponents = (el: HTMLElement): HTMLElement[] => {
232
+ return Array.from(el.children)
233
+ .filter((child): child is HTMLElement => isComponentElement(child as HTMLElement));
234
+ };
235
+
236
+ const getRootComponents = (): HTMLElement[] => {
237
+ return Array.from(document.body.children)
238
+ .filter((child): child is HTMLElement => isComponentElement(child as HTMLElement));
239
+ };
240
+
241
+ const getDropPosition = (el: HTMLElement, clientY: number): 'before' | 'inside' | 'after' => {
242
+ const rect = el.getBoundingClientRect();
243
+ const offset = clientY - rect.top;
244
+ const edgeSize = rect.height * dropEdgeThreshold;
245
+
246
+ if (offset <= edgeSize) return 'before';
247
+ if (offset >= rect.height - edgeSize) return 'after';
248
+ return 'inside';
249
+ };
250
+
251
+ // Double-tap detection for mobile
252
+ const doubleTapDelay = 300; // ms
253
+ let lastTapTime = 0;
254
+ let lastTapElement: HTMLElement | null = null;
255
+
256
+ const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
257
+
258
+ function handleDoubleAction(el: HTMLElement) {
259
+ const imgElement = getImageElement(el);
260
+ if (imgElement) {
261
+ startImageEdit(el, imgElement);
262
+ } else {
263
+ startTextEdit(el);
264
+ }
265
+ }
266
+
267
+ function initWYSIWYG() {
268
+ fileInput = document.createElement('input');
269
+ fileInput.type = 'file';
270
+ fileInput.id = 'editorts-file-input';
271
+ fileInput.accept = 'image/*';
272
+ fileInput.addEventListener('change', handleImageSelect);
273
+ document.body.appendChild(fileInput);
274
+
275
+ document.querySelectorAll<HTMLElement>('[id]').forEach((el) => {
276
+ if (!el.id || el.id.startsWith('editorts-')) return;
277
+
278
+ el.classList.add('editorts-highlight');
279
+ el.setAttribute('draggable', 'true');
280
+
281
+ el.addEventListener('dragstart', (e) => {
282
+ if (editingElement || imageEditTarget) {
283
+ e.preventDefault();
284
+ return;
285
+ }
286
+
287
+ draggedElement = el;
288
+ draggedId = el.id;
289
+ el.classList.add('editorts-dragging');
290
+
291
+ e.dataTransfer?.setData('text/plain', el.id);
292
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
293
+ });
294
+
295
+ el.addEventListener('dragend', () => {
296
+ if (draggedElement) draggedElement.classList.remove('editorts-dragging');
297
+ document.querySelectorAll('.editorts-drag-over').forEach((n) => n.classList.remove('editorts-drag-over'));
298
+ draggedElement = null;
299
+ draggedId = null;
300
+ });
301
+
302
+ el.addEventListener('dragover', (e) => {
303
+ if (!draggedId || draggedId === el.id) return;
304
+ e.preventDefault();
305
+ el.classList.add('editorts-drag-over');
306
+ });
307
+
308
+ el.addEventListener('dragleave', () => {
309
+ el.classList.remove('editorts-drag-over');
310
+ });
311
+
312
+ el.addEventListener('drop', (e) => {
313
+ e.preventDefault();
314
+ el.classList.remove('editorts-drag-over');
315
+
316
+ if (!draggedId || draggedId === el.id) return;
317
+
318
+ const position = getDropPosition(el, e.clientY);
319
+ const parentComponent = getParentComponent(el);
320
+ const targetParentId = parentComponent ? parentComponent.id : null;
321
+
322
+ let newParentId = targetParentId;
323
+ let newIndex = 0;
324
+
325
+ if (position === 'inside') {
326
+ newParentId = el.id;
327
+ newIndex = getChildComponents(el).length;
328
+ } else {
329
+ const siblings = parentComponent ? getChildComponents(parentComponent) : getRootComponents();
330
+ const currentIndex = siblings.findIndex((node) => node.id === el.id);
331
+ const baseIndex = currentIndex === -1 ? siblings.length : currentIndex;
332
+ newIndex = position === 'before' ? baseIndex : baseIndex + 1;
333
+ }
334
+
335
+ window.parent.postMessage(
336
+ {
337
+ type: 'editorts:canvasReorder',
338
+ draggedId,
339
+ targetId: el.id,
340
+ targetParentId: newParentId,
341
+ targetIndex: newIndex,
342
+ },
343
+ '*'
344
+ );
345
+ });
346
+
347
+ el.addEventListener('mouseenter', () => {
348
+ if (editingElement || imageEditTarget) return;
349
+ showBoxModelOverlay(el);
350
+ });
351
+
352
+ el.addEventListener('mouseleave', () => {
353
+ hideBoxModelOverlay(el);
354
+ });
355
+
356
+ el.addEventListener('click', (e) => {
357
+ if (editingElement === el) return;
358
+ e.stopPropagation();
359
+ selectElement(el);
360
+ });
361
+
362
+ el.addEventListener('dblclick', (e) => {
363
+ e.stopPropagation();
364
+ e.preventDefault();
365
+ handleDoubleAction(el);
366
+ });
367
+
368
+ if (isTouchDevice) {
369
+ el.addEventListener(
370
+ 'touchend',
371
+ (e) => {
372
+ const currentTime = Date.now();
373
+ const tapLength = currentTime - lastTapTime;
374
+
375
+ if (lastTapElement === el && tapLength < doubleTapDelay && tapLength > 0) {
376
+ e.preventDefault();
377
+ e.stopPropagation();
378
+ handleDoubleAction(el);
379
+ lastTapTime = 0;
380
+ lastTapElement = null;
381
+ } else {
382
+ lastTapTime = currentTime;
383
+ lastTapElement = el;
384
+ }
385
+ },
386
+ { passive: false }
387
+ );
388
+ }
389
+ });
390
+ }
391
+
392
+ function selectElement(el: HTMLElement) {
393
+ if (selectedElement) {
394
+ selectedElement.classList.remove('editorts-selected');
395
+ hideBoxModelOverlay(selectedElement);
396
+ const oldToolbar = selectedElement.querySelector('.editorts-context-toolbar');
397
+ if (oldToolbar) oldToolbar.remove();
398
+ }
399
+
400
+ selectedElement = el;
401
+ el.classList.add('editorts-selected');
402
+
403
+ window.parent.postMessage(
404
+ {
405
+ type: 'editorts:componentSelected',
406
+ id: el.id,
407
+ tagName: el.tagName.toLowerCase(),
408
+ className: el.className,
409
+ },
410
+ '*'
411
+ );
412
+
413
+ window.parent.postMessage(
414
+ {
415
+ type: 'editorts:getToolbar',
416
+ id: el.id,
417
+ },
418
+ '*'
419
+ );
420
+ }
421
+
422
+ type ToolbarAction = { id: string; label: string; icon: string; enabled: boolean; danger?: boolean };
423
+ type ToolbarConfig = { enabled: boolean; actions: ToolbarAction[] };
424
+
425
+ function isToolbarConfig(value: unknown): value is ToolbarConfig {
426
+ if (!value || typeof value !== 'object') return false;
427
+ const v = value as ToolbarConfig;
428
+ return typeof v.enabled === 'boolean' && Array.isArray(v.actions);
429
+ }
430
+
431
+ function renderToolbar(toolbarConfig: unknown, elementId: string) {
432
+ const el = document.getElementById(elementId);
433
+ if (!el || !isToolbarConfig(toolbarConfig) || !toolbarConfig.enabled) return;
434
+
435
+ const toolbar = document.createElement('div');
436
+ toolbar.className = 'editorts-context-toolbar';
437
+
438
+ const enabledActions = toolbarConfig.actions.filter((a) => a.enabled);
439
+ enabledActions.forEach((action) => {
440
+ const btn = document.createElement('button');
441
+ btn.className = 'toolbar-action' + (action.danger ? ' danger' : '');
442
+ btn.textContent = action.icon + ' ' + action.label;
443
+ btn.onclick = () => {
444
+ window.parent.postMessage(
445
+ {
446
+ type: 'editorts:toolbarAction',
447
+ action: action.id,
448
+ elementId,
449
+ },
450
+ '*'
451
+ );
452
+ };
453
+ toolbar.appendChild(btn);
454
+ });
455
+
456
+ el.appendChild(toolbar);
457
+
458
+ // If the toolbar would be clipped above the viewport, place it below.
459
+ const elRect = el.getBoundingClientRect();
460
+ const toolbarRect = toolbar.getBoundingClientRect();
461
+ const needsBelow = elRect.top - toolbarRect.height - 8 < 0;
462
+
463
+ if (needsBelow) {
464
+ toolbar.style.top = '100%';
465
+ toolbar.style.transform = 'translateY(8px)';
466
+ } else {
467
+ toolbar.style.top = '0';
468
+ toolbar.style.transform = 'translateY(calc(-100% - 8px))';
469
+ }
470
+ }
471
+
472
+ function handleToolbarAction(action: string) {
473
+ if (!selectedElement) return;
474
+
475
+ if (action === 'delete') {
476
+ selectedElement.remove();
477
+ }
478
+ }
479
+
480
+ function getImageElement(el: HTMLElement): HTMLImageElement | null {
481
+ if (el.tagName.toLowerCase() === 'img') return el as unknown as HTMLImageElement;
482
+ return el.querySelector('img');
483
+ }
484
+
485
+ function startTextEdit(el: HTMLElement) {
486
+ if (editingElement) return;
487
+
488
+ editingElement = el;
489
+ originalContent = el.textContent ?? '';
490
+
491
+ el.classList.add('editorts-editing');
492
+ el.contentEditable = 'true';
493
+ el.focus();
494
+
495
+ const onBlur = () => {
496
+ finishTextEdit(el, true);
497
+ el.removeEventListener('blur', onBlur);
498
+ };
499
+
500
+ el.addEventListener('blur', onBlur);
501
+ }
502
+
503
+ function finishTextEdit(el: HTMLElement, save: boolean) {
504
+ if (!editingElement) return;
505
+
506
+ const newContent = el.textContent ?? '';
507
+
508
+ el.contentEditable = 'false';
509
+ el.classList.remove('editorts-editing');
510
+
511
+ window.parent.postMessage(
512
+ {
513
+ type: 'editorts:textEditEnd',
514
+ id: el.id,
515
+ content: newContent,
516
+ originalContent,
517
+ saved: save && newContent !== originalContent,
518
+ },
519
+ '*'
520
+ );
521
+
522
+ editingElement = null;
523
+ originalContent = '';
524
+
525
+ selectElement(el);
526
+ }
527
+
528
+ function startImageEdit(container: HTMLElement, img: HTMLImageElement) {
529
+ imageEditTarget = container;
530
+ container.classList.add('editorts-image-editing');
531
+
532
+ window.parent.postMessage(
533
+ {
534
+ type: 'editorts:imageEditStart',
535
+ id: container.id,
536
+ src: img.getAttribute('src') ?? '',
537
+ },
538
+ '*'
539
+ );
540
+
541
+ if (fileInput) {
542
+ fileInput.click();
543
+ }
544
+ }
545
+
546
+ function handleImageSelect(e: Event) {
547
+ if (!imageEditTarget || !fileInput) return;
548
+
549
+ const target = e.target as HTMLInputElement;
550
+ const file = target.files?.[0];
551
+ if (!file) return;
552
+
553
+ const reader = new FileReader();
554
+ reader.onload = () => {
555
+ const img = getImageElement(imageEditTarget!);
556
+ if (img) {
557
+ img.src = String(reader.result ?? '');
558
+ }
559
+
560
+ window.parent.postMessage(
561
+ {
562
+ type: 'editorts:imageUpdate',
563
+ id: imageEditTarget!.id,
564
+ src: String(reader.result ?? ''),
565
+ file: {
566
+ name: file.name,
567
+ type: file.type,
568
+ size: file.size,
569
+ },
570
+ },
571
+ '*'
572
+ );
573
+
574
+ imageEditTarget!.classList.remove('editorts-image-editing');
575
+ imageEditTarget = null;
576
+ if (fileInput) {
577
+ fileInput.value = '';
578
+ }
579
+ };
580
+
581
+ reader.readAsDataURL(file);
582
+ }
583
+
584
+ let placementMode = false;
585
+
586
+ window.addEventListener('message', (event) => {
587
+ if (event.data.type === 'editorts:toolbarConfig') {
588
+ renderToolbar(event.data.config, event.data.elementId);
589
+ } else if (event.data.type === 'editorts:toolbarAction') {
590
+ handleToolbarAction(event.data.action);
591
+ } else if (event.data.type === 'editorts:selectComponent') {
592
+ const el = document.getElementById(event.data.id);
593
+ if (el) {
594
+ selectElement(el);
595
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
596
+ }
597
+ } else if (event.data.type === 'editorts:placementMode') {
598
+ placementMode = !!event.data.enabled;
599
+ document.body.style.cursor = placementMode ? 'crosshair' : '';
600
+ } else if (event.data.type === 'editorts:flashSelect') {
601
+ const el = document.getElementById(event.data.id);
602
+ if (el) {
603
+ // Flash by retriggering CSS animation
604
+ el.classList.remove('editorts-flash');
605
+ // Force reflow
606
+ void el.offsetHeight;
607
+ el.classList.add('editorts-flash');
608
+
609
+ selectElement(el);
610
+ showBoxModelOverlay(el);
611
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
612
+ }
613
+
614
+ }
615
+ });
616
+
617
+ document.addEventListener('click', (e) => {
618
+ if (!placementMode) return;
619
+
620
+ const target = e.target as HTMLElement | null;
621
+ const el = target?.closest('[id]') as HTMLElement | null;
622
+ if (!el || !el.id || el.id.startsWith('editorts-')) return;
623
+
624
+ e.preventDefault();
625
+ e.stopPropagation();
626
+
627
+ placementMode = false;
628
+ document.body.style.cursor = '';
629
+
630
+ window.parent.postMessage(
631
+ {
632
+ type: 'editorts:placeComponent',
633
+ targetId: el.id,
634
+ },
635
+ '*'
636
+ );
637
+ }, true);
638
+
639
+ if (document.readyState === 'loading') {
640
+ document.addEventListener('DOMContentLoaded', initWYSIWYG);
641
+ } else {
642
+ initWYSIWYG();
643
+ }
644
+ }
645
+
646
+ export function buildIframeCanvasSrcdoc(options: IframeCanvasBuildOptions): string {
647
+ const { title, css, htmlBody } = options;
648
+
649
+ // Serialize a real function body into the iframe.
650
+ const scriptSource = `(${iframeWysiwygScript.toString()})();`;
651
+
652
+ return `<!DOCTYPE html>
653
+ <html>
654
+ <head>
655
+ <meta charset="UTF-8">
656
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
657
+ <title>${title}</title>
658
+ <style>${css}</style>
659
+ <style>${buildWysiwygCss()}</style>
660
+ </head>
661
+ ${htmlBody}
662
+ <script>${scriptSource}</script>
663
+ </html>`;
664
+ }
665
+
666
+ export function buildIframeCanvasSrcdocFromPage(page: Page): string {
667
+ return buildIframeCanvasSrcdoc({
668
+ title: page.getTitle(),
669
+ css: page.getCSS(),
670
+ htmlBody: page.getHTML(),
671
+ });
672
+ }