| 1 |
/** |
| 2 |
* Header "Save and preview" — CPTs are non-public so core has no Preview. |
| 3 |
* Saves first, then opens the front-end preview URL (blank tab opened on gesture). |
| 4 |
*/ |
| 5 |
|
| 6 |
import { __ } from '@wordpress/i18n'; |
| 7 |
import { registerPlugin } from '@wordpress/plugins'; |
| 8 |
import { Button } from '@wordpress/components'; |
| 9 |
import { createPortal, useEffect, useState, useCallback } from '@wordpress/element'; |
| 10 |
import { useSelect, useDispatch, select as dataSelect } from '@wordpress/data'; |
| 11 |
import { addQueryArgs } from '@wordpress/url'; |
| 12 |
|
| 13 |
const SLOT_ATTR = 'data-wppb-fb-preview-slot'; |
| 14 |
// Newer @wordpress/editor header uses `.editor-header__settings`; classic |
| 15 |
// edit-post wraps it as `.edit-post-header__settings`. Match either. |
| 16 |
const HEADER_SETTINGS_SELECTOR = '.edit-post-header__settings, .editor-header__settings'; |
| 17 |
|
| 18 |
/** |
| 19 |
* Returns the external-link glyph used on the button (inline so we don't take a |
| 20 |
* dependency on @wordpress/icons / the wp-icons script handle). |
| 21 |
*/ |
| 22 |
const ExternalIcon = () => ( |
| 23 |
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"> |
| 24 |
<path |
| 25 |
style={ { fill: 'currentColor' } } |
| 26 |
d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.4l-6.6 6.6 1 1 6.7-6.6v3.4h1.5V3h-6z" |
| 27 |
/> |
| 28 |
</svg> |
| 29 |
); |
| 30 |
|
| 31 |
/** |
| 32 |
* Finds the header settings region and ensures our portal slot is its first |
| 33 |
* child (so the button renders to the left of the Save/Update button). |
| 34 |
* |
| 35 |
* @return {HTMLElement|null} The slot element, or null if the header isn't mounted yet. |
| 36 |
*/ |
| 37 |
const ensureSlot = () => { |
| 38 |
const settings = document.querySelector( HEADER_SETTINGS_SELECTOR ); |
| 39 |
if ( ! settings ) return null; |
| 40 |
|
| 41 |
let slot = settings.querySelector( `[${ SLOT_ATTR }]` ); |
| 42 |
if ( ! slot ) { |
| 43 |
slot = document.createElement( 'div' ); |
| 44 |
slot.setAttribute( SLOT_ATTR, '' ); |
| 45 |
slot.style.display = 'flex'; |
| 46 |
slot.style.alignItems = 'center'; |
| 47 |
} |
| 48 |
// Keep it as the first child even if the header re-renders and reorders. |
| 49 |
if ( settings.firstChild !== slot ) { |
| 50 |
settings.insertBefore( slot, settings.firstChild ); |
| 51 |
} |
| 52 |
return slot; |
| 53 |
}; |
| 54 |
|
| 55 |
const FormPreviewButton = () => { |
| 56 |
const { postType, postId, isSaving, isAutosaving } = useSelect( ( select ) => { |
| 57 |
const editor = select( 'core/editor' ); |
| 58 |
return { |
| 59 |
postType: editor.getCurrentPostType(), |
| 60 |
postId: editor.getCurrentPostId(), |
| 61 |
isSaving: editor.isSavingPost(), |
| 62 |
isAutosaving: editor.isAutosavingPost(), |
| 63 |
}; |
| 64 |
}, [] ); |
| 65 |
|
| 66 |
const { savePost } = useDispatch( 'core/editor' ); |
| 67 |
const [ slot, setSlot ] = useState( null ); |
| 68 |
|
| 69 |
const isOurPostType = [ 'wppb-rf-cpt', 'wppb-epf-cpt' ].includes( postType ); |
| 70 |
|
| 71 |
useEffect( () => { |
| 72 |
if ( ! isOurPostType ) return undefined; |
| 73 |
|
| 74 |
const sync = () => { |
| 75 |
const found = ensureSlot(); |
| 76 |
setSlot( ( prev ) => ( prev === found ? prev : found ) ); |
| 77 |
}; |
| 78 |
|
| 79 |
// Coalesce a burst of Gutenberg DOM mutations into a single sync per |
| 80 |
// animation frame. Undebounced, the observer invoked sync on every |
| 81 |
// mutation batch on the typing→paint path (INP). |
| 82 |
let frame = 0; |
| 83 |
const scheduleSync = () => { |
| 84 |
if ( frame ) return; |
| 85 |
frame = window.requestAnimationFrame( () => { |
| 86 |
frame = 0; |
| 87 |
sync(); |
| 88 |
} ); |
| 89 |
}; |
| 90 |
|
| 91 |
sync(); |
| 92 |
const observer = new MutationObserver( scheduleSync ); |
| 93 |
// Scope to the editor skeleton (the header lives inside it) instead of |
| 94 |
// the whole document body, so admin-chrome mutations don't wake the |
| 95 |
// observer. Fall back to body if the skeleton class ever moves. |
| 96 |
const root = document.querySelector( '.interface-interface-skeleton' ) || document.body; |
| 97 |
observer.observe( root, { childList: true, subtree: true } ); |
| 98 |
return () => { |
| 99 |
observer.disconnect(); |
| 100 |
if ( frame ) window.cancelAnimationFrame( frame ); |
| 101 |
}; |
| 102 |
}, [ isOurPostType ] ); |
| 103 |
|
| 104 |
const onPreview = useCallback( () => { |
| 105 |
// Open the tab synchronously within the click gesture to dodge pop-up |
| 106 |
// blockers; redirect it once the save resolves. |
| 107 |
const win = window.open( 'about:blank', '_blank' ); |
| 108 |
|
| 109 |
Promise.resolve( savePost() ) |
| 110 |
.then( () => { |
| 111 |
const bridge = ( window.wppbFb && window.wppbFb.preview ) || {}; |
| 112 |
const id = dataSelect( 'core/editor' ).getCurrentPostId(); |
| 113 |
if ( ! bridge.home || ! id ) { |
| 114 |
if ( win ) win.close(); |
| 115 |
return; |
| 116 |
} |
| 117 |
const url = addQueryArgs( bridge.home, { |
| 118 |
wppb_fb_preview: id, |
| 119 |
_wppbnonce: bridge.nonce, |
| 120 |
} ); |
| 121 |
if ( win ) { |
| 122 |
win.location = url; |
| 123 |
} else { |
| 124 |
window.open( url, '_blank' ); |
| 125 |
} |
| 126 |
} ) |
| 127 |
.catch( () => { |
| 128 |
if ( win ) win.close(); |
| 129 |
} ); |
| 130 |
}, [ savePost ] ); |
| 131 |
|
| 132 |
if ( ! isOurPostType || ! slot ) return null; |
| 133 |
|
| 134 |
return createPortal( |
| 135 |
<Button |
| 136 |
variant="secondary" |
| 137 |
className="wppb-fb-preview-button" |
| 138 |
icon={ <ExternalIcon /> } |
| 139 |
iconPosition="right" |
| 140 |
onClick={ onPreview } |
| 141 |
disabled={ isSaving && ! isAutosaving } |
| 142 |
aria-haspopup="dialog" |
| 143 |
> |
| 144 |
{ __( 'Save and preview', 'profile-builder' ) } |
| 145 |
</Button>, |
| 146 |
slot |
| 147 |
); |
| 148 |
}; |
| 149 |
|
| 150 |
registerPlugin( 'wppb-fb-form-preview-button', { render: FormPreviewButton } ); |
| 151 |
|