Add editor-preview eye button with live split-view preview - #398
Merged
Conversation
Post/page/CPT editor windows get a "Preview" (eye) title-bar button. Clicking it autosaves the editor over the bridge, snaps it to the left half, and opens the official front-end preview (get_preview_post_link(), autosave-aware + nonce'd) as a companion window snapped right. The pairing keeps the preview current and cleans itself up: - Live typing updates: the editor iframe watches its own content and, 1.5s after a typing pause, autosaves (__unstableSaveForPreview) and nudges the shell to refresh — with three feedback-loop guards so the watcher's own saves never read as fresh edits (absorb save churn, dirty gate, autosaveable gate). Classic editor rides core's own autosave ticks. - Silent double-buffered refresh: new Window.swapReload() loads the fresh render into a twin iframe UNDERNEATH the visible one (fully rasterized, covered while loading) and cuts over in one tick on load — no loading overlay, no white flash, scroll preserved. Save-driven reloads (broadcast bus) share the same path. - Pairing lifecycle: editor close destroys the companion, preview close only unpairs, navigating to different content closes it, second eye click toggles off (aria-pressed tracked). Companion is ephemeral (new WindowConfig flag) — never session-restored, its nonce wouldn't survive. - Unsaved "Add New" screens show the eye disabled with a save hint; the first save enables it live via the identity refetch. - Bridge-less same-origin iframe documents (front-end preview, home default window) now forward pointerdown to the shell focus path, so clicking anywhere in the preview focuses its window. Server side: desktop_mode_window_preview_url() attaches previewUrl to post-editor content identities (page render + REST recompute), gated on viewability + edit_post, filterable via desktop_mode_window_preview_url. New bridge messages (editor-autosave-request/response, editor-live-watch/unwatch/saved) are catalogued in the protocol and documented in bridge-protocol.md; JS surface (EDITOR_PREVIEW_* hooks, live filter, swapReload, ephemeral) in javascript-reference.md; PHPUnit + Vitest coverage throughout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015x434YzuoS1aXywkaS55St
…nion Two gaps found reviewing #398: - Window.swapReload(): the promoted twin re-wired the overlay contract and the focus forwarder, but not the submenu tab-sync `load` listener from construction — any window with submenu tabs stopped highlighting the active tab after its first swap. Extracted `_wireTabNavSync()`, shared by construction and the swap promotion (which also syncs once for the already-fired load). - onEyeClick(): the pairing is recorded only after `await manager.open()`, so an editor that closed (or navigated to different content) while the companion was opening slipped past the lifecycle handlers and stranded an orphaned preview window. Re-validate after the open and destroy the companion when the editor is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015x434YzuoS1aXywkaS55St
The chromeless bridge posts `desktop-mode-focus-request` on every pointerdown — but only on the outer admin document. Gutenberg renders the post canvas inside a nested iframe (`editor-canvas`), and TinyMCE's visual mode uses `#content_ifr`: clicks in there never reach the outer document's listener, so clicking into the canvas of an unfocused editor window was swallowed (only the toolbar/sidebar would activate it — visible since the preview companion made iframe → iframe focus moves routine). Attach the same capture-phase escalation inside every same-origin nested frame: swept at boot and on DOM mutations (Gutenberg mounts the canvas asynchronously and re-creates it), re-hooked per document on each frame load. WeakSets keep the sweep idempotent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015x434YzuoS1aXywkaS55St
epeicher
approved these changes
Jul 24, 2026
epeicher
left a comment
Collaborator
There was a problem hiding this comment.
Thanks @AllTerrainDeveloper! I have tested this, and it works as expected. I can see the new Preview there. Changes also LGTM! 🚢
…ye-button # Conflicts: # docs/api-index.md
The nested-frame focus escalation installed a MutationObserver in every chromeless iframe whose callback re-ran `document.querySelectorAll( 'iframe' )` on each mutation batch. The WeakSets made the hooking idempotent but not the sweep, so the full-tree query fired regardless — an O(DOM) walk on Gutenberg's typing path, which is exactly when the editor mutates hardest and the editor-preview pairing is live. Walk each record's `addedNodes` instead, matching the shape the component-sniffer observer at the top of this file already uses. A frame that was never inserted cannot need hooking, so the narrow sweep is behaviour-preserving; the WeakSets still cover subtrees that are moved rather than created. Also stub `contentWindow` in the post-swap `reload()` test: jsdom has no navigation, so the real `location.reload()` logged "Not implemented" to the virtual console (it does not throw, so `reload()`'s own catch never ran) and the noise landed in CI logs. jsdom's `Location` rejects spies, hence the element-level stub. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
epeicher
added a commit
that referenced
this pull request
Jul 27, 2026
Resolves conflicts from the editor-preview (#398) and corner-radius (#422) features by keeping trunk's new content with its version stamps stripped, per the new AGENTS.md rule: new @SInCE tags in window-links.php, editor-preview/*, types.ts, window/index.ts, hooks.ts, and the new hooks-reference / javascript-reference / bridge-protocol / api-index sections all land clean. Also repairs three headings in bridge-protocol.md that the original cleanup had accidentally glued to their following paragraph, and the one cross-reference that pointed at a glued slug.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Post/page/CPT editor windows (Gutenberg and classic) get a Preview (eye) title-bar button. Clicking it:

__unstableSaveForPreview()), so the preview reflects on-screen content.get_preview_post_link(), autosave-aware + nonce'd) as a companion window snapped right.The editor↔preview pairing then keeps itself correct:
desktop-mode.editor-preview.livefilter.Window.swapReload(): the fresh render loads into a twin iframe underneath the visible one (fully rasterized while covered) and cuts over in one tick on load. No loading overlay, no white flash, scroll position preserved. Save-driven reloads (broadcast bus: Gutenberg save-watcher, classic footer emitter, heartbeat catch-up) share the same path.aria-pressed). The companion isephemeral(newWindowConfigflag) — never session-restored, its preview nonce wouldn't survive.pointerdownto the shell focus path, so clicking anywhere inside the preview focuses its window (previously only the title bar worked).Server side
desktop_mode_window_preview_url()attachespreviewUrlto post-editor content identities (page render + REST recompute), gated onis_post_type_viewable()+current_user_can( 'edit_post' ), filterable via the newdesktop_mode_window_preview_urlfilter. The client engine only accepts same-origin values.Protocol & docs
New typed bridge messages:
desktop-mode-editor-autosave-request/-response,desktop-mode-editor-live-watch/-unwatch/-saved— catalogued insrc/protocol/window-messages.ts, documented indocs/bridge-protocol.md. JS surface (EDITOR_PREVIEW_*hooks, the live filter,swapReload,ephemeral) indocs/javascript-reference.md; PHP filter indocs/hooks-reference.md; events/messages indexed indocs/api-index.md.Testing
Tests_DesktopMode_WindowLinks, 31 passing; full suite 1335 green).swapReloadmechanics (instant cut, supersede, overlay contract, focus forwarder survival),ephemeralsnapshot exclusion. Full suite: 2209 passing across 234 files.🤖 Generated with Claude Code