PluginProbe
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor / 4.0.3
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor v4.0.3
4.0.3 4.0.2 4.0.1 4.0.0 3.16.6 3.16.5 3.16.4 3.16.3 3.16.2 3.16.1 3.16.0 3.15.9 3.9.9 3.9.5 3.9.6 3.9.7 3.9.8 1.1.7 1.1.8 1.1.9 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 341 releases
profile-builder / form-builder / blocks / src / components / FormPreviewButton.js

FormPreviewButton.js in User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor 4.0.3, at form-builder/blocks/src/components/FormPreviewButton.js

151 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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