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

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

181 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Route core's in-between / empty-block "+" through the full mini inserter.
3 * No prop seam for `__experimentalIsQuick`; click is intercepted and our own
4 * <Inserter> mounts on an invisible anchor (core's "+" unmounts on hover-end).
5 * Anchor tracks the destination block each frame via DOM writes, not React state.
6 */
7 import { Inserter } from '@wordpress/block-editor';
8 import { select as dataSelect, useSelect } from '@wordpress/data';
9 import { createPortal, useCallback, useEffect, useRef, useState } from '@wordpress/element';
10 import { registerPlugin } from '@wordpress/plugins';
11
12 import { resolveMiniInserterPosition } from '../lib/miniInserterPlacement';
13 import { clearMiniInserterTarget, setMiniInserterTarget } from '../lib/miniInserterTarget';
14
15 const PB_CPTS = [ 'wppb-rf-cpt', 'wppb-epf-cpt' ];
16
17 const IN_BETWEEN_CLASS = 'block-editor-block-list__insertion-point-inserter';
18 const EMPTY_BLOCK_CLASS = 'block-editor-block-list__empty-block-inserter';
19 const HOST_SELECTOR = `.${ IN_BETWEEN_CLASS }, .${ EMPTY_BLOCK_CLASS }`;
20
21 /**
22 * Insert destination for a "+" click (`clientId` = insert before; else append).
23 *
24 * @param {Element} host The "+" wrapper that was clicked.
25 * @return {?Object} `{ rootClientId, clientId, isAppender }` or null.
26 */
27 const resolveDestination = ( host ) => {
28 const editor = dataSelect( 'core/block-editor' );
29 if ( ! editor ) return null;
30
31 if ( host.classList.contains( EMPTY_BLOCK_CLASS ) ) {
32 const clientId = editor.getSelectedBlockClientId();
33 if ( ! clientId ) return null;
34 return { rootClientId: editor.getBlockRootClientId( clientId ) || undefined, clientId };
35 }
36
37 const point = editor.getBlockInsertionPoint();
38 if ( ! point ) return null;
39 const rootClientId = point.rootClientId || undefined;
40 const clientId = ( editor.getBlockOrder( point.rootClientId ) || [] )[ point.index ];
41 return clientId ? { rootClientId, clientId } : { rootClientId, isAppender: true };
42 };
43
44 /**
45 * Destination block position in main-document coords (iframe offset added).
46 * Used as a moving origin so the anchor tracks canvas scroll/reflow.
47 *
48 * @param {Object} destination `{ rootClientId, clientId }` from resolveDestination.
49 * @return {?Object} `{ left, top }` or null when the canvas isn't reachable.
50 */
51 const measureDestinationOrigin = ( { clientId, rootClientId } ) => {
52 const iframe = document.querySelector( 'iframe[name="editor-canvas"]' );
53 const doc = iframe ? iframe.contentDocument : document;
54 if ( ! doc ) return null;
55 const frame = iframe ? iframe.getBoundingClientRect() : { left: 0, top: 0 };
56
57 const el =
58 ( clientId && doc.querySelector( `[data-block="${ clientId }"]` ) ) ||
59 ( rootClientId && doc.querySelector( `[data-block="${ rootClientId }"]` ) ) ||
60 doc.querySelector( '.block-editor-block-list__layout.is-root-container' );
61 if ( ! el ) return null;
62
63 const rect = el.getBoundingClientRect();
64 return { left: frame.left + rect.left, top: frame.top + rect.top };
65 };
66
67 /** Invisible toggle that opens its Dropdown once on mount. */
68 const AutoOpenToggle = ( { onToggle } ) => {
69 const openedRef = useRef( false );
70 useEffect( () => {
71 if ( openedRef.current ) return;
72 openedRef.current = true;
73 onToggle();
74 // eslint-disable-next-line react-hooks/exhaustive-deps
75 }, [] );
76 return <span className="wppb-fb-point-inserter__toggle" aria-hidden="true" />;
77 };
78
79 const CanvasPointInserter = () => {
80 const postType = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostType(), [] );
81 const isOurPostType = PB_CPTS.includes( postType );
82
83 const [ target, setTarget ] = useState( null );
84 const close = useCallback( () => setTarget( null ), [] );
85 const anchorRef = useRef( null );
86
87 useEffect( () => {
88 if ( ! isOurPostType ) return undefined;
89
90 const onClickCapture = ( event ) => {
91 const el = event.target;
92 if ( ! el || typeof el.closest !== 'function' ) return;
93 const host = el.closest( HOST_SELECTOR );
94 if ( ! host ) return;
95
96 const destination = resolveDestination( host );
97 if ( ! destination ) return;
98
99 event.preventDefault();
100 event.stopPropagation();
101 event.stopImmediatePropagation();
102
103 const rect = host.getBoundingClientRect();
104 setTarget( {
105 rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
106 position: resolveMiniInserterPosition( rect ),
107 origin: measureDestinationOrigin( destination ),
108 ...destination,
109 } );
110 };
111
112 // "+" popovers live in the main document; one capture listener covers both.
113 document.addEventListener( 'click', onClickCapture, true );
114 return () => document.removeEventListener( 'click', onClickCapture, true );
115 }, [ isOurPostType ] );
116
117 // Track destination movement with DOM writes (state would re-render each frame).
118 useEffect( () => {
119 if ( ! target || ! target.origin ) return undefined;
120
121 let raf = 0;
122 let applied = null;
123 const sync = () => {
124 raf = window.requestAnimationFrame( sync );
125
126 const node = anchorRef.current;
127 const now = measureDestinationOrigin( target );
128 if ( ! node || ! now ) return;
129
130 const dx = now.left - target.origin.left;
131 const dy = now.top - target.origin.top;
132 if ( applied && applied.dx === dx && applied.dy === dy ) return;
133 applied = { dx, dy };
134
135 node.style.left = `${ target.rect.left + dx }px`;
136 node.style.top = `${ target.rect.top + dy }px`;
137 };
138
139 raf = window.requestAnimationFrame( sync );
140 return () => window.cancelAnimationFrame( raf );
141 }, [ target ] );
142
143 useEffect( () => {
144 if ( ! target ) return undefined;
145 setMiniInserterTarget( target );
146 return () => clearMiniInserterTarget();
147 }, [ target ] );
148
149 if ( ! isOurPostType || ! target ) return null;
150
151 return createPortal(
152 <div
153 className="wppb-fb-point-inserter"
154 ref={ anchorRef }
155 style={ {
156 left: `${ target.rect.left }px`,
157 top: `${ target.rect.top }px`,
158 width: `${ target.rect.width }px`,
159 height: `${ target.rect.height }px`,
160 } }
161 >
162 <Inserter
163 rootClientId={ target.rootClientId }
164 clientId={ target.clientId }
165 isAppender={ !! target.isAppender }
166 position={ target.position }
167 onToggle={ ( isOpen ) => {
168 if ( ! isOpen ) close();
169 } }
170 onSelectOrClose={ close }
171 renderToggle={ ( { onToggle } ) => <AutoOpenToggle onToggle={ onToggle } /> }
172 />
173 </div>,
174 document.body
175 );
176 };
177
178 registerPlugin( 'wppb-fb-canvas-point-inserter', { render: CanvasPointInserter } );
179
180 export default CanvasPointInserter;
181