folder under assets/widgets/. * * @var array */ private static $free_widgets = array( 'offcanvas' => 'offcanvas', 'modal' => 'modal', 'content-switcher' => 'content-switcher', 'before-after' => 'before-after', 'progress' => 'progress', 'hotspots' => 'hotspots', 'comparison-table' => 'comparison-table', 'timeline' => 'timeline', 'marquee' => 'marquee', 'carousel' => 'carousel', 'smart-menu' => 'smart-menu', 'scroll-nav' => 'scroll-nav', 'multistep-form' => 'multistep-form', ); /** * Pro-only widget handle => folder under assets/widgets/. * * @var array */ private static $pro_widgets = array( 'dynamic-loop' => 'dynamic-loop', 'relationship-loop' => 'relationship-loop', 'repeater-loop' => 'repeater-loop', 'dynamic-carousel' => 'dynamic-carousel', 'dynamic-gallery' => 'dynamic-gallery', 'smart-filters' => 'smart-filters', 'live-search' => 'live-search', 'directory-map' => 'directory-map', 'dynamic-table' => 'dynamic-table', 'dynamic-mega-menu' => 'dynamic-mega-menu', 'product-grid' => 'product-grid', 'product-gallery' => 'product-gallery', 'related-products' => 'related-products', 'ajax-product-search' => 'ajax-product-search', 'quick-view' => 'quick-view', 'cart-drawer' => 'cart-drawer', 'sticky-add-to-cart' => 'sticky-add-to-cart', 'bought-together' => 'bought-together', 'product-compare' => 'product-compare', 'wishlist' => 'wishlist', 'variation-swatches' => 'variation-swatches', 'product-table' => 'product-table', 'recently-viewed' => 'recently-viewed', 'shipping-progress' => 'shipping-progress', 'form-summary' => 'form-summary', 'ai-concierge' => 'ai-concierge', ); /** * @var bool */ private static $hooks_added = false; /** * @return void */ public static function init() { if ( self::$hooks_added ) { return; } self::$hooks_added = true; add_action( 'wp_enqueue_scripts', array( __CLASS__, 'register_assets' ), 5 ); // Print our CSS after Elementor's atomic base styles (see print_after_elementor_base_styles()). add_action( 'wp_print_styles', array( __CLASS__, 'print_after_elementor_base_styles' ), 1 ); add_action( 'wp_footer', array( __CLASS__, 'print_after_elementor_base_styles' ), 1 ); add_action( 'elementor/frontend/after_register_scripts', array( __CLASS__, 'register_assets' ), 5 ); add_action( 'elementor/editor/after_enqueue_scripts', array( __CLASS__, 'register_assets' ), 5 ); // Atomic Editor V4 canvas / classic preview often skip per-widget depends — // force enabled widget assets into the preview iframe so carousel JS mounts. add_action( 'elementor/preview/enqueue_scripts', array( __CLASS__, 'enqueue_preview_assets' ), 20 ); add_action( 'elementor/preview/enqueue_styles', array( __CLASS__, 'enqueue_preview_assets' ), 20 ); add_action( 'elementor/editor/after_enqueue_scripts', array( __CLASS__, 'enqueue_preview_assets' ), 20 ); // Editor window: mirror PHP template context into the V4 client-side twig render. add_action( 'elementor/editor/after_enqueue_scripts', array( __CLASS__, 'enqueue_editor_render_context' ), 6 ); // Preview iframe: keep our defaults between Elementor's base styles and V4 classes / local styles. add_action( 'elementor/preview/enqueue_scripts', array( __CLASS__, 'enqueue_editor_cascade' ), 21 ); // Editor window: reload the document's global-class styles if Elementor's store lost them. add_action( 'elementor/editor/after_enqueue_scripts', array( __CLASS__, 'enqueue_editor_global_classes_guard' ), 20 ); } /** * Editor window only. On a slow server Elementor 4.3.x can reset its global-classes store after the * per-post styles request times out, so classes keep their names but lose their styles in the canvas. * The guard asks Elementor to reload the current document's classes (public addDocumentClasses), max 3 times. * * @return void */ public static function enqueue_editor_global_classes_guard() { $url = self::plugin_url(); if ( '' === $url ) { return; } $rel = 'assets/shared/editor-global-classes-guard.js'; wp_enqueue_script( 'reactwoo-atomic-editor-gc-guard', $url . $rel, array(), self::asset_file_version( $rel, self::plugin_version() ), true ); $debug = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ( defined( 'WP_DEBUG' ) && WP_DEBUG ); $guard = Script_Json::encode( array( 'debug' => $debug ) ); if ( '' !== $guard ) { wp_add_inline_script( 'reactwoo-atomic-editor-gc-guard', 'window.reactwooAtomicGcGuard = ' . $guard . ';', 'before' ); } } /** * Preview iframe only. The editor canvas appends Elementor's base styles, global classes and local * styles to the end of , after our stylesheets; this script keeps a copy of our stylesheets * right after the base styles so the cascade matches the live page (see editor-cascade.js). * * @return void */ public static function enqueue_editor_cascade() { $url = self::plugin_url(); if ( '' === $url ) { return; } $rel = 'assets/shared/editor-cascade.js'; wp_enqueue_script( 'reactwoo-atomic-editor-cascade', $url . $rel, array(), self::asset_file_version( $rel, self::plugin_version() ), true ); } /** * Editor window only. Elementor 4.3.1 renders Has_Element_Template twig in JS without * build_template_context()/Render_Context, so parent-derived values (trigger visibility, * ids, hotspot x/y, comparison cells...) were blank in the builder. This bridge supplies them. * * @return void */ public static function enqueue_editor_render_context() { $url = self::plugin_url(); if ( '' === $url ) { return; } $rel = 'assets/shared/editor-render-context.js'; $handle = 'reactwoo-atomic-editor-render-context'; $deps = wp_script_is( 'elementor-v2-editor-canvas', 'registered' ) ? array( 'elementor-v2-editor-canvas' ) : array(); wp_register_script( $handle, $url . $rel, $deps, self::asset_file_version( $rel, self::plugin_version() ), true ); $context = Script_Json::encode( array( 'closeLabel' => __( 'Close', 'reactwoo-atomic' ) ) ); if ( '' !== $context ) { wp_add_inline_script( $handle, 'window.reactwooAtomicEditorContext=' . $context . ';', 'before' ); } wp_enqueue_script( $handle ); } /** * Cascade order for V4: Elementor base styles < ReactWoo Atomic defaults < V4 classes / local styles. * * Our selectors are capped at the specificity of V4 class and local-style rules, (0,2,0). Elementor * prints its atomic base styles (`.elementor .{type}-base`, also (0,2,0), e.g. display:block on our * widget roots) after the plugin styles on the live site, so at equal specificity they would win. * Making our widget styles depend on the unconditional base-styles file prints them right after it * and before the global-class and local-style files, which then override our defaults by source * order. The editor canvas is handled by enqueue_editor_cascade(). * * @return void */ public static function print_after_elementor_base_styles() { $styles = wp_styles(); $base = array(); foreach ( $styles->registered as $handle => $dep ) { $src = is_string( $dep->src ) ? $dep->src : ''; $media = isset( $dep->args ) ? (string) $dep->args : ''; if ( 0 === strpos( (string) $handle, 'base-' ) && false !== strpos( $src, '/elementor/css/base-' ) && ( '' === $media || 'all' === $media ) ) { $base[] = (string) $handle; } } if ( empty( $base ) ) { return; } foreach ( $styles->registered as $handle => $dep ) { if ( 0 !== strpos( (string) $handle, 'reactwoo-atomic-' ) || 'reactwoo-atomic-admin' === $handle ) { continue; } foreach ( $base as $base_handle ) { if ( ! in_array( $base_handle, $dep->deps, true ) ) { $dep->deps[] = $base_handle; } } } } /** * @return string */ public static function plugin_url() { $url = \ReactWoo\Atomic\Runtime\Env::url(); return '' !== $url ? trailingslashit( $url ) : ''; } /** * @return string */ public static function plugin_version() { return \ReactWoo\Atomic\Runtime\Env::version(); } /** * Bust browser cache when an asset file changes (keeps plugin version prefix). * * @param string $relative Path under plugin root, e.g. assets/widgets/carousel/carousel.js. * @param string $fallback Fallback version. * @return string */ public static function asset_file_version( $relative, $fallback = '' ) { $base = \ReactWoo\Atomic\Runtime\Env::path(); $path = $base . ltrim( str_replace( '\\', '/', (string) $relative ), '/' ); $ver = '' !== $fallback ? $fallback : self::plugin_version(); if ( $base && is_readable( $path ) ) { return $ver . '.' . (string) filemtime( $path ); } return $ver; } /** * @return void */ public static function register_assets() { $url = self::plugin_url(); $ver = self::plugin_version(); if ( '' === $url ) { return; } $lifecycle = 'reactwoo-atomic-lifecycle'; wp_register_script( $lifecycle, $url . 'assets/shared/lifecycle.js', array(), self::asset_file_version( 'assets/shared/lifecycle.js', $ver ), true ); $tabs_rel = 'assets/shared/atomic-tabs.css'; wp_register_style( 'reactwoo-atomic-tabs', $url . $tabs_rel, array(), self::asset_file_version( $tabs_rel, $ver ) ); $leaf_rel = 'assets/shared/pro-leaf.css'; wp_register_style( 'reactwoo-atomic-pro-leaf', $url . $leaf_rel, array(), self::asset_file_version( $leaf_rel, $ver ) ); // Self-hosted Inter (Figma typeface). Only loads as a dependency of widget styles, so pages // without Atomic widgets never request it; the editor preview gets it via interaction. $inter_rel = 'assets/shared/inter.css'; wp_register_style( 'reactwoo-atomic-inter', $url . $inter_rel, array(), self::asset_file_version( $inter_rel, $ver ) ); // Shared hover/focus/active states for widget controls (beats the theme's button/link reset). // atomic-tabs.css (scoped to Atomic Tabs inside our widgets) rides along, so it loads only on // pages that render a ReactWoo Atomic widget. $interaction_rel = 'assets/shared/interaction-states.css'; wp_register_style( 'reactwoo-atomic-interaction', $url . $interaction_rel, array( 'reactwoo-atomic-inter', 'reactwoo-atomic-tabs' ), self::asset_file_version( $interaction_rel, $ver ) ); // Builder-only affordances (hidden-trigger ghosts, empty-view fixes). Enqueued for editor/preview only. $editor_canvas_rel = 'assets/shared/editor-canvas.css'; wp_register_style( 'reactwoo-atomic-editor-canvas', $url . $editor_canvas_rel, array( 'reactwoo-atomic-interaction' ), self::asset_file_version( $editor_canvas_rel, $ver ) ); $frame_rel = 'assets/shared/figma-frames.css'; wp_register_style( 'reactwoo-atomic-figma-frames', $url . $frame_rel, array( 'reactwoo-atomic-pro-leaf' ), self::asset_file_version( $frame_rel, $ver ) ); $map = self::$free_widgets; if ( Edition::is_pro() ) { $map = array_merge( $map, self::$pro_widgets ); } $folder_to_id = array(); foreach ( Catalog::all() as $item ) { if ( ! empty( $item['asset_folder'] ) && ! empty( $item['id'] ) ) { $folder_to_id[ (string) $item['asset_folder'] ] = (string) $item['id']; } } $always_enqueue = Frontend_Prefs::always_enqueue_assets(); // Pro: shared live-data view helper for the Figma-structured collection widgets // (Product Table, Dynamic Table, Dynamic Gallery). Not a widget folder of its own. $view_deps = array(); if ( Edition::is_pro() ) { $view_rel = 'assets/shared/collection-view.js'; wp_register_script( 'reactwoo-atomic-collection-view', $url . $view_rel, array( $lifecycle ), self::asset_file_version( $view_rel, $ver ), true ); $view_deps = array( 'product-table', 'dynamic-table', 'dynamic-gallery' ); } foreach ( $map as $handle_key => $folder ) { $element_id = isset( $folder_to_id[ $folder ] ) ? $folder_to_id[ $folder ] : ''; if ( '' !== $element_id && ! Enabled_Store::is_enabled( $element_id ) ) { continue; } $handle = 'reactwoo-atomic-' . $handle_key; $rel = 'assets/widgets/' . $folder . '/' . $folder; $base = $url . $rel; $css_ver = self::asset_file_version( $rel . '.css', $ver ); $js_ver = self::asset_file_version( $rel . '.js', $ver ); $css_deps = isset( self::$pro_widgets[ $handle_key ] ) ? array( 'reactwoo-atomic-figma-frames', 'reactwoo-atomic-interaction' ) : array( 'reactwoo-atomic-interaction' ); wp_register_style( $handle, $base . '.css', $css_deps, $css_ver ); $js_deps = in_array( $handle_key, $view_deps, true ) ? array( $lifecycle, 'reactwoo-atomic-collection-view' ) : array( $lifecycle ); // Pro: Multi-step Form branching / tracking / analytics hooks load before the Free script, // only while Pro features are licensed for this domain (0.8.0). if ( 'multistep-form' === $handle_key && Edition::pro_features_enabled() ) { $pro_rel = 'assets/widgets/multistep-form-pro/multistep-form-pro.js'; if ( '' !== \ReactWoo\Atomic\Runtime\Env::path() && is_readable( \ReactWoo\Atomic\Runtime\Env::path() . $pro_rel ) ) { wp_register_script( 'reactwoo-atomic-multistep-form-pro', $url . $pro_rel, array(), self::asset_file_version( $pro_rel, $ver ), true ); $js_deps[] = 'reactwoo-atomic-multistep-form-pro'; } } wp_register_script( $handle, $base . '.js', $js_deps, $js_ver, true ); // "Always" strategy enqueues every enabled element site-wide. // Conditional (default) only registers; Elementor enqueues via widget depends. if ( $always_enqueue && ! is_admin() ) { wp_enqueue_style( $handle ); wp_enqueue_script( $handle ); } } if ( Edition::is_pro() ) { self::localize_query_config( array( 'reactwoo-atomic-smart-filters', 'reactwoo-atomic-live-search', 'reactwoo-atomic-collection-view', ) ); self::localize_commerce_config( array( 'reactwoo-atomic-cart-drawer', 'reactwoo-atomic-ajax-product-search', 'reactwoo-atomic-sticky-add-to-cart', 'reactwoo-atomic-product-grid', 'reactwoo-atomic-product-gallery', 'reactwoo-atomic-shipping-progress', 'reactwoo-atomic-wishlist', 'reactwoo-atomic-product-compare', ) ); self::localize_ai_config( array( 'reactwoo-atomic-ai-concierge', ) ); self::localize_maps_config( array( 'reactwoo-atomic-directory-map' ) ); } } /** * Google Maps JavaScript API key from Elementor's own integration * (Elementor > Settings > Integrations > Google Maps, option `elementor_google_maps_api_key`). * Other integrations can supply one through the `reactwoo_atomic/google_maps_api_key` filter. * Never stored by ReactWoo Atomic. * * @return string Empty when no key is configured. */ public static function google_maps_key() { $key = (string) get_option( 'elementor_google_maps_api_key', '' ); $key = (string) apply_filters( 'reactwoo_atomic/google_maps_api_key', $key ); return trim( $key ); } /** * Where an admin adds the key (Elementor's Integrations tab). * * @return string */ public static function google_maps_settings_url() { if ( class_exists( '\\Elementor\\Settings' ) && method_exists( '\\Elementor\\Settings', 'get_settings_tab_url' ) ) { return (string) \Elementor\Settings::get_settings_tab_url( 'integrations' ); } return admin_url( 'admin.php?page=elementor-settings#tab-integrations' ); } /** * Directory Map runtime config. The key is a browser key by Google's design (restrict it by * HTTP referrer in Google Cloud); it is only printed when one is configured. * * @param array $handles Script handles. * @return void */ private static function localize_maps_config( array $handles ) { $key = self::google_maps_key(); $config = array( 'enabled' => '' !== $key, 'key' => $key, 'settingsUrl' => current_user_can( 'manage_options' ) ? esc_url_raw( self::google_maps_settings_url() ) : '', ); self::inline_config( $handles, 'reactwooAtomicDirectoryMap', $config ); } /** * Enqueue registered widget assets for Elementor editor / preview iframe. * * @return void */ public static function enqueue_preview_assets() { self::register_assets(); if ( wp_style_is( 'reactwoo-atomic-tabs', 'registered' ) ) { wp_enqueue_style( 'reactwoo-atomic-tabs' ); } if ( wp_style_is( 'reactwoo-atomic-figma-frames', 'registered' ) ) { wp_enqueue_style( 'reactwoo-atomic-figma-frames' ); } if ( wp_style_is( 'reactwoo-atomic-pro-leaf', 'registered' ) ) { wp_enqueue_style( 'reactwoo-atomic-pro-leaf' ); } if ( wp_style_is( 'reactwoo-atomic-interaction', 'registered' ) ) { wp_enqueue_style( 'reactwoo-atomic-interaction' ); } if ( wp_style_is( 'reactwoo-atomic-editor-canvas', 'registered' ) ) { wp_enqueue_style( 'reactwoo-atomic-editor-canvas' ); } if ( wp_script_is( 'reactwoo-atomic-lifecycle', 'registered' ) ) { wp_enqueue_script( 'reactwoo-atomic-lifecycle' ); } $map = self::$free_widgets; if ( Edition::is_pro() ) { $map = array_merge( $map, self::$pro_widgets ); } $folder_to_id = array(); foreach ( Catalog::all() as $item ) { if ( ! empty( $item['asset_folder'] ) && ! empty( $item['id'] ) ) { $folder_to_id[ (string) $item['asset_folder'] ] = (string) $item['id']; } } foreach ( $map as $handle_key => $folder ) { $element_id = isset( $folder_to_id[ $folder ] ) ? $folder_to_id[ $folder ] : ''; if ( '' !== $element_id && ! Enabled_Store::is_enabled( $element_id ) ) { continue; } $handle = 'reactwoo-atomic-' . $handle_key; if ( wp_style_is( $handle, 'registered' ) ) { wp_enqueue_style( $handle ); } if ( wp_script_is( $handle, 'registered' ) ) { wp_enqueue_script( $handle ); } } } /** * Shared REST config for Pro query widgets. * * @param array $handles Script handles. * @return void */ private static function localize_query_config( array $handles ) { $config = array( 'queryUrl' => esc_url_raw( rest_url( 'reactwoo-atomic/v1/query' ) ), 'collectionUrl' => esc_url_raw( rest_url( 'reactwoo-atomic/v1/collection' ) ), 'termsUrl' => esc_url_raw( rest_url( 'reactwoo-atomic/v1/terms' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), ); self::inline_config( $handles, 'reactwooAtomicQuery', $config ); } /** * Shared REST config for Pro commerce widgets. * * @param array $handles Script handles. * @return void */ private static function localize_commerce_config( array $handles ) { $base = 'reactwoo-atomic/v1'; $config = array( 'restBase' => esc_url_raw( rest_url( $base . '/' ) ), 'productsUrl' => esc_url_raw( rest_url( $base . '/commerce/products' ) ), 'cartUrl' => esc_url_raw( rest_url( $base . '/commerce/cart' ) ), 'addUrl' => esc_url_raw( rest_url( $base . '/commerce/cart/add' ) ), 'updateUrl' => esc_url_raw( rest_url( $base . '/commerce/cart/update' ) ), 'removeUrl' => esc_url_raw( rest_url( $base . '/commerce/cart/remove' ) ), 'wishlistUrl' => esc_url_raw( rest_url( $base . '/wishlist' ) ), 'compareUrl' => esc_url_raw( rest_url( $base . '/compare' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), ); if ( function_exists( 'get_woocommerce_currency_symbol' ) ) { $config['currency'] = array( 'symbol' => html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' ), 'position' => (string) get_option( 'woocommerce_currency_pos', 'left' ), 'decimals' => wc_get_price_decimals(), 'decimal' => wc_get_price_decimal_separator(), 'thousand' => wc_get_price_thousand_separator(), ); } self::inline_config( $handles, 'reactwooAtomicCommerce', $config ); } /** * Shared REST config for AI Concierge. * * @param array $handles Script handles. * @return void */ private static function localize_ai_config( array $handles ) { $base = 'reactwoo-atomic/v1'; $config = array( 'conciergeUrl' => esc_url_raw( rest_url( $base . '/ai/concierge' ) ), 'abilityUrl' => esc_url_raw( rest_url( $base . '/ai/ability' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), ); self::inline_config( $handles, 'reactwooAtomicAi', $config ); } /** * Assign a config object on a script handle with script-safe JSON. * * @param array $handles Script handles. * @param string $object_name Global name (identifier). * @param array $config Data. * @return void */ private static function inline_config( array $handles, $object_name, array $config ) { $object_name = (string) preg_replace( '/[^A-Za-z0-9_]/', '', (string) $object_name ); $json = Script_Json::encode( $config ); if ( '' === $object_name || '' === $json ) { return; } $script = 'var ' . $object_name . '=' . $json . ';'; foreach ( $handles as $handle ) { if ( wp_script_is( $handle, 'registered' ) ) { wp_add_inline_script( $handle, $script, 'before' ); } } } }