refactor(uve): explicit iframe sizing with DevTools-style canvas controls#35539
Conversation
Phase 1 of #35514. Removes the ResizeObserver/MutationObserver pipeline that derived the canvas height from the iframe document. Pages with viewport-relative units on layout containers (e.g. min-height: 100dvh) caused an infinite growth loop because the iframe's height was both an input to and an output of layout. The iframe now has explicit dimensions (viewIframeWidth, viewIframeHeight) that are user-controlled inputs, defaulting to 1520 x 1080. Content taller than the iframe scrolls inside the iframe. Inside the iframe, vh/dvh resolve against a fixed viewport, so there is no feedback loop. Overlays will glitch on iframe scroll until phase 2 wires scroll-aware clearing of contentlet bounds.
The iframe element used h-full but its host (<dot-uve-iframe>) had no height, so 100% resolved against zero. Add w-full h-full to the host class so it fills .iframe-wrapper.
…-sizing WIP — more tweaks pending. Adds DevTools-style explicit iframe sizing: - viewIframeWidth / viewIframeHeight are now device-preset and user-driven (resize handles, numeric inputs), not derived from page content. - New $viewIsResponsiveMode computed (true when no specific device preset). - viewSetDevice / viewSetOrientation now set explicit pixel dimensions. - viewSetIframeSize clamps to MIN_IFRAME_WIDTH / MIN_IFRAME_HEIGHT. Editor: - ResizeObserver on .canvas-viewport syncs iframe size to viewport in responsive mode. Effect resyncs when user exits a device preset. - Removed legacy $iframeWrapperStyles override; wrapper always fills the store-sized .canvas-inner. - Initial state width/height = 0; ngAfterViewInit applies real size before first paint to avoid scrollbar flash on load. New components (Tailwind, no SCSS): - <dot-uve-iframe-resize-handles>: full-length right/bottom bars + corner ball. Hidden in device mode. Drag delta divided by zoom. - <dot-uve-iframe-size-input>: width × height numeric inputs in toolbar. Editing while a device is active flips back to responsive mode.
Previously the iframe could grow beyond what the canvas could show at the current zoom — e.g. drag to 2000px at 50% zoom (visually fits in 1000px), then zoom back to 100% and the canvas had to scroll. Now in responsive mode the iframe size is clamped so iframe * zoom never exceeds the canvas viewport. The clamp re-applies on every zoom change (viewZoomSetLevel, in/out/reset all delegate to it) and on every drag / numeric-input update. Device-mode keeps preset dimensions unchanged and lets the canvas scroll if needed. Adds viewCanvasAvailableWidth/Height to UVEState and viewSetCanvasAvailableSize action; the editor's ResizeObserver feeds the real (padding/gutter-excluded) canvas size into the store.
Reuses the existing scroll plumbing — updateEditorScrollState on pointer down, updateEditorOnScrollEnd on pointer up — so editorBounds and editorContentArea clear during the drag and restore on release.
The canvas row uses margin: 0 auto, so growing the iframe shifted the right/bottom edge by only half the size delta — the cursor visibly outpaced the handle. Switch the drag math to measure the handle's current edge each frame and grow the iframe by the cursor's distance from that edge. The handle stays under the cursor whether the canvas is centered, left-anchored after overflow, or transitioning between the two.
Drag and numeric-input updates now cap the iframe so its zoomed size never exceeds the available canvas — no horizontal/vertical canvas scrollbars in responsive mode. Device mode is unaffected (preset sizes keep their dimensions; canvas scrolls if needed).
Switch .browser-url-bar-container from align-items: center to align-items: stretch so all toolbar items match the tallest one. Drop the redundant h-full on dot-uve-iframe-size-input — flex stretch handles it.
- viewZoomSetLevel no longer reshapes the iframe — zooming in past the canvas just makes the canvas scroll (DevTools behavior). Size inputs stay stable while zooming. - viewZoomReset now also snaps the iframe to the canvas viewport in responsive mode (device mode keeps its preset size). Reset becomes a one-click 'fit and zoom 100%'.
The iframe's on-screen size is now always the user-set dimensions (handles never move with zoom). Zoom changes only the iframe's internal viewport via inverse scaling: at 70% zoom the iframe is laid out at dimensions/0.7 CSS pixels and scaled by 0.7 to fill the same on-screen box. The page inside the iframe sees a larger or smaller viewport depending on zoom; canvas never scrolls.
viewIframeWidth/Height now represent on-screen size (since zoom adjusts the iframe's internal viewport, not its on-screen extent). Update both the resize-handle drag math and the responsive-mode canvas clamp to work directly in on-screen pixels — no more dividing by zoom — so the iframe stays inside the canvas at any zoom.
…mode - Picking a device computes a fit zoom so device dims × zoom fit the canvas; iframe gets the on-screen size and viewZoomLevel is set to match. Same logic on orientation flip. - Resize handles now render in device mode too. Dragging from a device preset switches back to responsive (clears the device) so the canvas clamp and user-driven sizing take over.
The default device left zoom and size untouched, so it inherited values from a previously-selected device. Picking it now snaps the iframe to the available canvas viewport at 100% zoom — the expected responsive default.
The zoom controls now expose a borderless p-select (50/75/100/150/200%) instead of the discrete +/- buttons. Borders, background, padding and focus ring are zeroed via [dt] design-token overrides so only the value and chevron icon remain in the toolbar.
Add size="small" and zero the label's right padding so the value sits flush against the chevron.
The size input and zoom select share a single rounded gray container with thin dividers between sections. The reset button moved to the left of the pill (inside it) since it now resets both zoom and canvas size.
Device presets compute an auto-fit zoom (e.g. 67%) that isn't in the predefined list, so p-select rendered an empty label. The options list now appends the current zoom when it isn't a preset, and the bound value is rounded to match the option label exactly.
- dot-uve-device-controls now renders the device buttons inside p-buttongroup; the active device is filled and the rest are outlined. - Drop the gray pill background from dot-uve-device-controls so the group sits flat on the toolbar. - Tighten left padding on the merged size+zoom pill (the reset button brings its own padding).
Lock .browser-toolbar to flex-wrap: nowrap and prevent the start/end slots from shrinking. The center slot, the URL container, and the URL pill all get min-width: 0 so the URL bar can compress and ellipsis its text on narrow toolbars instead of wrapping to a second row.
- Toolbar center now flex+gap and never wraps; uses auto-margin centering so its overflow scrolls all the way to the start. - Browser-toolbar contains its scroll instead of bleeding the layout. - :host grid uses minmax(0, 1fr) and .editor-content min-width: 0 so the editor track shrinks under both side panels at narrow widths. - Device-controls reverts to plain rounded buttons in a gray pill (active = bg-gray-200), no host padding so buttons sit edge-to-edge. - Round all toolbar p-button icons to text-gray-900 (copy-url, zoom reset, orientation, device buttons). - Browser URL pill gets pr-3; size+zoom select padding zeroed via dt and label !p-0.
Use a container query on .p-toolbar-center: under 48rem the URL text hides and the pill drops its padding/max-width so only the copy-link icon button remains. Move the pill's right padding from HTML utility class into SCSS so the query can clear it.
Switch .browser-url-pill from flex: 1 1 0 (always grow) to flex: 0 1 auto so it sits at its content width up to max-width: 28rem and only shrinks under toolbar pressure.
Restructure the copy-URL popover so each row stacks the (lighter gray-700) label above the URL with a small gap, and places the copy button to the right vertically centered with both. List items get more breathing room and a divider line beneath each row.
Inject GlobalStore in the editor and append a third entry in $pageURLS using the current site's hostname (from globalStore.siteDetails) plus the current page path. Uses the i18n key uve.toolbar.page.current.site.url.
Move the progress bar out of the toolbar's center template into a zero-height sibling wrapper. The bar inside is absolutely positioned just above its slot so it overlays the bottom edge of the toolbar without pushing the toolbar contents around.
viewSetDevice(DEFAULT_DEVICE) snapped the iframe back to canvas at 100% zoom, causing a visible jump the moment the user grabbed a resize handle on a device preview. Add viewExitDevicePreset that just clears the device flag without changing size or zoom, and call it from the resize handles after flipping editorState to SCROLLING — the responsive-mode sync effect now skips its canvas-snap while scrolling, so the iframe stays exactly where the user grabbed it.
The active-state check only matched on inode equality, so after exiting a device preset (viewDevice = null) no button highlighted. Treat a null device as the desktop preset so the desktop button reflects 'responsive mode' both on explicit selection and on auto-exit.
Switch handle elements from divs to <button> for semantics and a11y, bump the bar thickness to 1rem (w-4 / h-4), and use a quieter gray palette: base bg-gray-300, hover bg-gray-500, active bg-gray-600 for all three handles.
Drop the .edit-panel-wrapper clamp min from 400px to 360px so the sidebar fits more comfortably alongside the iframe canvas on smaller displays.
Drop the per-tab utility classes and styleClass overrides on both the palette and the right sidebar tablist; the design tokens already size and lay out the tabs. Add a tablist background design token override (var(--gray-100)) for both.
|
Pull Request Unsafe to Rollback!!!
|
…b.com/dotCMS/core into issue-35514-uve-iframe-sizing-phase-1
|
Pull Request Unsafe to Rollback!!!
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pull Request Unsafe to Rollback!!!
|
|
Pull Request Unsafe to Rollback!!!
|
Summary
Closes #35514. Replaces the iframe height-observation pipeline with explicit, user-controllable iframe sizing, and rebuilds the overlay, selection, and bounds-sync machinery on top.
The original bug was an infinite growth loop on pages using viewport-relative units (e.g.
min-height: 100dvh) — the iframe's height was both an input to and output of layout.Iframe sizing
Bounds and overlay anchoring
UVE_FLUSH_BOUNDS) for drag/drop's synchronous needs.$iframeLayoutLockedpredicate captures "iframe is mid-flux" in one place.withSelectionAnchorslice.(inode, container.identifier, container.uuid)to disambiguate duplicate contentlets.Selection and the hover toolbar
Quick-edit form
markAllAsTouched()to surface validation errors instead of silently no-op'ing.Page navigation
pageAssetResponse: nullwas being written inside the load reset.UI consistency
<button>+ Tailwind utilities — nop-button/[dt]/::ng-deepworkarounds.Architecture doc
libs/portlets/edit-ema/ARCHITECTURE.mdcovers the contracts above.Test plan
min-height: 100dvhpage renders without runaway growthTests added
withView.spec.ts(~30 cases): iframe sizing, device-fit math, orientation refit, exit-preset, zoom reset/snapdot-uve-iframe-resize-handles.component.spec.ts: pointerdown/move/up + destroy-mid-dragdot-uve-zoom-controls.component.spec.ts: option list, change handlerdot-uve-contentlet-tools.component.spec.ts: hover overlay rendering + content-type labeledit-ema-editor.component.spec.ts:handleSectionOffsetregressionSome specs reference symbols renamed or removed during the bounds-channel and binary-selector refactors; cleanup deferred.
🤖 Generated with Claude Code
This PR fixes: #35514