| @@ -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,12 +23,46 @@ | ||
| 20 | 23 | * |
| 21 | 24 | * @since 0.0.1 |
| 22 | 25 | */ |
| 23 | 26 | class Frontend_Assets { |
| 27 | + use Get_Instance; | |
| 24 | 28 | |
| 25 | - use Get_Instance; | |
| 29 | + /** | |
| 30 | + * JS Assets. | |
| 31 | + * | |
| 32 | + * @since 0.0.11 | |
| 33 | + * @var array<string> | |
| 34 | + */ | |
| 35 | + public static $js_assets = [ | |
| 36 | + 'form-submit' => 'formSubmit', | |
| 37 | + 'frontend' => 'frontend', | |
| 38 | + ]; | |
| 26 | 39 | |
| 27 | 40 | /** |
| 41 | + * CSS Assets. | |
| 42 | + * | |
| 43 | + * @since 0.0.11 | |
| 44 | + * @var array<string> | |
| 45 | + */ | |
| 46 | + public static $css_assets = [ | |
| 47 | + 'frontend-default' => 'blocks/default/frontend', | |
| 48 | + 'common' => 'common', | |
| 49 | + 'form' => 'frontend/form', | |
| 50 | + 'single' => 'single', | |
| 51 | + ]; | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * External CSS Assets. | |
| 55 | + * | |
| 56 | + * @since 0.0.11 | |
| 57 | + * @var array<string> | |
| 58 | + */ | |
| 59 | + public static $css_external_assets = [ | |
| 60 | + 'tom-select' => 'tom-select', | |
| 61 | + 'intl-tel-input' => 'intl/intlTelInput.min', | |
| 62 | + ]; | |
| 63 | + | |
| 64 | + /** | |
| 28 | 65 | * Constructor |
| 29 | 66 | * |
| 30 | 67 | * @since 0.0.1 |
| 31 | 68 | */ |
| @@ -30,10 +67,9 @@ | ||
| 30 | 67 | * @since 0.0.1 |
| 31 | 68 | */ |
| 32 | 69 | public function __construct() { |
| 33 | 70 | add_filter( 'template_include', [ $this, 'page_template' ], PHP_INT_MAX ); |
| 34 | - | |
| 35 | - add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); | |
| 71 | + add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] ); | |
| 36 | 72 | add_filter( 'render_block', [ $this, 'generate_render_script' ], 10, 2 ); |
| 37 | 73 | } |
| 38 | 74 | |
| 39 | 75 | /** |
| @@ -41,20 +77,17 @@ | ||
| 41 | 77 | * |
| 42 | 78 | * @return void |
| 43 | 79 | * @since 0.0.1 |
| 44 | 80 | */ |
| 45 | - public function enqueue_scripts() { | |
| 81 | + public function register_scripts() { | |
| 46 | 82 | $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min'; |
| 47 | 83 | $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified'; |
| 48 | 84 | $js_uri = SRFM_URL . 'assets/js/' . $dir_name . '/'; |
| 49 | 85 | $css_uri = SRFM_URL . 'assets/css/' . $dir_name . '/'; |
| 50 | 86 | $css_vendor = SRFM_URL . 'assets/css/minified/deps/'; |
| 87 | + $is_rtl = is_rtl(); | |
| 88 | + $rtl = $is_rtl ? '-rtl' : ''; | |
| 51 | 89 | |
| 52 | - /* RTL */ | |
| 53 | - if ( is_rtl() ) { | |
| 54 | - $file_prefix .= '-rtl'; | |
| 55 | - } | |
| 56 | - | |
| 57 | 90 | $security_setting_options = get_option( 'srfm_security_settings_options' ); |
| 58 | 91 | $is_set_v2_site_key = false; |
| 59 | 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'] ) ) { |
| 60 | 93 | $is_set_v2_site_key = true; |
| @@ -60,53 +93,162 @@ | ||
| 60 | 93 | $is_set_v2_site_key = true; |
| 61 | 94 | } |
| 62 | 95 | |
| 63 | 96 | // Styles based on meta style. |
| 64 | - wp_enqueue_style( SRFM_SLUG . '-frontend-default', $css_uri . '/blocks/default/frontend' . $file_prefix . '.css', [], SRFM_VER ); | |
| 97 | + foreach ( self::$css_assets as $handle => $path ) { | |
| 98 | + wp_register_style( SRFM_SLUG . '-' . $handle, $css_uri . $path . $file_prefix . $rtl . '.css', [], SRFM_VER ); | |
| 99 | + } | |
| 65 | 100 | |
| 66 | - // Common styles for all meta styles. | |
| 67 | - wp_enqueue_style( SRFM_SLUG . '-common', $css_uri . 'common' . $file_prefix . '.css', [], SRFM_VER, 'all' ); | |
| 68 | - wp_enqueue_style( SRFM_SLUG . '-form', $css_uri . 'frontend/form' . $file_prefix . '.css', [], SRFM_VER, 'all' ); | |
| 101 | + // External styles. | |
| 102 | + foreach ( self::$css_external_assets as $handle => $path ) { | |
| 103 | + wp_register_style( SRFM_SLUG . '-' . $handle, $css_vendor . $path . '.css', [], SRFM_VER ); | |
| 104 | + } | |
| 69 | 105 | |
| 70 | - if ( is_single() ) { | |
| 71 | - wp_enqueue_style( SRFM_SLUG . '-single', $css_uri . 'single' . $file_prefix . '.css', [], SRFM_VER ); | |
| 106 | + // Scripts. | |
| 107 | + foreach ( self::$js_assets as $handle => $name ) { | |
| 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. | |
| 124 | + wp_register_script( | |
| 125 | + SRFM_SLUG . '-' . $handle, | |
| 126 | + SRFM_URL . 'assets/build/' . $name . '.js', | |
| 127 | + [ 'wp-i18n', 'wp-hooks' ], | |
| 128 | + SRFM_VER, | |
| 129 | + true | |
| 130 | + ); | |
| 131 | + } else { | |
| 132 | + wp_register_script( | |
| 133 | + SRFM_SLUG . '-' . $handle, | |
| 134 | + $js_uri . $name . $file_prefix . '.js', | |
| 135 | + [], | |
| 136 | + SRFM_VER, | |
| 137 | + true | |
| 138 | + ); | |
| 139 | + } | |
| 72 | 140 | } |
| 73 | 141 | |
| 74 | - // Dependencies | |
| 75 | - // Nice Select CSS. | |
| 76 | - wp_enqueue_style( SRFM_SLUG . '-tom-select', $css_vendor . 'tom-select.css', [], SRFM_VER ); | |
| 77 | - // Int-tel-input CSS. | |
| 78 | - wp_enqueue_style( SRFM_SLUG . '-intl-tel-input', $css_vendor . 'intl/intlTelInput.min.css', [], SRFM_VER ); | |
| 79 | - | |
| 80 | - wp_enqueue_script( | |
| 81 | - SRFM_SLUG . '-form-submit', | |
| 82 | - SRFM_URL . 'assets/build/formSubmit.js', | |
| 83 | - [], | |
| 84 | - SRFM_VER, | |
| 85 | - true | |
| 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 | + ] | |
| 86 | 149 | ); |
| 87 | 150 | |
| 88 | - // Frontend common and validation before submit. | |
| 89 | - wp_enqueue_script( SRFM_SLUG . '-frontend', $js_uri . 'frontend.min.js', [], SRFM_VER, true ); | |
| 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 ); | |
| 90 | 155 | |
| 91 | 156 | wp_localize_script( |
| 92 | 157 | SRFM_SLUG . '-form-submit', |
| 93 | 158 | SRFM_SLUG . '_submit', |
| 94 | 159 | [ |
| 95 | - 'site_url' => site_url(), | |
| 96 | - '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' ) ), | |
| 97 | 189 | ] |
| 98 | 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 | + } | |
| 99 | 205 | } |
| 100 | 206 | |
| 101 | 207 | /** |
| 208 | + * Enqueue scripts and styles. | |
| 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. | |
| 214 | + * @return void | |
| 215 | + * @since 0.0.11 | |
| 216 | + */ | |
| 217 | + public static function enqueue_scripts_and_styles( $skip_form_styles = false ) { | |
| 218 | + // Load the styles. | |
| 219 | + if ( ! $skip_form_styles ) { | |
| 220 | + foreach ( self::$css_assets as $handle => $path ) { | |
| 221 | + | |
| 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 ); | |
| 228 | + } | |
| 229 | + } | |
| 230 | + | |
| 231 | + // Load the external styles. Like Phone and Tom Select. | |
| 232 | + foreach ( self::$css_external_assets as $handle => $path ) { | |
| 233 | + wp_enqueue_style( SRFM_SLUG . '-' . $handle ); | |
| 234 | + } | |
| 235 | + | |
| 236 | + // Load the scripts. | |
| 237 | + foreach ( self::$js_assets as $handle => $path ) { | |
| 238 | + wp_enqueue_script( SRFM_SLUG . '-' . $handle ); | |
| 239 | + } | |
| 240 | + } | |
| 241 | + | |
| 242 | + /** | |
| 102 | 243 | * Enqueue block scripts |
| 103 | 244 | * |
| 104 | - * @param string $block_type block name. | |
| 245 | + * @param string $block_type block name. | |
| 246 | + * @param array<string, mixed> $attr Array of block attributes. | |
| 105 | 247 | * @since 0.0.1 |
| 106 | 248 | * @return void |
| 107 | 249 | */ |
| 108 | - public function enqueue_srfm_script( $block_type ) { | |
| 250 | + public function enqueue_srfm_script( $block_type, $attr ) { | |
| 109 | 251 | $block_name = str_replace( 'srfm/', '', $block_type ); |
| 110 | 252 | // associative array to keep the count of block that requires scripts to work. |
| 111 | 253 | $script_dep_blocks = [ |
| 112 | 254 | 'dropdown' => 0, |
| @@ -114,8 +256,9 @@ | ||
| 114 | 256 | 'number' => 0, |
| 115 | 257 | 'textarea' => 0, |
| 116 | 258 | 'url' => 0, |
| 117 | 259 | 'phone' => 0, |
| 260 | + 'input' => 0, | |
| 118 | 261 | ]; |
| 119 | 262 | |
| 120 | 263 | $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min'; |
| 121 | 264 | $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified'; |
| @@ -127,13 +270,18 @@ | ||
| 127 | 270 | ) { |
| 128 | 271 | $script_dep_blocks[ $block_name ] += 1; |
| 129 | 272 | $js_uri = SRFM_URL . 'assets/js/' . $dir_name . '/blocks/'; |
| 130 | 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 ); | |
| 131 | 278 | |
| 132 | - if ( 'phone' === $block_name | |
| 133 | - ) { | |
| 134 | - wp_enqueue_script( SRFM_SLUG . "-{$block_name}-intl-input-deps", $js_vendor_uri . 'intl/intTelInput.min.js', [], SRFM_VER, true ); | |
| 135 | - 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 | + ); | |
| 136 | 284 | } |
| 137 | 285 | |
| 138 | 286 | if ( 'dropdown' === $block_name ) { |
| 139 | 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. |
| @@ -146,30 +294,226 @@ | ||
| 146 | 294 | SRFM_URL . 'assets/build/formSubmit.js', |
| 147 | 295 | [ |
| 148 | 296 | 'srfm-tom-select', |
| 149 | 297 | 'srfm-dropdown', |
| 298 | + 'wp-api-fetch', | |
| 150 | 299 | ], |
| 151 | 300 | SRFM_VER, |
| 152 | 301 | true |
| 153 | 302 | ); |
| 303 | + } | |
| 154 | 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 | + | |
| 155 | 343 | wp_localize_script( |
| 156 | - SRFM_SLUG . '-form-submit', | |
| 157 | - SRFM_SLUG . '_submit', | |
| 344 | + SRFM_SLUG . '-textarea', | |
| 345 | + 'srfm_quill_i18n', | |
| 158 | 346 | [ |
| 159 | - 'site_url' => site_url(), | |
| 160 | - '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' ), | |
| 161 | 359 | ] |
| 162 | 360 | ); |
| 163 | 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 | + } | |
| 164 | 373 | |
| 165 | - if ( 'dropdown' !== $block_name ) { | |
| 166 | - wp_enqueue_script( SRFM_SLUG . "-{$block_name}", $js_uri . $block_name . $file_prefix . '.js', [], SRFM_VER, true ); | |
| 167 | - } | |
| 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 | + ); | |
| 384 | + | |
| 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 | + ); | |
| 168 | 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 | + ); | |
| 169 | 433 | } |
| 170 | 434 | |
| 171 | 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 | + /** | |
| 172 | 516 | * Render function. |
| 173 | 517 | * |
| 174 | 518 | * @param string $block_content Entire Block Content. |
| 175 | 519 | * @param array<string> $block Block Properties As An Array. |
| @@ -176,10 +520,17 @@ | ||
| 176 | 520 | * @return string |
| 177 | 521 | */ |
| 178 | 522 | public function generate_render_script( $block_content, $block ) { |
| 179 | 523 | |
| 524 | + if ( isset( $block['attrs']['isEditing'] ) ) { | |
| 525 | + // Only load block assets on the frontend. | |
| 526 | + return $block_content; | |
| 527 | + } | |
| 528 | + | |
| 180 | 529 | if ( isset( $block['blockName'] ) ) { |
| 181 | - 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 ); | |
| 182 | 533 | } |
| 183 | 534 | return $block_content; |
| 184 | 535 | } |
| 185 | 536 | |
| @@ -196,8 +547,10 @@ | ||
| 196 | 547 | // Bail if not SureForms post type. |
| 197 | 548 | return $template; |
| 198 | 549 | } |
| 199 | 550 | |
| 551 | + self::reset_printed_assets(); | |
| 552 | + | |
| 200 | 553 | $file_name = 'single-form.php'; |
| 201 | 554 | $template = locate_template( $file_name ); |
| 202 | 555 | |
| 203 | 556 | /** |
| @@ -205,7 +558,45 @@ | ||
| 205 | 558 | * |
| 206 | 559 | * @since 0.0.1 |
| 207 | 560 | */ |
| 208 | 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 = []; | |
| 209 | 600 | } |
| 210 | 601 | |
| 211 | 602 | } |