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 +473 -48 1.0.0 → 1.6.1 View file →
@@ -6,11 +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;
13 +use SureDonation\Inc\Payments\PayPal\PayPal_Helper;
11 14 use SureDonation\Inc\Payments\Payment_Helper;
12 15 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
16 +use SureDonation\Inc\Post_Types\Donation_Form;
13 17 use SureDonation\Inc\Traits\Get_Instance;
14 18
15 19 // Exit if accessed directly.
16 20 if ( ! defined( 'ABSPATH' ) ) {
@@ -25,8 +29,20 @@
25 29 class Register {
26 30 use Get_Instance;
27 31
28 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 + /**
29 45 * Constructor.
30 46 *
31 47 * @since 0.0.1
32 48 */
@@ -34,13 +50,128 @@
34 50 add_action( 'init', [ $this, 'register_embed_block_script' ], 5 );
35 51 add_action( 'init', [ $this, 'register_blocks' ] );
36 52 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
37 53 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_campaign_editor_assets' ] );
54 + add_action( 'enqueue_block_editor_assets', [ $this, 'localize_embed_preview_images' ] );
38 55 add_filter( 'block_categories_all', [ $this, 'register_block_category' ], 10, 2 );
39 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 );
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' ] );
40 60 }
41 61
42 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 + /**
43 174 * Register the donation form embed block editor script.
44 175 *
45 176 * Runs before register_blocks() so the handle exists when block.json is read.
46 177 * Not gated by post type — the embed block should work on all post types.
@@ -73,8 +204,16 @@
73 204 'suredonationCampaignBlocks',
74 205 $this->get_campaign_blocks_data()
75 206 );
76 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 +
77 216 wp_register_style(
78 217 'suredonation-donation-form-editor',
79 218 SUREDONATION_URL . 'assets/build/blocks/donation-form/editor.css',
80 219 [],
@@ -82,19 +221,56 @@
82 221 );
83 222 }
84 223
85 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 + /**
86 251 * Data localized for the block editor placeholders (logo).
87 252 *
88 253 * Shared by the donation form embed block and the campaign display blocks,
89 254 * both of which expose it on the `suredonationCampaignBlocks` JS global.
90 255 *
256 + * `currentPostType` lets a block scope its editor registration to a single
257 + * post type (the Campaign Donate Button registers only on the campaign
258 + * editor). It is read from the current screen, so it is only populated for
259 + * the caller that runs on `enqueue_block_editor_assets` (the campaign editor
260 + * assets); the embed-block caller runs on `init`, where there is no screen,
261 + * so it receives an empty string. That is harmless — the embed block only
262 + * consumes `logoUrl`.
263 + *
91 264 * @return array<string, string>
92 265 * @since 1.0.0
93 266 */
94 267 public function get_campaign_blocks_data() {
268 + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
269 +
95 270 return [
96 - 'logoUrl' => esc_url_raw( SUREDONATION_URL . 'images/suredonation-logo.svg' ),
271 + 'logoUrl' => esc_url_raw( SUREDONATION_URL . 'images/suredonation-logo.svg' ),
272 + 'currentPostType' => $screen ? (string) $screen->post_type : '',
97 273 ];
98 274 }
99 275
100 276 /**
@@ -182,45 +358,41 @@
182 358 'suredonationCampaignBlocks',
183 359 $this->get_campaign_blocks_data()
184 360 );
185 361
186 - // Style the server-side-rendered block previews in the editor.
187 - $style_file = SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css';
188 - $style_version = file_exists( $style_file )
189 - ? (string) filemtime( $style_file )
190 - : SUREDONATION_VER;
191 -
192 - wp_enqueue_style(
362 + // Block-inserter preview images (see withFieldPreview / get_field_preview_images()).
363 + wp_localize_script(
193 364 'suredonation-campaign-blocks',
194 - SUREDONATION_URL . 'assets/build/blocks/campaign/style-style.css',
195 - [],
196 - $style_version
365 + 'suredonation_fields_preview',
366 + $this->get_field_preview_images()
197 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.
198 377 }
199 378
200 379 /**
201 - * Inject the campaign block styles into the editor canvas iframe.
380 + * Append an inline stylesheet to the block-editor iframe settings.
202 381 *
203 382 * Styles enqueued via enqueue_block_editor_assets load in the editor's outer
204 - * frame only; the block canvas is iframed, so the server-side-rendered campaign
205 - * block previews would otherwise render unstyled. Adding the CSS to the editor
206 - * 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.
207 386 *
208 - * @param array<string, mixed> $settings Block editor settings.
209 - * @param \WP_Block_Editor_Context $context Block editor context.
210 - * @return array<string, mixed> Modified settings.
211 - * @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
212 391 */
213 - public function add_campaign_iframe_styles( $settings, $context ) {
214 - // Inject wherever the campaign blocks can be used (everywhere except the
215 - // donation form editor), so their editor previews match the frontend.
216 - if ( ! isset( $context->post ) || 'suredonation_form' === $context->post->post_type ) {
217 - return $settings;
218 - }
219 -
220 - $css = $this->get_campaign_iframe_css();
392 + private function append_iframe_style( &$settings, $css ) {
221 393 if ( '' === $css ) {
222 - return $settings;
394 + return;
223 395 }
224 396
225 397 if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
226 398 $settings['styles'] = [];
@@ -226,41 +398,172 @@
226 398 $settings['styles'] = [];
227 399 }
228 400
229 401 $settings['styles'][] = [ 'css' => $css ];
230 -
231 - return $settings;
232 402 }
233 403
234 404 /**
235 - * Read the built campaign stylesheet, cached per request by file mtime so
236 - * the filter (which can run more than once per load) reads from disk at most
237 - * 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.
238 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.
239 415 * @return string The stylesheet contents, or '' when unavailable.
240 - * @since 1.0.0
416 + * @since 1.4.0
241 417 */
242 - private function get_campaign_iframe_css() {
243 - static $cached_css = null;
244 - 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 = [];
245 422
246 - $style_file = SUREDONATION_DIR . 'assets/build/blocks/campaign/style-style.css';
247 423 if ( ! file_exists( $style_file ) ) {
248 424 return '';
249 425 }
250 426
251 427 $mtime = filemtime( $style_file );
252 - if ( null === $cached_css || $cached_mtime !== $mtime ) {
253 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading the plugin's own built stylesheet to inline into the editor iframe.
254 - $css = file_get_contents( $style_file );
255 - $cached_css = false === $css ? '' : $css;
256 - $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 + ];
257 441 }
258 442
259 - return $cached_css;
443 + return $cache[ $style_file ]['css'];
260 444 }
261 445
262 446 /**
447 + * Inject the campaign block styles into the editor canvas iframe.
448 + *
449 + * @param array<string, mixed> $settings Block editor settings.
450 + * @param \WP_Block_Editor_Context $context Block editor context.
451 + * @return array<string, mixed> Modified settings.
452 + * @since 1.0.0
453 + */
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 ) {
467 + return $settings;
468 + }
469 +
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 ) {
511 + return $settings;
512 + }
513 +
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 + );
529 +
530 + return $settings;
531 + }
532 +
533 + /**
534 + * Inject the intl-tel-input stylesheet into the editor canvas iframe.
535 + *
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.
546 + * @since 1.1.1
547 + */
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;
552 + }
553 +
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 + );
561 +
562 + return $settings;
563 + }
564 +
565 + /**
263 566 * Enqueue block editor assets.
264 567 *
265 568 * Only loads on the donation form editor.
266 569 *
@@ -304,11 +607,15 @@
304 607 wp_localize_script(
305 608 'suredonation-blocks',
306 609 'suredonation_admin',
307 610 [
308 - 'payments' => [
611 + 'payments' => [
309 612 'stripe_connected' => Stripe_Helper::is_stripe_connected(),
613 + 'paypal_connected' => PayPal_Helper::is_paypal_connected(),
310 614 'stripe_connect_url' => Stripe_Helper::get_stripe_connect_url(),
615 + // Base payments-settings URL; the editor's "Configure Payment
616 + // Account" CTA appends the block's selected gateway subpage.
617 + 'settings_url' => Payment_Helper::get_settings_url(),
311 618 'offline_enabled' => Offline_Helper::is_offline_enabled(),
312 619 'gateways' => apply_filters(
313 620 'suredonation_editor_payment_gateways',
314 621 [
@@ -324,13 +631,131 @@
324 631 ],
325 632 ]
326 633 ),
327 634 ],
328 - 'fee_recovery' => Payment_Helper::get_fee_recovery_settings(),
329 - 'currency' => $global_currency,
330 - 'currencySymbol' => Payment_Helper::get_currency_symbol( $global_currency ),
635 + 'fee_recovery' => Payment_Helper::get_fee_recovery_settings(),
636 + 'currency' => $global_currency,
637 + 'currencySymbol' => Payment_Helper::get_currency_symbol( $global_currency ),
638 + // Resolved default validation messages so the editor can show
639 + // them as placeholders on each field's Error Message control.
640 + 'validationMessages' => \SureDonation\Inc\Field_Validation::get_resolved_validation_messages(),
331 641 ]
332 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;
333 758 }
334 759
335 760 /**
336 761 * Register all blocks.