/** * Header "Save and preview" — CPTs are non-public so core has no Preview. * Saves first, then opens the front-end preview URL (blank tab opened on gesture). */ import { __ } from '@wordpress/i18n'; import { registerPlugin } from '@wordpress/plugins'; import { Button } from '@wordpress/components'; import { createPortal, useEffect, useState, useCallback } from '@wordpress/element'; import { useSelect, useDispatch, select as dataSelect } from '@wordpress/data'; import { addQueryArgs } from '@wordpress/url'; const SLOT_ATTR = 'data-wppb-fb-preview-slot'; // Newer @wordpress/editor header uses `.editor-header__settings`; classic // edit-post wraps it as `.edit-post-header__settings`. Match either. const HEADER_SETTINGS_SELECTOR = '.edit-post-header__settings, .editor-header__settings'; /** * Returns the external-link glyph used on the button (inline so we don't take a * dependency on @wordpress/icons / the wp-icons script handle). */ const ExternalIcon = () => ( ); /** * Finds the header settings region and ensures our portal slot is its first * child (so the button renders to the left of the Save/Update button). * * @return {HTMLElement|null} The slot element, or null if the header isn't mounted yet. */ const ensureSlot = () => { const settings = document.querySelector( HEADER_SETTINGS_SELECTOR ); if ( ! settings ) return null; let slot = settings.querySelector( `[${ SLOT_ATTR }]` ); if ( ! slot ) { slot = document.createElement( 'div' ); slot.setAttribute( SLOT_ATTR, '' ); slot.style.display = 'flex'; slot.style.alignItems = 'center'; } // Keep it as the first child even if the header re-renders and reorders. if ( settings.firstChild !== slot ) { settings.insertBefore( slot, settings.firstChild ); } return slot; }; const FormPreviewButton = () => { const { postType, postId, isSaving, isAutosaving } = useSelect( ( select ) => { const editor = select( 'core/editor' ); return { postType: editor.getCurrentPostType(), postId: editor.getCurrentPostId(), isSaving: editor.isSavingPost(), isAutosaving: editor.isAutosavingPost(), }; }, [] ); const { savePost } = useDispatch( 'core/editor' ); const [ slot, setSlot ] = useState( null ); const isOurPostType = [ 'wppb-rf-cpt', 'wppb-epf-cpt' ].includes( postType ); useEffect( () => { if ( ! isOurPostType ) return undefined; const sync = () => { const found = ensureSlot(); setSlot( ( prev ) => ( prev === found ? prev : found ) ); }; // Coalesce a burst of Gutenberg DOM mutations into a single sync per // animation frame. Undebounced, the observer invoked sync on every // mutation batch on the typing→paint path (INP). let frame = 0; const scheduleSync = () => { if ( frame ) return; frame = window.requestAnimationFrame( () => { frame = 0; sync(); } ); }; sync(); const observer = new MutationObserver( scheduleSync ); // Scope to the editor skeleton (the header lives inside it) instead of // the whole document body, so admin-chrome mutations don't wake the // observer. Fall back to body if the skeleton class ever moves. const root = document.querySelector( '.interface-interface-skeleton' ) || document.body; observer.observe( root, { childList: true, subtree: true } ); return () => { observer.disconnect(); if ( frame ) window.cancelAnimationFrame( frame ); }; }, [ isOurPostType ] ); const onPreview = useCallback( () => { // Open the tab synchronously within the click gesture to dodge pop-up // blockers; redirect it once the save resolves. const win = window.open( 'about:blank', '_blank' ); Promise.resolve( savePost() ) .then( () => { const bridge = ( window.wppbFb && window.wppbFb.preview ) || {}; const id = dataSelect( 'core/editor' ).getCurrentPostId(); if ( ! bridge.home || ! id ) { if ( win ) win.close(); return; } const url = addQueryArgs( bridge.home, { wppb_fb_preview: id, _wppbnonce: bridge.nonce, } ); if ( win ) { win.location = url; } else { window.open( url, '_blank' ); } } ) .catch( () => { if ( win ) win.close(); } ); }, [ savePost ] ); if ( ! isOurPostType || ! slot ) return null; return createPortal( , slot ); }; registerPlugin( 'wppb-fb-form-preview-button', { render: FormPreviewButton } );