PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
← All changes | inc/frontend-assets.php +358 -40 0.0.13 → 2.12.8 View file →
@@ -8,9 +8,12 @@
8 8 */
9 9
10 10 namespace SRFM\Inc;
11 11
12 +use SRFM\Inc\Compatibility\Multilingual\String_Translator;
12 13 use SRFM\Inc\Traits\Get_Instance;
14 +use SRFM\Inc\Payments\Payment_Helper;
15 +use SRFM\Inc\Payments\Stripe\Stripe_Helper;
13 16
14 17 if ( ! defined( 'ABSPATH' ) ) {
15 18 exit; // Exit if accessed directly.
16 19 }
@@ -20,9 +23,8 @@
20 23 *
21 24 * @since 0.0.1
22 25 */
23 26 class Frontend_Assets {
24 -
25 27 use Get_Instance;
26 28
27 29 /**
28 30 * JS Assets.
@@ -30,10 +32,10 @@
30 32 * @since 0.0.11
31 33 * @var array<string>
32 34 */
33 35 public static $js_assets = [
34 - 'form-submit' => 'formSubmit.js',
35 - 'frontend' => 'frontend.min.js',
36 + 'form-submit' => 'formSubmit',
37 + 'frontend' => 'frontend',
36 38 ];
37 39
38 40 /**
39 41 * CSS Assets.
@@ -58,9 +60,8 @@
58 60 'tom-select' => 'tom-select',
59 61 'intl-tel-input' => 'intl/intlTelInput.min',
60 62 ];
61 63
62 -
63 64 /**
64 65 * Constructor
65 66 *
66 67 * @since 0.0.1
@@ -82,14 +83,11 @@
82 83 $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified';
83 84 $js_uri = SRFM_URL . 'assets/js/' . $dir_name . '/';
84 85 $css_uri = SRFM_URL . 'assets/css/' . $dir_name . '/';
85 86 $css_vendor = SRFM_URL . 'assets/css/minified/deps/';
87 + $is_rtl = is_rtl();
88 + $rtl = $is_rtl ? '-rtl' : '';
86 89
87 - /* RTL */
88 - if ( is_rtl() ) {
89 - $file_prefix .= '-rtl';
90 - }
91 -
92 90 $security_setting_options = get_option( 'srfm_security_settings_options' );
93 91 $is_set_v2_site_key = false;
94 92 if ( is_array( $security_setting_options ) && isset( $security_setting_options['srfm_v2_invisible_site_key'] ) && ! empty( $security_setting_options['srfm_v2_invisible_site_key'] ) ) {
95 93 $is_set_v2_site_key = true;
@@ -96,9 +94,9 @@
96 94 }
97 95
98 96 // Styles based on meta style.
99 97 foreach ( self::$css_assets as $handle => $path ) {
100 - wp_register_style( SRFM_SLUG . '-' . $handle, $css_uri . $path . $file_prefix . '.css', [], SRFM_VER );
98 + wp_register_style( SRFM_SLUG . '-' . $handle, $css_uri . $path . $file_prefix . $rtl . '.css', [], SRFM_VER );
101 99 }
102 100
103 101 // External styles.
104 102 foreach ( self::$css_external_assets as $handle => $path ) {
@@ -107,12 +105,27 @@
107 105
108 106 // Scripts.
109 107 foreach ( self::$js_assets as $handle => $name ) {
110 108 if ( 'form-submit' === $handle ) {
109 + // No 'wp-api-fetch' dependency: the script talks to the REST API via a
110 + // plain fetch() against the URLs localized below, not wp.apiFetch(),
111 + // so submissions no longer depend on that second script having loaded
112 + // and executed correctly. See the wp_localize_script() call below for
113 + // why wp.apiFetch's middleware (root-URL resolution, nonce injection)
114 + // isn't needed for either endpoint this script calls.
115 + //
116 + // 'wp-i18n' and 'wp-hooks' ARE required and must stay. The bundle imports
117 + // __() and applyFilters(), which @wordpress/scripts externalises to the
118 + // wp.i18n / window.wp.hooks globals instead of inlining — the generated
119 + // assets/build/formSubmit.asset.php is the authority on this list. They
120 + // used to arrive for free because 'wp-api-fetch' pulled them in through
121 + // its own dependency graph; dropping that above removed them, and an
122 + // undeclared wp.hooks is undefined under a JS-combining optimizer, which
123 + // kills every submission with the same TypeError this change prevents.
111 124 wp_register_script(
112 125 SRFM_SLUG . '-' . $handle,
113 - SRFM_URL . 'assets/build/' . $name,
114 - [],
126 + SRFM_URL . 'assets/build/' . $name . '.js',
127 + [ 'wp-i18n', 'wp-hooks' ],
115 128 SRFM_VER,
116 129 true
117 130 );
118 131 } else {
@@ -117,9 +130,9 @@
117 130 );
118 131 } else {
119 132 wp_register_script(
120 133 SRFM_SLUG . '-' . $handle,
121 - $js_uri . $name,
134 + $js_uri . $name . $file_prefix . '.js',
122 135 [],
123 136 SRFM_VER,
124 137 true
125 138 );
@@ -125,34 +138,95 @@
125 138 );
126 139 }
127 140 }
128 141
142 + $validation_messages = array_merge(
143 + Translatable::get_frontend_validation_messages(),
144 + [
145 + 'srfm_turnstile_error_message' => __( 'Turnstile sitekey verification failed. Please contact your site administrator.', 'sureforms' ),
146 + 'srfm_google_captcha_error_message' => __( 'Google Captcha sitekey verification failed. Please contact your site administrator.', 'sureforms' ),
147 + 'srfm_captcha_h_error_message' => __( 'HCaptcha sitekey verification failed. Please contact your site administrator.', 'sureforms' ),
148 + ]
149 + );
150 +
151 + // Translate each validation message through the active provider so
152 + // admin-supplied translations from WPML String Translation win over the
153 + // .mo-file fallback. When no provider is active, this is a pass-through.
154 + $validation_messages = String_Translator::get_instance()->translate_validation_messages( $validation_messages );
155 +
129 156 wp_localize_script(
130 157 SRFM_SLUG . '-form-submit',
131 158 SRFM_SLUG . '_submit',
132 159 [
133 - 'site_url' => site_url(),
134 - 'nonce' => wp_create_nonce( 'wp_rest' ),
160 + 'site_url' => site_url(),
161 + 'nonce' => wp_create_nonce( 'wp_rest' ),
162 + // Fully resolved REST endpoint URL, so the frontend can call it with a
163 + // plain fetch() instead of wp.apiFetch(). rest_url() already accounts
164 + // for pretty vs. plain permalinks (the latter needs a `?rest_route=`
165 + // query var rather than a path segment), subdirectory installs, and
166 + // multisite domain mapping — the same resolution wp.apiFetch's root-URL
167 + // middleware would otherwise do from a second, independently-loaded
168 + // script. submit-form's auth does not depend on that script either: it
169 + // is guarded by the X-WP-Submit-Token header (Submit_Token::verify()).
170 + //
171 + // The after-submission URL is deliberately NOT localized. It needs the
172 + // submission id and a per-submission nonce, so it is built server-side
173 + // and returned in the submit response instead (see Form_Submit). A base
174 + // URL here invited the client to concatenate those on, which silently
175 + // produced an unroutable URL wherever rest_url() returns a
176 + // `?rest_route=` form.
177 + 'submit_form_url' => esc_url_raw( rest_url( 'sureforms/v1/submit-form' ) ),
178 + 'messages' => $validation_messages,
179 + 'is_rtl' => $is_rtl,
180 + // Resolved RFC 5321 email limits so the client honors the
181 + // srfm_email_field_char_limits filter instead of hardcoding 64/255.
182 + 'email_char_limits' => Field_Validation::get_email_char_limits(),
183 + // Hint only. This value is baked into cached HTML and can be a full
184 + // cache TTL out of date, so the server re-checks on every write --
185 + // see Form_Submit::client_error_log_permissions_check(). Its job is
186 + // to keep the browser from posting when logging is plainly off.
187 + 'logging_enabled' => Client_Logger::is_enabled(),
188 + 'log_error_url' => esc_url_raw( rest_url( 'sureforms/v1/log-client-error' ) ),
135 189 ]
136 190 );
191 +
192 + $current_post = get_post();
193 +
194 + // Let's conditionally load form assets if current requested page has our forms.
195 + if ( $current_post instanceof \WP_Post ) {
196 + // Handles condition for Instant Form, Block Embedded, and Shortcode Embedded forms.
197 + $load_assets = ( SRFM_FORMS_POST_TYPE === $current_post->post_type || ( false !== strpos( $current_post->post_content, 'wp:srfm/form' ) || has_shortcode( $current_post->post_content, 'sureforms' ) ) );
198 +
199 + if ( $load_assets ) {
200 + // Load needed styles in head tag if current requested page has SureForms form.
201 + // Skip the SureForms stylesheets when every form on the page has default styling disabled.
202 + self::enqueue_scripts_and_styles( Form_Styling::should_skip_frontend_styles( $current_post ) );
203 + }
204 + }
137 205 }
138 206
139 207 /**
140 208 * Enqueue scripts and styles.
141 209 *
210 + * @param bool $skip_form_styles When true, the SureForms stylesheets are not enqueued
211 + * (default styling disabled for the form). External library
212 + * styles and scripts are always loaded so advanced fields
213 + * like Dropdown and Phone keep working.
142 214 * @return void
143 215 * @since 0.0.11
144 216 */
145 - public static function enqueue_scripts_and_styles() {
217 + public static function enqueue_scripts_and_styles( $skip_form_styles = false ) {
146 218 // Load the styles.
147 - foreach ( self::$css_assets as $handle => $path ) {
219 + if ( ! $skip_form_styles ) {
220 + foreach ( self::$css_assets as $handle => $path ) {
148 221
149 - // Skip single form styles if not on single form page.
150 - if ( 'single' === $handle && ! is_singular( SRFM_FORMS_POST_TYPE ) ) {
151 - continue;
222 + // Skip single form styles if not on single form page.
223 + if ( 'single' === $handle && ! is_singular( SRFM_FORMS_POST_TYPE ) ) {
224 + continue;
225 + }
226 +
227 + wp_enqueue_style( SRFM_SLUG . '-' . $handle );
152 228 }
153 -
154 - wp_enqueue_style( SRFM_SLUG . '-' . $handle );
155 229 }
156 230
157 231 // Load the external styles. Like Phone and Tom Select.
158 232 foreach ( self::$css_external_assets as $handle => $path ) {
@@ -167,13 +241,14 @@
167 241
168 242 /**
169 243 * Enqueue block scripts
170 244 *
171 - * @param string $block_type block name.
245 + * @param string $block_type block name.
246 + * @param array<string, mixed> $attr Array of block attributes.
172 247 * @since 0.0.1
173 248 * @return void
174 249 */
175 - public function enqueue_srfm_script( $block_type ) {
250 + public function enqueue_srfm_script( $block_type, $attr ) {
176 251 $block_name = str_replace( 'srfm/', '', $block_type );
177 252 // associative array to keep the count of block that requires scripts to work.
178 253 $script_dep_blocks = [
179 254 'dropdown' => 0,
@@ -195,13 +270,18 @@
195 270 ) {
196 271 $script_dep_blocks[ $block_name ] += 1;
197 272 $js_uri = SRFM_URL . 'assets/js/' . $dir_name . '/blocks/';
198 273 $js_vendor_uri = SRFM_URL . 'assets/js/minified/deps/';
274 + $css_vendor_uri = SRFM_URL . 'assets/css/minified/deps/';
275 + if ( 'phone' === $block_name ) {
276 + // Enqueue main intl-tel-input library.
277 + wp_enqueue_script( SRFM_SLUG . "-{$block_name}-intl-input-deps", $js_vendor_uri . 'intl/intTelInputWithUtils.min.js', [], SRFM_VER, true );
199 278
200 - if ( 'phone' === $block_name
201 - ) {
202 - wp_enqueue_script( SRFM_SLUG . "-{$block_name}-intl-input-deps", $js_vendor_uri . 'intl/intTelInput.min.js', [], SRFM_VER, true );
203 - wp_enqueue_script( SRFM_SLUG . "-{$block_name}-intl-utils-deps", $js_vendor_uri . 'intl/intTelUtils.min.js', [], SRFM_VER, true );
279 + // Enqueue i18n translations if available for current locale.
280 + self::enqueue_intl_tel_input_i18n(
281 + SRFM_SLUG . "-{$block_name}-intl-i18n",
282 + SRFM_SLUG . "-{$block_name}-intl-input-deps"
283 + );
204 284 }
205 285
206 286 if ( 'dropdown' === $block_name ) {
207 287 // if the dropdown / address-compact block is after any other block, then we need to dequeue the srfm-form-submit script and enqueue it again and load it with tom-select dependency.
@@ -214,35 +294,226 @@
214 294 SRFM_URL . 'assets/build/formSubmit.js',
215 295 [
216 296 'srfm-tom-select',
217 297 'srfm-dropdown',
298 + 'wp-api-fetch',
218 299 ],
219 300 SRFM_VER,
220 301 true
221 302 );
303 + }
222 304
305 + $is_not_dropdown = 'dropdown' !== $block_name;
306 + $is_not_textarea = 'textarea' !== $block_name;
307 +
308 + if ( $is_not_dropdown && $is_not_textarea ) {
309 + // Set dependencies for phone block to ensure intl-tel-input loads first.
310 + $block_dependencies = [];
311 + if ( 'phone' === $block_name ) {
312 + // Phone.js depends on intl-tel-input library (and i18n if loaded).
313 + $block_dependencies = [ SRFM_SLUG . "-{$block_name}-intl-input-deps" ];
314 + }
315 + wp_enqueue_script( SRFM_SLUG . "-{$block_name}", $js_uri . $block_name . $file_prefix . '.js', $block_dependencies, SRFM_VER, true );
316 +
317 + if ( 'phone' === $block_name ) {
318 + // Geo-country endpoint for per-visitor auto country detection.
319 + // Built with rest_url() so it is correct regardless of permalink structure.
320 + wp_localize_script(
321 + SRFM_SLUG . "-{$block_name}",
322 + 'srfm_phone_data',
323 + [
324 + 'geo_endpoint' => esc_url_raw( rest_url( 'sureforms/v1/geo-country' ) ),
325 + ]
326 + );
327 + }
328 + }
329 +
330 + if ( 'input' === $block_name && isset( $attr['inputMask'] ) && 'none' !== $attr['inputMask'] ) {
331 + // Input mask JS - only load when inputMask is configured.
332 + wp_enqueue_script( SRFM_SLUG . '-inputmask', $js_vendor_uri . 'inputmask.min.js', [], SRFM_VER, true );
333 + }
334 +
335 + // Adding js for the input textarea block.
336 + if ( 'textarea' === $block_name && ! empty( $attr['isRichText'] ) ) {
337 + wp_enqueue_script( SRFM_SLUG . '-quill-editor', $js_vendor_uri . '/quill.min.js', [], SRFM_VER, true );
338 +
339 + wp_enqueue_style( SRFM_SLUG . '-quill-editor', $css_vendor_uri . 'quill/quill.snow.css', [], SRFM_VER );
340 +
341 + wp_enqueue_script( SRFM_SLUG . '-textarea', $js_uri . 'textarea' . $file_prefix . '.js', [], SRFM_VER, true );
342 +
223 343 wp_localize_script(
224 - SRFM_SLUG . '-form-submit',
225 - SRFM_SLUG . '_submit',
344 + SRFM_SLUG . '-textarea',
345 + 'srfm_quill_i18n',
226 346 [
227 - 'site_url' => site_url(),
228 - 'nonce' => wp_create_nonce( 'wp_rest' ),
347 + 'normal' => _x( 'Normal', 'Quill heading picker: default paragraph style', 'sureforms' ),
348 + 'heading_1' => __( 'Heading 1', 'sureforms' ),
349 + 'heading_2' => __( 'Heading 2', 'sureforms' ),
350 + 'heading_3' => __( 'Heading 3', 'sureforms' ),
351 + 'heading_4' => __( 'Heading 4', 'sureforms' ),
352 + 'heading_5' => __( 'Heading 5', 'sureforms' ),
353 + 'heading_6' => __( 'Heading 6', 'sureforms' ),
354 + 'visit_url' => _x( 'Visit URL:', 'Quill link tooltip label', 'sureforms' ),
355 + 'enter_link' => _x( 'Enter link:', 'Quill link tooltip label', 'sureforms' ),
356 + 'edit' => _x( 'Edit', 'Quill link tooltip action', 'sureforms' ),
357 + 'save' => _x( 'Save', 'Quill link tooltip action', 'sureforms' ),
358 + 'remove' => _x( 'Remove', 'Quill link tooltip action', 'sureforms' ),
229 359 ]
230 360 );
231 361 }
362 + }
363 + /**
364 + * Enqueueing the input mask JS for input and date-picker blocks.
365 + * This is a workaround for the input mask JS to work with the date-picker block.
366 + * Not adding in the above existing condition because code only runs when free block are added in the form.
367 + * Aim is to reduce redundant code and library file duplication.
368 + */
369 + if ( 'date-picker' === $block_name ) {
370 + // Input mask JS.
371 + wp_enqueue_script( SRFM_SLUG . '-inputmask', SRFM_URL . 'assets/js/minified/deps/inputmask.min.js', [], SRFM_VER, true );
372 + }
232 373
233 - if ( 'dropdown' !== $block_name ) {
234 - wp_enqueue_script( SRFM_SLUG . "-{$block_name}", $js_uri . $block_name . $file_prefix . '.js', [], SRFM_VER, true );
235 - }
374 + if ( 'payment' === $block_name ) {
375 + // Register Stripe.js library from CDN.
376 + // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Stripe CDN ignores version; version param is included to keep linter happy.
377 + wp_enqueue_script(
378 + 'stripe-js',
379 + 'https://js.stripe.com/v3/',
380 + [],
381 + SRFM_VER,
382 + true
383 + );
236 384
237 - if ( 'input' === $block_name ) {
238 - // Input mask JS.
239 - wp_enqueue_script( SRFM_SLUG . '-inputmask', $js_vendor_uri . 'inputmask.min.js', [], SRFM_VER, true );
240 - }
385 + wp_enqueue_script(
386 + SRFM_SLUG . '-stripe-payment',
387 + SRFM_URL . 'assets/js/stripe-payment.js',
388 + [ 'stripe-js' ],
389 + SRFM_VER,
390 + true
391 + );
392 +
393 + // Enqueue Payment Manager for payment method switching.
394 + wp_enqueue_script(
395 + SRFM_SLUG . '-payment-manager',
396 + SRFM_URL . 'assets/js/payment-manager.js',
397 + [ SRFM_SLUG . '-stripe-payment' ],
398 + SRFM_VER,
399 + true
400 + );
401 +
402 + // Localize script for Stripe payment functionality.
403 + wp_localize_script(
404 + SRFM_SLUG . '-stripe-payment',
405 + 'srfm_ajax',
406 + [
407 + 'ajax_url' => admin_url( 'admin-ajax.php' ),
408 + ]
409 + );
410 +
411 + // Localize Stripe payment data for frontend.
412 + wp_localize_script(
413 + SRFM_SLUG . '-stripe-payment',
414 + 'srfmStripe',
415 + [
416 + 'zeroDecimalCurrencies' => Payment_Helper::get_zero_decimal_currencies(),
417 + 'currenciesData' => Payment_Helper::get_all_currencies_data(),
418 + 'strings' => Payment_Helper::get_payment_strings(),
419 + 'currencySignPosition' => Payment_Helper::get_currency_sign_position(),
420 + ]
421 + );
241 422 }
423 +
424 + // Trigger custom action hook to allow third-party plugins or add-ons
425 + // to enqueue additional scripts/styles for specific blocks (e.g., payment providers).
426 + do_action(
427 + 'srfm_enqueue_block_scripts',
428 + [
429 + 'block_name' => $block_name,
430 + 'attr' => $attr,
431 + ]
432 + );
242 433 }
243 434
244 435 /**
436 + * Maps WordPress locale to intl-tel-input language code.
437 + *
438 + * Converts WordPress locale format (e.g., fr_FR, de_DE, pt_BR)
439 + * to intl-tel-input language codes (e.g., fr, de, pt).
440 + * Returns null if the language is not supported by intl-tel-input.
441 + *
442 + * @since 2.5.0
443 + * @param string|null $locale WordPress locale string. If null, uses get_locale().
444 + * @return string|null Language code if supported, null if not supported or English.
445 + */
446 + public static function get_intl_tel_input_locale( $locale = null ) {
447 + // Get WordPress locale if not provided.
448 + if ( null === $locale ) {
449 + $locale = get_locale();
450 + }
451 +
452 + // Extract language code from WordPress locale (e.g., fr_FR -> fr, pt_BR -> pt).
453 + $lang_code = substr( $locale, 0, 2 );
454 +
455 + // List of supported languages - available translation files in /assets/js/minified/deps/intl/i18n/.
456 + // Reference: https://unpkg.com/browse/[email protected]/build/js/i18n/.
457 + $supported_languages = [
458 + 'de', // German - Deutsch.
459 + 'es', // Spanish - Español.
460 + 'fr', // French - Français.
461 + 'it', // Italian - Italiano.
462 + 'nl', // Dutch - Nederlands.
463 + 'pl', // Polish - Polski.
464 + 'pt', // Portuguese - Português.
465 + ];
466 +
467 + // Return language code only if supported and not English (English is default).
468 + if ( in_array( $lang_code, $supported_languages, true ) && 'en' !== $lang_code ) {
469 + return $lang_code;
470 + }
471 +
472 + // Return null for unsupported languages or English (uses default English).
473 + return null;
474 + }
475 +
476 + /**
477 + * Enqueues intl-tel-input i18n script for the phone field.
478 + *
479 + * This method handles conditional loading of language files based on WordPress locale.
480 + * Only enqueues if a supported non-English language is detected and the file exists.
481 + *
482 + * @since 2.5.0
483 + * @param string $handle Script handle to enqueue.
484 + * @param string $dependencies Optional. Script handle that this i18n depends on. Default empty.
485 + * @return bool True if i18n was enqueued, false otherwise.
486 + */
487 + public static function enqueue_intl_tel_input_i18n( $handle, $dependencies = '' ) {
488 + $intl_locale = self::get_intl_tel_input_locale();
489 +
490 + // Return early if no locale or English.
491 + if ( empty( $intl_locale ) ) {
492 + return false;
493 + }
494 +
495 + $i18n_file_path = SRFM_DIR . "assets/js/minified/deps/intl/i18n/{$intl_locale}/index.min.js";
496 +
497 + // Only enqueue if the language file exists.
498 + if ( ! file_exists( $i18n_file_path ) ) {
499 + return false;
500 + }
501 +
502 + $deps = ! empty( $dependencies ) ? [ $dependencies ] : [];
503 +
504 + wp_enqueue_script(
505 + $handle,
506 + SRFM_URL . "assets/js/minified/deps/intl/i18n/{$intl_locale}/index.min.js",
507 + $deps,
508 + SRFM_VER,
509 + true
510 + );
511 +
512 + return true;
513 + }
514 +
515 + /**
245 516 * Render function.
246 517 *
247 518 * @param string $block_content Entire Block Content.
248 519 * @param array<string> $block Block Properties As An Array.
@@ -249,10 +520,17 @@
249 520 * @return string
250 521 */
251 522 public function generate_render_script( $block_content, $block ) {
252 523
524 + if ( isset( $block['attrs']['isEditing'] ) ) {
525 + // Only load block assets on the frontend.
526 + return $block_content;
527 + }
528 +
253 529 if ( isset( $block['blockName'] ) ) {
254 - self::enqueue_srfm_script( $block['blockName'] );
530 + $attr = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [];
531 +
532 + self::enqueue_srfm_script( $block['blockName'], $attr );
255 533 }
256 534 return $block_content;
257 535 }
258 536
@@ -269,8 +547,10 @@
269 547 // Bail if not SureForms post type.
270 548 return $template;
271 549 }
272 550
551 + self::reset_printed_assets();
552 +
273 553 $file_name = 'single-form.php';
274 554 $template = locate_template( $file_name );
275 555
276 556 /**
@@ -278,7 +558,45 @@
278 558 *
279 559 * @since 0.0.1
280 560 */
281 561 return apply_filters( 'srfm_form_template', $template ? $template : SRFM_DIR . '/templates/' . $file_name );
562 + }
563 +
564 + /**
565 + * Let the Instant Form template print assets a discarded render already claimed.
566 + *
567 + * `page_template()` runs on `template_include` at PHP_INT_MAX and returns the
568 + * Instant Form template regardless of what earlier filters returned. A page
569 + * builder that renders the whole page inside its own `template_include`
570 + * filter has therefore already run `wp_head()` and `wp_footer()` into an
571 + * output buffer that is about to be thrown away.
572 + *
573 + * The buffer goes, but `WP_Styles::$done` and `WP_Scripts::$done` still hold
574 + * every handle it claimed, so `do_items()` skips them when `single-form.php`
575 + * calls `wp_head()` and `wp_footer()` for real. The form arrives with no
576 + * stylesheets, and -- because `srfm-form-submit` is registered for the footer
577 + * -- no submit handler either.
578 + *
579 + * Clearing both `done` lists wholesale is the right scope rather than an
580 + * over-broad one: only the render that begins after this filter returns
581 + * reaches the browser, so nothing recorded before it was ever delivered. The
582 + * handles are re-enqueued by the second `wp_enqueue_scripts` pass, so they
583 + * print normally once `done` stops shadowing them.
584 + *
585 + * Guarded on `wp_head` having already fired, so this is inert on the ordinary
586 + * path where no builder rendered first and nothing has been printed yet.
587 + *
588 + * @since 2.12.7
589 + * @return void
590 + */
591 + private static function reset_printed_assets() {
592 + // No earlier wp_head() means no discarded render, so there is nothing to
593 + // forget. Deny is the fallthrough: act only on the state this repairs.
594 + if ( ! did_action( 'wp_head' ) ) {
595 + return;
596 + }
597 +
598 + wp_styles()->done = [];
599 + wp_scripts()->done = [];
282 600 }
283 601
284 602 }