@@ -111,13 +111,44 @@ const URL_PREVIEW_SCROLL_BRIDGE = `<script data-od-url-scroll-bridge>
111111 if (window.__odUrlScrollBridge) return;
112112 window.__odUrlScrollBridge = true;
113113 var pending = false;
114+ var contentSizePending = false;
114115 function scrollElement(){
115116 return document.querySelector('.design-canvas') || document.scrollingElement || document.documentElement;
116117 }
117118 function num(value){
118119 var next = Number(value || 0);
119120 return Number.isFinite(next) ? next : 0;
120121 }
122+ function measureContentWidth(){
123+ var root = document.documentElement;
124+ var body = document.body || root;
125+ if (!root) return null;
126+ var values = [
127+ root.scrollWidth,
128+ body && body.scrollWidth,
129+ root.offsetWidth,
130+ body && body.offsetWidth,
131+ root.clientWidth,
132+ body && body.clientWidth
133+ ];
134+ var width = 0;
135+ for (var i = 0; i < values.length; i += 1) {
136+ var next = num(values[i]);
137+ if (next > width) width = next;
138+ }
139+ return width > 0 ? Math.ceil(width) : null;
140+ }
141+ function postContentSize(){
142+ window.parent.postMessage({ type: 'od:preview-content-size', width: measureContentWidth() }, '*');
143+ }
144+ function scheduleContentSize(){
145+ if (contentSizePending) return;
146+ contentSizePending = true;
147+ window.requestAnimationFrame(function(){
148+ contentSizePending = false;
149+ postContentSize();
150+ });
151+ }
121152 function post(){
122153 var el = scrollElement();
123154 if (!el) return;
@@ -172,22 +203,44 @@ const URL_PREVIEW_SCROLL_BRIDGE = `<script data-od-url-scroll-bridge>
172203 if (data.type === 'od:preview-scroll-by') {
173204 scrollBy(scrollElement(), data.left, data.top);
174205 schedule();
206+ scheduleContentSize();
207+ return;
208+ }
209+ if (data.type === 'od:preview-content-size-request') {
210+ scheduleContentSize();
175211 }
176212 });
177213 window.addEventListener('scroll', schedule, true);
178214 document.addEventListener('scroll', schedule, true);
179- window.addEventListener('resize', schedule);
215+ window.addEventListener('resize', function(){
216+ schedule();
217+ scheduleContentSize();
218+ });
219+ if (typeof ResizeObserver !== 'undefined') {
220+ try {
221+ var observer = new ResizeObserver(scheduleContentSize);
222+ observer.observe(document.documentElement);
223+ if (document.body) observer.observe(document.body);
224+ } catch (_) {}
225+ }
180226 if (document.readyState === 'loading') {
181227 document.addEventListener('DOMContentLoaded', function(){
182228 requestRestore();
183229 schedule();
230+ scheduleContentSize();
184231 });
185232 } else {
186233 setTimeout(function(){
187234 requestRestore();
188235 schedule();
236+ scheduleContentSize();
189237 }, 0);
190238 }
239+ setTimeout(scheduleContentSize, 80);
240+ setTimeout(scheduleContentSize, 260);
241+ if (document.fonts && document.fonts.ready) {
242+ document.fonts.ready.then(scheduleContentSize).catch(function(){});
243+ }
191244})();
192245</script>` ;
193246
@@ -809,6 +862,39 @@ function injectUrlPreviewBridge(html: string, bridge: 'scroll' | 'selection' | '
809862 return injectBeforeBodyClose ( html , 'data-od-url-snapshot-bridge' , URL_PREVIEW_SNAPSHOT_BRIDGE ) ;
810863}
811864
865+ function applyUrlPreviewBridgesToHtml (
866+ transformed : string | Buffer ,
867+ mime : string ,
868+ requestedBridge : unknown ,
869+ ) : string | Buffer {
870+ if (
871+ ! (
872+ wantsUrlPreviewScrollBridge ( requestedBridge ) ||
873+ wantsUrlPreviewSelectionBridge ( requestedBridge ) ||
874+ wantsUrlPreviewSnapshotBridge ( requestedBridge )
875+ ) ||
876+ ! / ^ t e x t \/ h t m l (?: ; | $ ) / i. test ( mime )
877+ ) {
878+ return transformed ;
879+ }
880+
881+ let html = Buffer . isBuffer ( transformed ) ? transformed . toString ( 'utf8' ) : transformed ;
882+ // Sanitize the <title> so Cmd+P -> "Save as PDF" produces a Teams-safe
883+ // filename. URL-load iframes cannot rely on the host rewriting the document
884+ // title after load, and powered previews are intentionally cross-origin.
885+ html = daemonSanitizeTitleInDoc ( html ) ;
886+ if ( wantsUrlPreviewScrollBridge ( requestedBridge ) ) {
887+ html = injectUrlPreviewBridge ( html , 'scroll' ) ;
888+ }
889+ if ( wantsUrlPreviewSelectionBridge ( requestedBridge ) ) {
890+ html = injectUrlPreviewBridge ( html , 'selection' ) ;
891+ }
892+ if ( wantsUrlPreviewSnapshotBridge ( requestedBridge ) ) {
893+ html = injectUrlPreviewBridge ( html , 'snapshot' ) ;
894+ }
895+ return html ;
896+ }
897+
812898// ---------------------------------------------------------------------------
813899// Teams-safe title sanitization for the URL-load preview path (issue #3918).
814900//
@@ -3222,39 +3308,15 @@ export function registerProjectFileRoutes(app: Express, ctx: RegisterProjectFile
32223308 project ?. metadata ,
32233309 undefined ,
32243310 skipHtmlPreviewBridge ? undefined : async ( file ) => {
3225- let transformed = await maybeResolveVitePreviewHtml ( {
3311+ const transformed = await maybeResolveVitePreviewHtml ( {
32263312 file,
32273313 projectId,
32283314 relPath,
32293315 metadata : project ?. metadata ,
32303316 projectsRoot : PROJECTS_DIR ,
32313317 readProjectFile,
32323318 } ) ;
3233- if (
3234- ( wantsUrlPreviewScrollBridge ( req . query . odPreviewBridge ) ||
3235- wantsUrlPreviewSelectionBridge ( req . query . odPreviewBridge ) ||
3236- wantsUrlPreviewSnapshotBridge ( req . query . odPreviewBridge ) ) &&
3237- / ^ t e x t \/ h t m l (?: ; | $ ) / i. test ( file . mime )
3238- ) {
3239- let html = Buffer . isBuffer ( transformed ) ? transformed . toString ( 'utf8' ) : transformed ;
3240- // Sanitize the <title> so Cmd+P → "Save as PDF" produces a
3241- // Teams-safe filename. The URL-load iframe uses sandbox without
3242- // allow-same-origin, so the host cannot rewrite contentDocument.title
3243- // after load — we must do it here in the response. The srcDoc path
3244- // has its own sanitization in buildSrcdoc (apps/web/src/runtime/srcdoc.ts).
3245- html = daemonSanitizeTitleInDoc ( html ) ;
3246- if ( wantsUrlPreviewScrollBridge ( req . query . odPreviewBridge ) ) {
3247- html = injectUrlPreviewBridge ( html , 'scroll' ) ;
3248- }
3249- if ( wantsUrlPreviewSelectionBridge ( req . query . odPreviewBridge ) ) {
3250- html = injectUrlPreviewBridge ( html , 'selection' ) ;
3251- }
3252- if ( wantsUrlPreviewSnapshotBridge ( req . query . odPreviewBridge ) ) {
3253- html = injectUrlPreviewBridge ( html , 'snapshot' ) ;
3254- }
3255- transformed = html ;
3256- }
3257- return transformed ;
3319+ return applyUrlPreviewBridgesToHtml ( transformed , file . mime , req . query . odPreviewBridge ) ;
32583320 } ,
32593321 true , // revalidate: emit ETag/Last-Modified so covers/preview/export reuse cached assets
32603322 ) ;
@@ -3304,15 +3366,17 @@ export function registerProjectFileRoutes(app: Express, ctx: RegisterProjectFile
33043366 relPath ,
33053367 project ?. metadata ,
33063368 ( ) => setPoweredPreviewHeaders ( res ) ,
3307- skipPoweredTransform ? undefined : async ( file ) =>
3308- maybeResolveVitePreviewHtml ( {
3369+ skipPoweredTransform ? undefined : async ( file ) => {
3370+ const transformed = await maybeResolveVitePreviewHtml ( {
33093371 file,
33103372 projectId,
3311- relPath,
3312- metadata : project ?. metadata ,
3313- projectsRoot : PROJECTS_DIR ,
3314- readProjectFile,
3315- } ) ,
3373+ relPath,
3374+ metadata : project ?. metadata ,
3375+ projectsRoot : PROJECTS_DIR ,
3376+ readProjectFile,
3377+ } ) ;
3378+ return applyUrlPreviewBridgesToHtml ( transformed , file . mime , req . query . odPreviewBridge ) ;
3379+ } ,
33163380 ) ;
33173381 } catch ( err : any ) {
33183382 const status = err && err . code === 'ENOENT' ? 404 : 400 ;
0 commit comments