PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
← All changes | inc/blocks/register.php +423 -92 1.3.0 → 1.6.1 View file →
@@ -6,12 +6,15 @@
6 6 */
7 7
8 8 namespace SureDonation\Inc\Blocks;
9 9
10 +use SureDonation\Inc\Assets\Register as Assets_Register;
11 +use SureDonation\Inc\Helper;
10 12 use SureDonation\Inc\Payments\Offline\Offline_Helper;
11 13 use SureDonation\Inc\Payments\PayPal\PayPal_Helper;
12 14 use SureDonation\Inc\Payments\Payment_Helper;
13 15 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
16 +use SureDonation\Inc\Post_Types\Donation_Form;
14 17 use SureDonation\Inc\Traits\Get_Instance;
15 18
16 19 // Exit if accessed directly.
17 20 if ( ! defined( 'ABSPATH' ) ) {
@@ -26,8 +29,20 @@
26 29 class Register {
27 30 use Get_Instance;
28 31
29 32 /**
33 + * Memoized block-inserter preview map (block key => image URL).
34 + *
35 + * Built once per request in get_field_preview_images() so every localized
36 + * copy is identical and the `suredonation_block_preview_images` filter runs
37 + * once.
38 + *
39 + * @var array<string,string>|null
40 + * @since 1.5.0
41 + */
42 + private $preview_images = null;
43 +
44 + /**
30 45 * Constructor.
31 46 *
32 47 * @since 0.0.1
33 48 */
@@ -35,14 +50,128 @@
35 50 add_action( 'init', [ $this, 'register_embed_block_script' ], 5 );
36 51 add_action( 'init', [ $this, 'register_blocks' ] );
37 52 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
38 53 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_campaign_editor_assets' ] );
54 + add_action( 'enqueue_block_editor_assets', [ $this, 'localize_embed_preview_images' ] );
39 55 add_filter( 'block_categories_all', [ $this, 'register_block_category' ], 10, 2 );
40 56 add_filter( 'block_editor_settings_all', [ $this, 'add_campaign_iframe_styles' ], 10, 2 );
57 + add_filter( 'block_editor_settings_all', [ $this, 'add_donation_form_iframe_styles' ], 10, 2 );
41 58 add_filter( 'block_editor_settings_all', [ $this, 'add_phone_iframe_styles' ], 10, 2 );
59 + add_action( 'enqueue_block_assets', [ $this, 'enqueue_preview_field_scripts' ] );
42 60 }
43 61
44 62 /**
63 + * Load the dropdown and phone field libraries into the block editor canvas.
64 + *
65 + * The donation form embed block previews the real form through the block's PHP
66 + * render_callback (ServerSideRender), which returns markup only — a REST render
67 + * emits no wp_footer(), so nothing the render callback enqueues ever reaches the
68 + * page. Without their libraries the dropdown stays an unstyled native <select>
69 + * and the phone field renders with no country flag or dial code.
70 + *
71 + * `enqueue_block_assets` is the hook WordPress replays when it collects assets
72 + * for the iframed canvas: _wp_get_iframed_editor_assets() fires it and returns
73 + * both the printed styles AND scripts, which the canvas injects into its own
74 + * document. It is explicitly the hook for front-end assets that need to run
75 + * against editor content.
76 + *
77 + * Only the two field libraries are loaded. The payment gateways are excluded on
78 + * purpose: mounting Stripe Elements or the PayPal SDK would pull third-party
79 + * scripts into wp-admin on every editor load and open live gateway connections
80 + * for a preview the author cannot interact with. Those keep their static
81 + * placeholders (see _editor-preview.scss).
82 + *
83 + * Both initialisers are safe here — each is ready-state aware, guards against
84 + * double-initialising via a dataset flag, and exposes a re-init hook the editor
85 + * calls once ServerSideRender has injected the markup (see the block's edit
86 + * component).
87 + *
88 + * @return void
89 + * @since 1.4.0
90 + */
91 + public function enqueue_preview_field_scripts() {
92 + // Front end already enqueues these per-block from the field render; this is
93 + // the editor-only path.
94 + if ( ! is_admin() ) {
95 + return;
96 + }
97 +
98 + // The form builder mounts its own React controls for these fields.
99 + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
100 + if ( $screen && 'suredonation_form' === $screen->post_type ) {
101 + return;
102 + }
103 +
104 + // The handles are registered on wp_enqueue_scripts, which never fires in
105 + // admin. Registration is side-effect free (wp_register_* only), so reuse it
106 + // rather than duplicating the definitions.
107 + Assets_Register::get_instance()->register_frontend_assets();
108 +
109 + // Each script handle already depends on its vendor library, so enqueuing the
110 + // initialiser pulls the library in, in the right order.
111 + wp_enqueue_style( 'suredonation-tom-select' );
112 + wp_enqueue_script( 'suredonation-dropdown' );
113 + wp_enqueue_style( 'suredonation-intl-tel-input' );
114 + wp_enqueue_script( 'suredonation-phone' );
115 +
116 + // The payment bundle mounts Stripe Elements and the PayPal buttons. Both are
117 + // client-only on mount: Stripe's elements()/mount() builds an iframe, and
118 + // PayPal's createOrder does not run until the button is clicked, so nothing
119 + // here reaches the server or creates a PaymentIntent.
120 + wp_enqueue_script( 'suredonation-form-frontend' );
121 +
122 + $this->enqueue_preview_gateway_assets();
123 + }
124 +
125 + /**
126 + * Enqueue gateway assets for each donation form embedded in the current post.
127 + *
128 + * The PayPal SDK is enqueued by gateway code hooked to
129 + * `suredonation_enqueue_form_frontend_scripts`, which the render callback fires
130 + * with the form's id and content — that hook is how a gateway decides whether it
131 + * is even used by the form. A REST render throws the enqueue away, so fire it
132 + * here instead, for the forms this post actually embeds.
133 + *
134 + * Resolving the forms (rather than loading every gateway unconditionally) keeps
135 + * the gateway's own `form_has_paypal()` style gating intact, so a post with no
136 + * PayPal-enabled form does not pull the SDK into wp-admin.
137 + *
138 + * Also localises the payment settings the bundle reads, per form.
139 + *
140 + * @return void
141 + * @since 1.4.0
142 + */
143 + private function enqueue_preview_gateway_assets() {
144 + $post = get_post();
145 +
146 + if ( ! $post instanceof \WP_Post || ! has_block( 'suredonation/donation-form', $post ) ) {
147 + return;
148 + }
149 +
150 + foreach ( parse_blocks( $post->post_content ) as $block ) {
151 + if ( 'suredonation/donation-form' !== ( $block['blockName'] ?? '' ) ) {
152 + continue;
153 + }
154 +
155 + $form_id = absint( $block['attrs']['formId'] ?? 0 );
156 + $form = $form_id ? get_post( $form_id ) : null;
157 +
158 + if ( ! $form instanceof \WP_Post || Donation_Form::POST_TYPE !== $form->post_type ) {
159 + continue;
160 + }
161 +
162 + /** This action is documented in inc/blocks/donation-form/block.php */
163 + do_action( 'suredonation_enqueue_form_frontend_scripts', $form_id, $form->post_content );
164 +
165 + wp_localize_script(
166 + 'suredonation-form-frontend',
167 + 'suredonationPayment',
168 + Helper::get_form_payment_settings( $form_id )
169 + );
170 + }
171 + }
172 +
173 + /**
45 174 * Register the donation form embed block editor script.
46 175 *
47 176 * Runs before register_blocks() so the handle exists when block.json is read.
48 177 * Not gated by post type — the embed block should work on all post types.
@@ -75,8 +204,16 @@
75 204 'suredonationCampaignBlocks',
76 205 $this->get_campaign_blocks_data()
77 206 );
78 207
208 + // Note: the inserter preview map is localized on this handle later, in
209 + // localize_embed_preview_images() on enqueue_block_editor_assets, not here
210 + // on init:5 — see that method for why.
211 +
212 + // Load JS translations for the embed editor bundle so the "Field preview"
213 + // string and the block's own UI strings can be translated.
214 + wp_set_script_translations( 'suredonation-donation-form-editor', 'suredonation' );
215 +
79 216 wp_register_style(
80 217 'suredonation-donation-form-editor',
81 218 SUREDONATION_URL . 'assets/build/blocks/donation-form/editor.css',
82 219 [],
@@ -84,8 +221,34 @@
84 221 );
85 222 }
86 223
87 224 /**
225 + * Localize the inserter preview map onto the donation-form embed script.
226 + *
227 + * The embed block is insertable on any post type, so this runs on every
228 + * editor screen (no post-type guard, unlike enqueue_editor_assets()).
229 + *
230 + * It runs on `enqueue_block_editor_assets` rather than at `init` — where the
231 + * handle is registered — deliberately: get_field_preview_images() memoizes on
232 + * its first call, so the first localizer to run fixes the map for the request.
233 + * Building it here (after `init`) lets consumers of the
234 + * `suredonation_block_preview_images` filter register on the default `init`
235 + * priority — the obvious thing to do, and what SureDonation Pro does on
236 + * `plugins_loaded` — and still be captured. The handle is registered by
237 + * register_embed_block_script() on init:5, so wp_localize_script attaches here.
238 + *
239 + * @return void
240 + * @since 1.5.0
241 + */
242 + public function localize_embed_preview_images() {
243 + wp_localize_script(
244 + 'suredonation-donation-form-editor',
245 + 'suredonation_fields_preview',
246 + $this->get_field_preview_images()
247 + );
248 + }
249 +
250 + /**
88 251 * Data localized for the block editor placeholders (logo).
89 252 *
90 253 * Shared by the donation form embed block and the campaign display blocks,
91 254 * both of which expose it on the `suredonationCampaignBlocks` JS global.
@@ -195,45 +358,41 @@
195 358 'suredonationCampaignBlocks',
196 359 $this->get_campaign_blocks_data()
197 360 );
198 361
199 - // Style the server-side-rendered block previews in the editor.
200 - $style_file = SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css';
201 - $style_version = file_exists( $style_file )
202 - ? (string) filemtime( $style_file )
203 - : SUREDONATION_VER;
204 -
205 - wp_enqueue_style(
362 + // Block-inserter preview images (see withFieldPreview / get_field_preview_images()).
363 + wp_localize_script(
206 364 'suredonation-campaign-blocks',
207 - SUREDONATION_URL . 'assets/build/blocks/campaign/style-style.css',
208 - [],
209 - $style_version
365 + 'suredonation_fields_preview',
366 + $this->get_field_preview_images()
210 367 );
368 +
369 + // No stylesheet is enqueued here. The campaign blocks only ever render in
370 + // the canvas, and add_campaign_iframe_styles() inlines this exact CSS into
371 + // it through the editor settings, where it carries no element id and so is
372 + // invisible to the compatibility pass. Enqueueing it on the outer frame as
373 + // well put its one `.editor-styles-wrapper` rule in the admin document,
374 + // which is all it takes for WordPress to clone the whole sheet into the
375 + // canvas and log "suredonation-campaign-blocks-css was added to the iframe
376 + // incorrectly" on every campaign editor load.
211 377 }
212 378
213 379 /**
214 - * Inject the campaign block styles into the editor canvas iframe.
380 + * Append an inline stylesheet to the block-editor iframe settings.
215 381 *
216 382 * Styles enqueued via enqueue_block_editor_assets load in the editor's outer
217 - * frame only; the block canvas is iframed, so the server-side-rendered campaign
218 - * block previews would otherwise render unstyled. Adding the CSS to the editor
219 - * settings makes WordPress inject it inside the iframe, matching the frontend.
383 + * frame only; the block canvas is iframed, so server-side-rendered previews
384 + * would otherwise render unstyled. Adding CSS here makes WordPress inject it
385 + * inside the iframe, matching the frontend.
220 386 *
221 - * @param array<string, mixed> $settings Block editor settings.
222 - * @param \WP_Block_Editor_Context $context Block editor context.
223 - * @return array<string, mixed> Modified settings.
224 - * @since 1.0.0
387 + * @param array<string, mixed> $settings Block editor settings (by reference).
388 + * @param string $css Stylesheet contents to inline.
389 + * @return void
390 + * @since 1.4.0
225 391 */
226 - public function add_campaign_iframe_styles( $settings, $context ) {
227 - // Inject wherever the campaign blocks can be used (everywhere except the
228 - // donation form editor), so their editor previews match the frontend.
229 - if ( ! isset( $context->post ) || 'suredonation_form' === $context->post->post_type ) {
230 - return $settings;
231 - }
232 -
233 - $css = $this->get_campaign_iframe_css();
392 + private function append_iframe_style( &$settings, $css ) {
234 393 if ( '' === $css ) {
235 - return $settings;
394 + return;
236 395 }
237 396
238 397 if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
239 398 $settings['styles'] = [];
@@ -239,112 +398,169 @@
239 398 $settings['styles'] = [];
240 399 }
241 400
242 401 $settings['styles'][] = [ 'css' => $css ];
243 -
244 - return $settings;
245 402 }
246 403
247 404 /**
248 - * Read the built campaign stylesheet, cached per request by file mtime so
249 - * the filter (which can run more than once per load) reads from disk at most
250 - * once until the asset changes.
405 + * Read a stylesheet for iframe inlining, cached per request by file mtime so
406 + * the filter (which can run more than once per load) reads each file from disk
407 + * at most once until it changes.
251 408 *
409 + * @param string $style_file Absolute path to the stylesheet.
410 + * @param array<string, string> $replacements Optional search => replace pairs
411 + * applied to the CSS, e.g. to rewrite
412 + * relative asset URLs to absolute
413 + * plugin URLs so they resolve inside
414 + * the iframe.
252 415 * @return string The stylesheet contents, or '' when unavailable.
253 - * @since 1.0.0
416 + * @since 1.4.0
254 417 */
255 - private function get_campaign_iframe_css() {
256 - static $cached_css = null;
257 - static $cached_mtime = null;
418 + private function read_iframe_css( $style_file, $replacements = [] ) {
419 + // Keyed by path so the aggregate + vendor stylesheets do not evict each
420 + // other's cache entry.
421 + static $cache = [];
258 422
259 - $style_file = SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css';
260 423 if ( ! file_exists( $style_file ) ) {
261 424 return '';
262 425 }
263 426
264 427 $mtime = filemtime( $style_file );
265 - if ( null === $cached_css || $cached_mtime !== $mtime ) {
266 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading the plugin's own built stylesheet to inline into the editor iframe.
267 - $css = file_get_contents( $style_file );
268 - $cached_css = false === $css ? '' : $css;
269 - $cached_mtime = $mtime;
428 + if ( ! isset( $cache[ $style_file ] ) || $cache[ $style_file ]['mtime'] !== $mtime ) {
429 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading the plugin's own/vendored stylesheet to inline into the editor iframe.
430 + $css = file_get_contents( $style_file );
431 + $css = false === $css ? '' : $css;
432 +
433 + if ( '' !== $css && ! empty( $replacements ) ) {
434 + $css = str_replace( array_keys( $replacements ), array_values( $replacements ), $css );
435 + }
436 +
437 + $cache[ $style_file ] = [
438 + 'mtime' => $mtime,
439 + 'css' => $css,
440 + ];
270 441 }
271 442
272 - return $cached_css;
443 + return $cache[ $style_file ]['css'];
273 444 }
274 445
275 446 /**
276 - * Inject the intl-tel-input stylesheet into the editor canvas iframe.
447 + * Inject the campaign block styles into the editor canvas iframe.
277 448 *
278 - * The phone block renders the real intl-tel-input control in the editor so
279 - * its preview (flag + dial code) matches the front end. The library's CSS is
280 - * needed inside the canvas, which is iframed, so we add it to the editor
281 - * settings (the same mechanism used for the campaign block previews) rather
282 - * than enqueuing it in the outer frame where the iframe can't reach it.
283 - * Gated to the donation form editor, where the phone block lives.
284 - *
285 449 * @param array<string, mixed> $settings Block editor settings.
286 450 * @param \WP_Block_Editor_Context $context Block editor context.
287 451 * @return array<string, mixed> Modified settings.
288 - * @since 1.1.1
452 + * @since 1.0.0
289 453 */
290 - public function add_phone_iframe_styles( $settings, $context ) {
291 - // Only the donation form editor uses the field blocks (incl. phone).
292 - if ( ! isset( $context->post ) || 'suredonation_form' !== $context->post->post_type ) {
454 + public function add_campaign_iframe_styles( $settings, $context ) {
455 + // Inject wherever the campaign blocks can be used, which is every editor
456 + // except the donation form builder — matching the blocks' own registration
457 + // and add_donation_form_iframe_styles() below.
458 + //
459 + // The post is checked only when there is one. Requiring it excluded exactly
460 + // the contexts that have none: the widget editor never sets it
461 + // (wp-admin/widgets-form-blocks.php), and the Site Editor only sets it for a
462 + // numeric postId, so editing any template or template part
463 + // (postId=theme//slug) has none either. The campaign blocks are insertable in
464 + // both, and this stylesheet also carries the placeholder rules the donation
465 + // form embed block reuses, so bailing there left both unstyled.
466 + if ( isset( $context->post ) && 'suredonation_form' === $context->post->post_type ) {
293 467 return $settings;
294 468 }
295 469
296 - $css = $this->get_phone_iframe_css();
297 - if ( '' === $css ) {
470 + $this->append_iframe_style(
471 + $settings,
472 + $this->read_iframe_css( SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css' )
473 + );
474 +
475 + return $settings;
476 + }
477 +
478 + /**
479 + * Inject the donation form styles into the editor canvas iframe.
480 + *
481 + * The donation form embed block previews the real form via ServerSideRender,
482 + * and the canvas is iframed, so styles enqueued on the outer frame never reach
483 + * it. Three stylesheets are inlined:
484 + *
485 + * - the aggregate donation-form CSS, which also carries every field block's
486 + * styles and the editor-preview reconciliation (see _editor-preview.scss);
487 + * - the tom-select vendor CSS, which paints both the dropdown field's
488 + * server-rendered `.ts-wrapper` placeholder and the real control tom-select
489 + * mounts over it; and
490 + * - the intl-tel-input vendor CSS, for the `.iti` wrapper that library builds
491 + * around the phone input. Its flag sprites are referenced relative to the
492 + * stylesheet, so those paths are rewritten to absolute plugin URLs — inlining
493 + * drops the base they resolve against.
494 + *
495 + * Both libraries genuinely run in the canvas: they are enqueued on
496 + * enqueue_block_assets (see enqueue_preview_field_scripts) and re-initialised by
497 + * the block's edit component once ServerSideRender has injected the markup. The
498 + * payment gateways are not, so their placeholders stay static.
499 + *
500 + * @param array<string, mixed> $settings Block editor settings.
501 + * @param \WP_Block_Editor_Context $context Block editor context.
502 + * @return array<string, mixed> Modified settings.
503 + * @since 1.4.0
504 + */
505 + public function add_donation_form_iframe_styles( $settings, $context ) {
506 + // Inject wherever the embed block can be used, including the Site Editor and
507 + // widget contexts where $context->post is unset. Only the donation form
508 + // builder is excluded; it styles its own field blocks separately (see
509 + // add_phone_iframe_styles + form-editor).
510 + if ( isset( $context->post ) && 'suredonation_form' === $context->post->post_type ) {
298 511 return $settings;
299 512 }
300 513
301 - if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
302 - $settings['styles'] = [];
303 - }
514 + $this->append_iframe_style(
515 + $settings,
516 + $this->read_iframe_css( SUREDONATION_DIR . 'assets/build/blocks/donation-form/style-style.css' )
517 + );
518 + $this->append_iframe_style(
519 + $settings,
520 + $this->read_iframe_css( SUREDONATION_DIR . 'assets/css/vendor/tom-select.css' )
521 + );
522 + $this->append_iframe_style(
523 + $settings,
524 + $this->read_iframe_css(
525 + SUREDONATION_DIR . 'assets/css/vendor/intl/intlTelInput.min.css',
526 + [ '../intl/img/' => SUREDONATION_URL . 'assets/css/vendor/intl/img/' ]
527 + )
528 + );
304 529
305 - $settings['styles'][] = [ 'css' => $css ];
306 -
307 530 return $settings;
308 531 }
309 532
310 533 /**
311 - * Read the vendored intl-tel-input stylesheet, cached per request by file
312 - * mtime so the filter (which can run more than once per load) reads from disk
313 - * at most once until the asset changes.
534 + * Inject the intl-tel-input stylesheet into the editor canvas iframe.
314 535 *
315 - * @return string The stylesheet contents, or '' when unavailable.
536 + * The phone block renders the real intl-tel-input control in the form builder
537 + * editor so its preview (flag + dial code) matches the front end. The canvas
538 + * is iframed, so the library CSS is added to the editor settings rather than
539 + * enqueued on the outer frame. Gated to the donation form editor, where the
540 + * phone block lives. (The relative flag sprite paths are rewritten to absolute
541 + * plugin URLs so they resolve inside the iframe.)
542 + *
543 + * @param array<string, mixed> $settings Block editor settings.
544 + * @param \WP_Block_Editor_Context $context Block editor context.
545 + * @return array<string, mixed> Modified settings.
316 546 * @since 1.1.1
317 547 */
318 - private function get_phone_iframe_css() {
319 - static $cached_css = null;
320 - static $cached_mtime = null;
321 -
322 - $style_file = SUREDONATION_DIR . 'assets/css/vendor/intl/intlTelInput.min.css';
323 - if ( ! file_exists( $style_file ) ) {
324 - return '';
548 + public function add_phone_iframe_styles( $settings, $context ) {
549 + // Only the donation form editor uses the field blocks (incl. phone).
550 + if ( ! isset( $context->post ) || 'suredonation_form' !== $context->post->post_type ) {
551 + return $settings;
325 552 }
326 553
327 - $mtime = filemtime( $style_file );
328 - if ( null === $cached_css || $cached_mtime !== $mtime ) {
329 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading the plugin's vendored stylesheet to inline into the editor iframe.
330 - $css = file_get_contents( $style_file );
554 + $this->append_iframe_style(
555 + $settings,
556 + $this->read_iframe_css(
557 + SUREDONATION_DIR . 'assets/css/vendor/intl/intlTelInput.min.css',
558 + [ '../intl/img/' => SUREDONATION_URL . 'assets/css/vendor/intl/img/' ]
559 + )
560 + );
331 561
332 - if ( false === $css ) {
333 - $cached_css = '';
334 - } else {
335 - // The stylesheet references the flag/globe sprites with paths
336 - // relative to its own location (../intl/img/…). Inlining drops
337 - // that base, so rewrite them to absolute plugin URLs so the
338 - // flags resolve inside the iframe.
339 - $img_url = SUREDONATION_URL . 'assets/css/vendor/intl/img/';
340 - $cached_css = str_replace( '../intl/img/', $img_url, $css );
341 - }
342 -
343 - $cached_mtime = $mtime;
344 - }
345 -
346 - return $cached_css;
562 + return $settings;
347 563 }
348 564
349 565 /**
350 566 * Enqueue block editor assets.
@@ -423,8 +639,123 @@
423 639 // them as placeholders on each field's Error Message control.
424 640 'validationMessages' => \SureDonation\Inc\Field_Validation::get_resolved_validation_messages(),
425 641 ]
426 642 );
643 +
644 + // Field-preview images shown in the block inserter's preview pane (mirrors
645 + // SureForms). Blocks whose block.json sets example.attributes.preview render
646 + // this image via the withFieldPreview HOC; unmapped blocks fall back to the
647 + // shared placeholder key on the JS side.
648 + wp_localize_script(
649 + 'suredonation-blocks',
650 + 'suredonation_fields_preview',
651 + $this->get_field_preview_images()
652 + );
653 + }
654 +
655 + /**
656 + * Block-inserter preview images, keyed by block name (slug, hyphens as
657 + * underscores) to mirror the JS lookup in withFieldPreview.
658 + *
659 + * Shared by every editor bundle (field blocks, campaign blocks, donation-form
660 + * embed) so the same map is localized wherever a SureDonation block can be
661 + * inserted. Field-type blocks reuse the SureForms field-preview art; campaign
662 + * and donation-form display blocks use SureDonation's own mockups; anything not
663 + * listed falls back to the shared placeholder on the JS side.
664 + *
665 + * Pro-only field blocks (date/time picker) register their own art through the
666 + * `suredonation_block_preview_images` filter — Pro owns those assets, so they
667 + * are not listed here.
668 + *
669 + * Memoized so the map is built once per request: every localized copy is then
670 + * identical regardless of which editor hook fires first, and the filter runs
671 + * exactly once.
672 + *
673 + * @return array<string,string> Map of block key => image URL.
674 + * @since 1.5.0
675 + */
676 + public function get_field_preview_images() {
677 + if ( null !== $this->preview_images ) {
678 + return $this->preview_images;
679 + }
680 +
681 + $base = SUREDONATION_URL . 'images/field-previews/';
682 +
683 + /**
684 + * Filters the block-inserter preview image map.
685 + *
686 + * Keys are block slugs with hyphens replaced by underscores
687 + * (`suredonation/date-picker` => `date_picker`) to match the JS lookup in
688 + * withFieldPreview; values are absolute image URLs.
689 + *
690 + * Register no later than `init` — SureDonation Pro adds its date/time
691 + * picker art on `plugins_loaded`. The map is first built on
692 + * `enqueue_block_editor_assets` and then memoized, so a filter added after
693 + * that first build is silently ignored.
694 + *
695 + * @since 1.5.0
696 + *
697 + * @param array<string,string> $images Map of block key => image URL.
698 + */
699 + $images = apply_filters(
700 + 'suredonation_block_preview_images',
701 + [
702 + // Field blocks.
703 + 'input' => $base . 'input.svg',
704 + 'email' => $base . 'email.svg',
705 + 'number' => $base . 'number.svg',
706 + 'checkbox' => $base . 'checkbox.svg',
707 + // A multi-line message field — the textarea art, not the text input's.
708 + 'donor_comment' => $base . 'donor-comment.svg',
709 + // Anonymous-donation and cover-fees both render a single checkbox.
710 + 'anonymous_donation' => $base . 'checkbox.svg',
711 + 'cover_fees' => $base . 'checkbox.svg',
712 + 'dropdown' => $base . 'dropdown.svg',
713 + // Donation-amount is a grid of selectable amounts (radio options).
714 + 'donation_amount' => $base . 'multi-choice.svg',
715 + 'address' => $base . 'address.svg',
716 + 'phone' => $base . 'phone.svg',
717 + 'url' => $base . 'url.svg',
718 + 'heading' => $base . 'heading.svg',
719 + 'html' => $base . 'html.svg',
720 + 'image' => $base . 'image.svg',
721 + 'payment' => $base . 'payment.svg',
722 + 'donate_button' => $base . 'button.svg',
723 + // Campaign + donation-form display blocks.
724 + 'donation_form' => $base . 'donation-form.svg',
725 + 'campaign_goal' => $base . 'campaign-goal.svg',
726 + 'campaign_stats' => $base . 'campaign-stats.svg',
727 + 'campaign_donations' => $base . 'campaign-donations.svg',
728 + 'campaign_donors' => $base . 'campaign-donors.svg',
729 + 'campaign_donor_comments' => $base . 'campaign-donor-comments.svg',
730 + 'campaign_social_sharing' => $base . 'campaign-social-sharing.svg',
731 + 'campaign_donate_button' => $base . 'button.svg',
732 + // Fallback for any block without its own image.
733 + 'placeholder' => $base . 'placeholder.svg',
734 + ]
735 + );
736 +
737 + // Coerce defensively: a filter returning a non-array or non-string values
738 + // must not poison the map (mirrors the JS-side guard).
739 + if ( ! is_array( $images ) ) {
740 + $this->preview_images = [];
741 + return $this->preview_images;
742 + }
743 +
744 + $result = [];
745 + foreach ( $images as $key => $value ) {
746 + if ( is_string( $key ) && is_string( $value ) ) {
747 + // esc_url_raw for consistency with get_campaign_blocks_data(); the
748 + // value is consumed by JS as an <img src>, not printed as HTML. The
749 + // plugin version busts the browser cache when redesigned art ships
750 + // in a release (filemtime is avoided: filter-supplied Pro URLs do
751 + // not resolve to a local path).
752 + $result[ $key ] = esc_url_raw( add_query_arg( 'ver', SUREDONATION_VER, $value ) );
753 + }
754 + }
755 +
756 + $this->preview_images = $result;
757 + return $this->preview_images;
427 758 }
428 759
429 760 /**
430 761 * Register all blocks.