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 / RootCanvasAppender.js

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

113 lines 4.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Replace core's root appender with AddFieldButton (mini inserter, not quick inserter).
3 * Own host node: core's root appender hides while a block is selected.
4 * `sync()` keeps our node last when Gutenberg appends after it.
5 */
6 import { useSelect } from '@wordpress/data';
7 import { createPortal, useCallback, useEffect, useRef, useState } from '@wordpress/element';
8 import { __ } from '@wordpress/i18n';
9 import { registerPlugin } from '@wordpress/plugins';
10
11 import AddFieldButton from './AddFieldButton';
12
13 const PB_CPTS = [ 'wppb-rf-cpt', 'wppb-epf-cpt' ];
14
15 // Root list only — a nested appender (Columns / Repeater) renders our
16 // AddFieldButton from the block's own edit() already.
17 const ROOT_LIST_SELECTOR = '.block-editor-block-list__layout.is-root-container';
18 const HOST_CLASS = 'wppb-fb-root-appender';
19
20 const RootCanvasAppender = () => {
21 const postType = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostType(), [] );
22 const isOurPostType = PB_CPTS.includes( postType );
23
24 const [ target, setTarget ] = useState( null );
25 // Ref in lockstep with `target` so the observer effect never lists it as a
26 // dependency (that would tear down and rebuild the observers on every sync).
27 const targetRef = useRef( null );
28 const applyTarget = useCallback( ( next ) => {
29 if ( targetRef.current === next ) return;
30 targetRef.current = next;
31 setTarget( next );
32 }, [] );
33
34 useEffect( () => {
35 if ( ! isOurPostType ) return undefined;
36
37 let frame = 0;
38 let canvasDoc = null;
39 let canvasObserver = null;
40
41 const schedule = () => {
42 if ( frame ) return;
43 frame = window.requestAnimationFrame( () => {
44 frame = 0;
45 sync();
46 } );
47 };
48
49 const sync = () => {
50 const iframe = document.querySelector( 'iframe[name="editor-canvas"]' );
51 // `document` fallback covers a non-iframed canvas (device preview /
52 // older Gutenberg): the block list then lives in the main document.
53 const doc = iframe ? iframe.contentDocument : document;
54
55 // Re-point the inner observer whenever the canvas document changes
56 // (the iframe is recreated on device-preview switches).
57 if ( doc !== canvasDoc ) {
58 if ( canvasObserver ) canvasObserver.disconnect();
59 canvasDoc = doc;
60 canvasObserver = null;
61 if ( doc && doc.body ) {
62 canvasObserver = new MutationObserver( schedule );
63 canvasObserver.observe( doc.body, { childList: true, subtree: true } );
64 }
65 }
66
67 const list = doc ? doc.querySelector( ROOT_LIST_SELECTOR ) : null;
68 if ( ! list ) {
69 applyTarget( null );
70 return;
71 }
72
73 // Idempotent: re-running on every canvas mutation must not mutate the
74 // DOM again, or the observer would feed itself. Hence the two
75 // guards — create only when missing, move only when displaced by a
76 // block React appended after it.
77 let host = list.querySelector( `:scope > .${ HOST_CLASS }` );
78 if ( ! host ) {
79 host = doc.createElement( 'div' );
80 host.className = HOST_CLASS;
81 list.appendChild( host );
82 } else if ( list.lastElementChild !== host ) {
83 list.appendChild( host );
84 }
85 applyTarget( host );
86 };
87
88 sync();
89 // Outer observer: catches the canvas iframe being (re)created.
90 const outerObserver = new MutationObserver( schedule );
91 outerObserver.observe( document.body, { childList: true, subtree: true } );
92
93 return () => {
94 outerObserver.disconnect();
95 if ( canvasObserver ) canvasObserver.disconnect();
96 if ( frame ) window.cancelAnimationFrame( frame );
97 };
98 }, [ isOurPostType, applyTarget ] );
99
100 if ( ! isOurPostType || ! target ) return null;
101
102 // No rootClientId → insert at the end of the root list, matching what core's
103 // root appender did.
104 return createPortal(
105 <AddFieldButton label={ __( 'Add field', 'profile-builder' ) } />,
106 target
107 );
108 };
109
110 registerPlugin( 'wppb-fb-root-canvas-appender', { render: RootCanvasAppender } );
111
112 export default RootCanvasAppender;
113