PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.2
ShopBuilder – WooCommerce Builder For Elementor v3.4.2
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
shopbuilder / app / Controllers / AssetRegistry.php

AssetRegistry.php in ShopBuilder – WooCommerce Builder For Elementor 3.4.2, at app/Controllers/AssetRegistry.php

1,407 lines 34.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class AssetRegistry
4 *
5 * Gathers and processes frontend JS and CSS assets based on active ShopBuilder modules.
6 *
7 * @package RadiusTheme\SB\Services
8 * @since 1.0.0
9 */
10
11 namespace RadiusTheme\SB\Controllers;
12
13 use RadiusTheme\SB\Helpers\Fns;
14 use RadiusTheme\SB\Helpers\BuilderFns;
15 use RadiusTheme\SB\Models\AssetBundler;
16 use RadiusTheme\SB\Traits\SingletonTrait;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * Class AssetRegistry
24 */
25 class AssetRegistry {
26 use SingletonTrait;
27
28 /**
29 * List of enabled module slugs.
30 *
31 * @var array
32 */
33 protected $enabled_modules = [];
34
35 /**
36 * List of enabled modules JS.
37 *
38 * @var array
39 */
40 protected $enabled_modules_js = [];
41
42 /**
43 * List of enabled modules CSS.
44 *
45 * @var array
46 */
47 protected $enabled_modules_css = [];
48
49 /**
50 * List of enabled Elementor widgets.
51 *
52 * @var array
53 */
54 protected $enabled_el_widgets = [];
55
56 /**
57 * List of enabled Elementor widgets.
58 *
59 * @var array
60 */
61 protected $enabled_widgets = [];
62
63 /**
64 * Full paths to JS assets.
65 *
66 * @var array
67 */
68 protected $js_files = [];
69
70 /**
71 * Full paths to CSS assets.
72 *
73 * @var array
74 */
75 protected $css_files = [];
76
77 /**
78 * Cached bundled JS asset.
79 *
80 * @var array|null
81 */
82 protected $bundled_js = null;
83
84 /**
85 * Cached bundled CSS asset.
86 *
87 * @var array|null
88 */
89 protected $bundled_css = null;
90
91 /**
92 * RTL suffix.
93 *
94 * @var string
95 */
96 private $suffix = '';
97
98 /**
99 * Directory path.
100 *
101 * @var string
102 */
103 private $dir;
104
105 /**
106 * Cached bundled assets.
107 *
108 * @var array|null
109 */
110 private $bundled_assets = null;
111
112 /**
113 * List of loaded asset types.
114 *
115 * @var array
116 */
117 private static $loaded_types = [];
118
119 /**
120 * List of loaded shared assets.
121 *
122 * @var array
123 */
124 private $shared_loaded = [];
125
126 /**
127 * Contextual loading.
128 *
129 * @var bool
130 */
131 private $contextual_loading;
132
133 /**
134 * Class constructor.
135 */
136 private function __construct() {
137 $this->enabled_modules = $this->get_enabled_modules();
138 $this->enabled_modules_js = $this->get_enabled_modules_js_with_context();
139 $this->enabled_modules_css = $this->get_enabled_modules_css_with_context();
140 $this->enabled_el_widgets = $this->get_enabled_el_widgets();
141 $this->enabled_widgets = $this->get_enabled_widgets_with_context();
142 $this->suffix = is_rtl() ? '-rtl' : '';
143 $this->dir = is_rtl() ? trailingslashit( 'rtl' ) : trailingslashit( 'css' );
144 $this->contextual_loading = Fns::is_contextual_loading();
145 }
146
147 /**
148 * Get bundled JS assets by context.
149 *
150 * @param bool $all Build every materialised context rather than only the one
151 * this request serves. Used by the manual regeneration path.
152 *
153 * @return array
154 */
155 public function get_bundled_js_by_context( $all = false ) {
156 $js_files_by_context = $this->get_contextual_assets( 'js' );
157 $bundled_contexts = [];
158 $wanted = $all ? array_keys( $js_files_by_context ) : $this->contexts_to_build( $js_files_by_context );
159
160 foreach ( $js_files_by_context as $context => $files ) {
161 if ( empty( $files ) || ! in_array( $context, $wanted, true ) ) {
162 continue;
163 }
164
165 $bundler = new AssetBundler( "rtsb-bundled-$context", $files, 'js' );
166 $src = $this->build_bundle( $bundler, $context );
167 $deps = [ 'jquery', 'rtsb-tipsy' ];
168
169 $builder_page_id = BuilderFns::builder_page_id_by_page();
170
171 if ( Fns::is_elementor_page( $builder_page_id ) ) {
172 $deps[] = 'imagesloaded';
173 }
174
175 $bundled_contexts[ $context ] = [
176 'handle' => "rtsb-bundled-$context",
177 'src' => $src,
178 'deps' => apply_filters( 'rtsb/optimizer/scripts/deps', $deps ),
179 'footer' => true,
180 ];
181 }
182
183 return $bundled_contexts;
184 }
185
186 /**
187 * Returns assets grouped by context with `global` inherited for each.
188 *
189 * @param string $type 'css' or 'js'.
190 *
191 * @return array
192 */
193 public function get_contextual_assets( $type ) {
194 $all_contexts = 'js' === $type ? $this->get_scripts_by_context() : $this->get_styles_by_context();
195
196 $normalized = array_map(
197 function ( $paths ) {
198 return array_unique( $paths );
199 },
200 $all_contexts
201 );
202
203 foreach ( $normalized as $context => $paths ) {
204 if ( 'global' === $context ) {
205 continue;
206 }
207
208 $normalized[ $context ] = array_unique(
209 array_merge(
210 $normalized['global'] ?? [],
211 $paths
212 )
213 );
214 }
215
216 return $normalized;
217 }
218
219 /**
220 * Returns scripts grouped by context.
221 *
222 * @return array
223 */
224 public function get_scripts_by_context() {
225 $context_scripts = [];
226
227 // Add core-init globally.
228 $core_path = $this->context()->get_assets_path( 'js/frontend/core-init.js' );
229 $context_scripts['global'][] = $core_path;
230
231 $shared_registry = apply_filters(
232 'rtsb/optimizer/scripts/shared/components',
233 [
234 'coupon' => [
235 'path' => rtsb()->get_assets_path( 'js/modules/shared/coupon.js' ),
236 'modules' => [ 'shopify-checkout', 'quick-checkout' ],
237 'widgets' => [ 'cart-coupon-form', 'coupon-form' ],
238 'context' => Fns::is_module_active( 'quick_checkout' ) ? [ 'global' ] : [ 'cart', 'checkout' ],
239 ],
240 'quantity' => [
241 'path' => rtsb()->get_assets_path( 'js/modules/shared/cart-quantity-handler.js' ),
242 'modules' => [ 'sticky-add-to-cart' ],
243 'widgets' => [ 'cart-table', 'product-add-to-cart' ],
244 'context' => Fns::is_module_active( 'quick_view' ) ? [ 'global' ] : [ 'product', 'cart' ],
245 ],
246 ]
247 );
248
249 $shared_loaded = [];
250
251 // Load module-based shared scripts.
252 foreach ( $this->enabled_modules_js as $module_slug => $module ) {
253 if ( ! $module['enabled'] ) {
254 continue;
255 }
256
257 foreach ( $shared_registry as $key => $data ) {
258 if ( in_array( $module_slug, $data['modules'], true ) ) {
259 $shared_loaded[ $key ] = $data;
260 }
261 }
262
263 $module_slug = apply_filters( 'rtsb/optimizer/module/remap', $module_slug );
264 $path = Fns::locate_asset( "js/modules/$module_slug.js" );
265
266 if ( $path ) {
267 $category = ! empty( $module['category'] ) ? $module['category'] : [ 'global' ];
268
269 foreach ( $category as $cat ) {
270 $context_scripts[ $cat ][] = $path;
271 }
272 }
273 }
274
275 // Elementor-specific widget scripts.
276 if ( defined( 'ELEMENTOR_VERSION' ) && Fns::should_load_elementor_scripts() ) {
277 foreach ( $this->enabled_widgets as $slug => $widget ) {
278 if ( ! $widget['enabled'] ) {
279 continue;
280 }
281
282 foreach ( $shared_registry as $key => $data ) {
283 if ( isset( $shared_loaded[ $key ] ) ) {
284 continue;
285 }
286
287 if ( in_array( $slug, $data['widgets'], true ) ) {
288 $shared_loaded[ $key ] = $data;
289 }
290 }
291
292 $path = Fns::locate_asset( "js/frontend/elementor/$slug.js" );
293
294 if ( $path ) {
295 $category = ! empty( $widget['category'] ) ? $widget['category'] : 'global';
296
297 if ( 'general' === $category ) {
298 $category = 'global';
299 }
300
301 $context_scripts[ $category ][] = $path;
302 }
303 }
304
305 $core_el = Fns::locate_asset( 'js/frontend/elementor/el-frontend-core.js' );
306
307 if ( $core_el ) {
308 $context_scripts['global'][] = $core_el;
309 }
310 }
311
312 foreach ( $shared_loaded as $data ) {
313 if ( empty( $data['path'] ) ) {
314 continue;
315 }
316
317 $contexts = $data['context'] ?? [ 'global' ];
318
319 foreach ( (array) $contexts as $ctx ) {
320 $context_scripts[ $ctx ][] = $data['path'];
321 }
322 }
323
324 $context_scripts = apply_filters(
325 'rtsb/optimizer/scripts/shared',
326 $context_scripts,
327 $this->enabled_modules,
328 $this->enabled_widgets
329 );
330 return apply_filters( 'rtsb/optimizer/scripts', $context_scripts );
331 }
332
333 /**
334 * Get structured JS scripts.
335 *
336 * @return array[]
337 */
338 public function get_scripts() {
339 $scripts = [];
340 $shared_scripts = [];
341 $shared_loaded = [];
342
343 // Load core.
344 $scripts[] = $this->context()->get_assets_path( 'js/frontend/core-init.js' );
345
346 $shared_registry = apply_filters(
347 'rtsb/optimizer/scripts/shared/components',
348 [
349 'coupon' => [
350 'path' => rtsb()->get_assets_path( 'js/modules/shared/coupon.js' ),
351 'modules' => [ 'shopify-checkout', 'quick-checkout' ],
352 'widgets' => [ 'cart-coupon-form', 'coupon-form' ],
353 ],
354 'quantity' => [
355 'path' => rtsb()->get_assets_path( 'js/modules/shared/cart-quantity-handler.js' ),
356 'modules' => [ 'sticky-add-to-cart' ],
357 'widgets' => [ 'cart-table', 'product-add-to-cart' ],
358 ],
359 ]
360 );
361
362 foreach ( $this->enabled_modules as $module ) {
363 foreach ( $shared_registry as $key => $data ) {
364 if ( in_array( $module, $data['modules'], true ) ) {
365 $shared_loaded[ $key ] = true;
366 }
367 }
368
369 $module = apply_filters( 'rtsb/optimizer/module/remap', $module );
370 $path = Fns::locate_asset( "js/modules/$module.js" );
371
372 if ( $path ) {
373 $scripts[] = $path;
374 }
375 }
376
377 if ( defined( 'ELEMENTOR_VERSION' ) && Fns::should_load_elementor_scripts() ) {
378 foreach ( $this->enabled_el_widgets as $widget ) {
379 foreach ( $shared_registry as $key => $data ) {
380 if ( isset( $shared_loaded[ $key ] ) ) {
381 continue;
382 }
383
384 if ( in_array( $widget, $data['widgets'], true ) ) {
385 $shared_loaded[ $key ] = true;
386 }
387 }
388
389 $path = Fns::locate_asset( "js/frontend/elementor/$widget.js" );
390
391 if ( $path ) {
392 $scripts[] = $path;
393 }
394 }
395
396 $scripts[] = Fns::locate_asset( 'js/frontend/elementor/el-frontend-core.js' );
397 }
398
399 foreach ( $shared_loaded as $key => $_ ) {
400 if ( isset( $shared_registry[ $key ]['path'] ) ) {
401 $shared_scripts[] = $shared_registry[ $key ]['path'];
402 }
403 }
404
405 $shared_scripts = apply_filters(
406 'rtsb/optimizer/scripts/shared',
407 $shared_scripts,
408 $this->enabled_modules,
409 $this->enabled_el_widgets
410 );
411
412 $scripts = array_merge( $scripts, array_unique( $shared_scripts ) );
413
414 return array_unique( apply_filters( 'rtsb/optimizer/scripts', $scripts ) );
415 }
416
417 /**
418 * Get structured CSS styles.
419 *
420 * @return array[]
421 */
422 public function get_styles() {
423 $elementor_styles = defined( 'ELEMENTOR_VERSION' ) && Fns::should_load_elementor_scripts()
424 ? $this->get_elementor_styles() : [];
425
426 return apply_filters(
427 'rtsb/optimizer/styles',
428 array_merge(
429 $this->get_base_styles(),
430 $this->get_module_styles(),
431 $elementor_styles
432 )
433 );
434 }
435
436 /**
437 * Get structured CSS styles.
438 *
439 * @return array[]
440 */
441 public function get_styles_by_context() {
442 $core = $this->get_base_styles_by_context();
443 $modules = $this->get_module_styles_by_context();
444 $elementor = defined( 'ELEMENTOR_VERSION' ) && Fns::should_load_elementor_scripts() ? $this->get_elementor_styles_by_context() : [];
445
446 $merged = [];
447
448 foreach ( [ $core, $modules, $elementor ] as $group ) {
449 foreach ( $group as $context => $paths ) {
450 foreach ( $paths as $file ) {
451 $merged[ $context ][] = $file;
452 }
453 }
454 }
455
456 return apply_filters( 'rtsb/optimizer/styles/by_context', $merged );
457 }
458
459 /**
460 * Get base styles.
461 *
462 * @return array
463 */
464 protected function get_base_styles() {
465 return apply_filters(
466 'rtsb/optimizer/styles/base',
467 [
468 rtsb()->get_assets_path( 'css/frontend/rtsb-fonts.css' ),
469 Fns::locate_asset( $this->dir . 'frontend/frontend-core' . $this->suffix . '.css' ),
470 ]
471 );
472 }
473
474 /**
475 * Get base styles.
476 *
477 * @return array
478 */
479 protected function get_base_styles_by_context() {
480 $styles = array_filter(
481 [
482 rtsb()->get_assets_path( 'css/frontend/rtsb-fonts.css' ),
483 Fns::locate_asset( $this->dir . 'frontend/frontend-core' . $this->suffix . '.css' ),
484 ]
485 );
486
487 return [
488 'global' => $styles,
489 ];
490 }
491
492 /**
493 * Get module styles.
494 *
495 * @return array
496 */
497 protected function get_module_styles() {
498 $styles = [];
499
500 if ( empty( $this->enabled_modules ) ) {
501 return $styles;
502 }
503
504 $styles = array_merge( $styles, $this->get_shared_module_styles() );
505
506 foreach ( $this->enabled_modules as $module ) {
507 $module = apply_filters( 'rtsb/optimizer/module/remap', $module );
508 $path = Fns::locate_asset( "{$this->dir}modules/$module$this->suffix.css" );
509
510 if ( $path ) {
511 $styles[] = $path;
512 }
513 }
514
515 $styles = apply_filters( 'rtsb/optimizer/styles/module', $styles );
516
517 return array_unique( $styles );
518 }
519
520 /**
521 * Get module styles.
522 *
523 * @return array
524 */
525 protected function get_module_styles_by_context() {
526 $content_styles = [];
527
528 if ( empty( $this->enabled_modules_css ) ) {
529 return $content_styles;
530 }
531
532 $content_styles = array_merge( $content_styles, $this->get_shared_module_styles() );
533
534 foreach ( $this->enabled_modules_css as $module_slug => $module ) {
535 if ( ! $module['enabled'] ) {
536 continue;
537 }
538
539 $module_slug = apply_filters( 'rtsb/optimizer/module/remap', $module_slug );
540 $path = Fns::locate_asset( "{$this->dir}modules/$module_slug$this->suffix.css" );
541
542 if ( $path ) {
543 $category = ! empty( $module['category'] ) ? $module['category'] : [ 'global' ];
544
545 foreach ( $category as $cat ) {
546 $content_styles[ $cat ][] = $path;
547 }
548 }
549 }
550
551 return apply_filters( 'rtsb/optimizer/styles/module', $content_styles );
552 }
553
554 /**
555 * Get Elementor styles.
556 *
557 * @return array
558 */
559 protected function get_elementor_styles() {
560 $styles = [ Fns::locate_asset( $this->dir . 'frontend/elementor/el-frontend-core' . $this->suffix . '.css' ) ];
561 $shared = $this->contextual_loading ? $this->get_elementor_shared_widget_styles_by_context() : $this->get_elementor_shared_widget_styles();
562 $styles = array_merge( $styles, $shared );
563
564 foreach ( $this->enabled_el_widgets as $widget ) {
565 $path = Fns::locate_asset( "{$this->dir}frontend/elementor/$widget$this->suffix.css" );
566
567 if ( $path ) {
568 $styles[] = $path;
569 }
570 }
571
572 return array_unique( $styles );
573 }
574
575 /**
576 * Get Elementor styles.
577 *
578 * @return array
579 */
580 protected function get_elementor_styles_by_context() {
581 $styles = [];
582
583 $styles['global'] = [
584 Fns::locate_asset( $this->dir . 'frontend/elementor/el-frontend-core' . $this->suffix . '.css' ),
585 ];
586
587 $shared_styles = $this->get_elementor_shared_widget_styles_by_context();
588
589 foreach ( $shared_styles as $context => $context_styles ) {
590 foreach ( (array) $context_styles as $style ) {
591 $styles[ $context ][] = $style;
592 }
593 }
594
595 foreach ( $this->enabled_widgets as $widget_slug => $info ) {
596 if ( empty( $info['enabled'] ) ) {
597 continue;
598 }
599
600 $contexts = $info['category'] ?? [ 'global' ];
601
602 foreach ( (array) $contexts as $ctx ) {
603 if ( 'general' === $ctx ) {
604 $ctx = 'global';
605 }
606
607 $path = Fns::locate_asset( "{$this->dir}frontend/elementor/{$widget_slug}{$this->suffix}.css" );
608
609 if ( $path ) {
610 $styles[ $ctx ][] = $path;
611 }
612 }
613 }
614
615 return $styles;
616 }
617
618 /**
619 * Get shared styles based on enabled modules.
620 *
621 * @return array
622 */
623 protected function get_shared_module_styles() {
624 $shared_styles_map = apply_filters(
625 'rtsb/optimizer/styles/modules/shared',
626 [
627 [
628 'modules' => [ 'wishlist', 'compare' ],
629 'file_name' => 'shared/module-buttons',
630 'context' => [ 'global' ],
631 ],
632 [
633 'modules' => [ 'quick-view', 'product-size-chart', 'quick-checkout' ],
634 'file_name' => 'shared/modal-layouts',
635 'context' => [ 'global' ],
636 ],
637 ]
638 );
639
640 // Add global shared.
641 $shared_by_components = $this->get_shared_style_components();
642
643 if ( ! $this->contextual_loading ) {
644 foreach ( $shared_by_components as $key => $data ) {
645 if ( ! empty( $data['modules'] ) && array_intersect( $data['modules'], $this->enabled_modules ) ) {
646 $shared_styles_map[] = [
647 'modules' => $data['modules'],
648 'file_name' => $data['file_name'],
649 ];
650 $this->shared_loaded[ $key ] = true;
651 }
652 }
653
654 $styles = [];
655
656 if ( empty( $this->enabled_modules ) ) {
657 return $styles;
658 }
659
660 foreach ( $shared_styles_map as $group ) {
661 if ( array_intersect( $group['modules'], $this->enabled_modules ) ) {
662 $styles[] = Fns::locate_asset( "{$this->dir}modules/{$group['file_name']}{$this->suffix}.css" );
663 }
664 }
665
666 return array_unique( $styles );
667 }
668
669 $enabled_widget_slugs = array_keys(
670 array_filter( $this->enabled_widgets, fn( $w ) => ! empty( $w['enabled'] ) )
671 );
672
673 $enabled_module_slugs = array_keys(
674 array_filter( $this->enabled_modules_css ?? [], fn( $m ) => ! empty( $m['enabled'] ) )
675 );
676
677 foreach ( $shared_by_components as $key => $data ) {
678 $modules = $data['modules'] ?? [];
679 $widgets = $data['widgets'] ?? [];
680
681 if (
682 array_intersect( $modules, $enabled_module_slugs ) ||
683 array_intersect( $widgets, $enabled_widget_slugs )
684 ) {
685 $shared_styles_map[] = [
686 'modules' => $modules,
687 'widgets' => $widgets,
688 'file_name' => $data['file_name'],
689 'context' => $data['context'] ?? [ 'global' ],
690 ];
691 $this->shared_loaded[ $key ] = true;
692 }
693 }
694
695 $styles = [];
696
697 foreach ( $shared_styles_map as $group ) {
698 $modules = $group['modules'] ?? [];
699 $widgets = $group['widgets'] ?? [];
700
701 $has_enabled_module = array_intersect( $modules, $enabled_module_slugs );
702 $has_enabled_widget = array_intersect( $widgets, $enabled_widget_slugs );
703
704 if ( ! $has_enabled_module && ! $has_enabled_widget ) {
705 continue;
706 }
707
708 foreach ( (array) $group['context'] as $ctx ) {
709 $path = Fns::locate_asset( "{$this->dir}modules/{$group['file_name']}{$this->suffix}.css" );
710
711 if ( $path ) {
712 $styles[ $ctx ][] = $path;
713 }
714 }
715 }
716
717 return $styles;
718 }
719
720 /**
721 * Get shared Elementor widget styles based on enabled widgets.
722 *
723 * @return array
724 */
725 protected function get_elementor_shared_widget_styles() {
726 $shared_styles_map = $this->get_elementor_shared_styles_map();
727
728 // Add global shared.
729 $shared_by_components = $this->get_shared_style_components();
730 $styles = [];
731
732 foreach ( $shared_by_components as $key => $data ) {
733 if ( isset( $this->shared_loaded[ $key ] ) ) {
734 continue;
735 }
736
737 if ( ! empty( $data['widgets'] ) && array_intersect( $data['widgets'], $this->enabled_el_widgets ) ) {
738 $path = Fns::locate_asset( "{$this->dir}modules/{$data['file_name']}{$this->suffix}.css" );
739
740 if ( $path ) {
741 $styles[] = $path;
742 }
743 }
744 }
745
746 foreach ( $shared_styles_map as $group ) {
747 if ( array_intersect( $group['widgets'], $this->enabled_el_widgets ) ) {
748 $styles[] = Fns::locate_asset( "{$this->dir}frontend/elementor/{$group['file_name']}$this->suffix.css" );
749 }
750 }
751
752 return array_unique( $styles );
753 }
754
755 /**
756 * Get shared Elementor widget styles based on enabled widgets.
757 *
758 * @return array
759 */
760 protected function get_elementor_shared_widget_styles_by_context() {
761 $shared_styles_map = $this->get_elementor_shared_styles_map();
762
763 // Add global shared.
764 $shared_by_components = $this->get_shared_style_components();
765 $styles = [];
766
767 $enabled_widget_slugs = array_keys(
768 array_filter(
769 $this->enabled_widgets ?? [],
770 fn( $widget ) => ! empty( $widget['enabled'] )
771 )
772 );
773
774 foreach ( $shared_by_components as $key => $data ) {
775 if ( isset( $this->shared_loaded[ $key ] ) ) {
776 continue;
777 }
778
779 if (
780 ! empty( $data['widgets'] ) &&
781 array_intersect( $data['widgets'], $enabled_widget_slugs )
782 ) {
783 $contexts = $data['context'] ?? [ 'global' ];
784
785 foreach ( (array) $contexts as $ctx ) {
786 if ( 'general' === $ctx ) {
787 $ctx = 'global';
788 }
789
790 $path = Fns::locate_asset( "{$this->dir}modules/{$data['file_name']}{$this->suffix}.css" );
791
792 if ( $path ) {
793 $styles[ $ctx ][] = $path;
794 }
795 }
796
797 $this->shared_loaded[ $key ] = true;
798 }
799 }
800
801 foreach ( $shared_styles_map as $group ) {
802 if (
803 ! empty( $group['widgets'] ) &&
804 array_intersect( $group['widgets'], $enabled_widget_slugs )
805 ) {
806 $contexts = $group['context'] ?? [ 'global' ];
807
808 if ( ! is_array( $contexts ) ) {
809 $contexts = [ $contexts ];
810 }
811
812 foreach ( $contexts as $ctx ) {
813 $path = Fns::locate_asset( "{$this->dir}frontend/elementor/{$group['file_name']}{$this->suffix}.css" );
814
815 if ( $path ) {
816 $styles[ $ctx ][] = $path;
817 }
818 }
819 }
820 }
821
822 return $styles;
823 }
824
825 /**
826 * Get Elementor shared styles map.
827 *
828 * @return array
829 */
830 private function get_elementor_shared_styles_map() {
831 return apply_filters(
832 'rtsb/optimizer/styles/elementor/shared',
833 [
834 [
835 'widgets' => [
836 'products-grid',
837 'products-list',
838 'products-slider',
839 'products-single-cateogory',
840 'product-cateogories',
841 'products-archive-catalog-custom',
842 'advanced-heading',
843 'dropcaps',
844 'shopbuilder-button',
845 'shopbuilder-cta',
846 'shopbuilder-info-box',
847 'shopbuilder-flip-box',
848 'shopbuilder-pricing-table',
849 'shopbuilder-counter',
850 'shopbuilder-countdown',
851 'shopbuilder-progress-bar',
852 'image-accordion',
853 'shopbuilder-faq',
854 'logo-slider-and-grid',
855 'testimonial',
856 'team-member',
857 'post-grid',
858 'post-list',
859 'upsells-product-custom',
860 'upsells-products-slider-custom',
861 'related-product-custom',
862 'related-products-slider-custom',
863 'cross-sells-custom',
864 'cross-sells-custom-slider',
865 ],
866 'file_name' => 'shared/el-grid',
867 'context' => [ 'global' ],
868 ],
869 [
870 'widgets' => [
871 'shopbuilder-button',
872 'shopbuilder-cta',
873 'shopbuilder-info-box',
874 'shopbuilder-flip-box',
875 'shopbuilder-pricing-table',
876 ],
877 'file_name' => 'shared/shopbuilder-button',
878 'context' => [ 'global' ],
879 ],
880 [
881 'widgets' => [
882 'products-grid',
883 'products-list',
884 'products-slider',
885 'products-single-cateogory',
886 'product-cateogories',
887 'products-archive-catalog-custom',
888 'upsells-product-custom',
889 'upsells-products-slider-custom',
890 'related-product-custom',
891 'related-products-slider-custom',
892 'cross-sells-custom',
893 'cross-sells-custom-slider',
894 ],
895 'file_name' => 'shared/el-general-product',
896 'context' => [ 'global' ],
897 ],
898 [
899 'widgets' => [
900 'products-grid',
901 'products-slider',
902 'products-archive-catalog-custom',
903 ],
904 'file_name' => 'shared/products-grid-slider',
905 'context' => [ 'global' ],
906 ],
907 [
908 'widgets' => [
909 'products-list',
910 'products-archive-catalog-custom',
911 ],
912 'file_name' => 'shared/products-list',
913 'context' => [ 'global' ],
914 ],
915 [
916 'widgets' => [ 'post-grid', 'post-list' ],
917 'file_name' => 'shared/post',
918 'context' => [ 'global' ],
919 ],
920 [
921 'widgets' => [
922 'products-archive-catalog',
923 'products-archive-catalog-custom',
924 ],
925 'file_name' => 'shared/products-archive',
926 'context' => [ 'shop' ],
927 ],
928 [
929 'widgets' => [
930 'product-title',
931 'product-description',
932 'product-short-description',
933 'product-images',
934 'product-onsale',
935 'product-additional-info',
936 'product-price',
937 'product-meta',
938 'product-categories',
939 'product-rating',
940 'product-tags',
941 'product-sku',
942 'product-stock',
943 'actions-button',
944 'product-add-to-cart',
945 'product-share',
946 'product-tabs',
947 'product-reviews',
948 'upsells-product',
949 'related-product',
950 ],
951 'file_name' => 'shared/single-product',
952 'context' => [ 'product' ],
953 ],
954 [
955 'widgets' => [
956 'cart-table',
957 'cart-totals',
958 'cross-sells',
959 ],
960 'file_name' => 'shared/cart',
961 'context' => [ 'cart' ],
962 ],
963 ]
964 );
965 }
966
967 /**
968 * Get enabled modules. Filterable.
969 *
970 * @return array
971 */
972 protected function get_enabled_modules() {
973 $module_list = Fns::get_modules_list();
974 $enabled = [];
975
976 foreach ( $module_list as $slug => $info ) {
977 if ( defined( 'RTWPVG_VERSION' ) && 'variation_gallery' === $slug ) {
978 continue;
979 }
980 if ( defined( 'RTWPVS_VERSION' ) && 'variation_swatches' === $slug ) {
981 continue;
982 }
983 if ( ! empty( $info['active'] ) ) {
984 $enabled[] = str_replace( '_', '-', sanitize_key( $slug ) );
985 }
986 }
987
988 return $enabled;
989 }
990
991 /**
992 * Get enabled modules JS with context.
993 *
994 * @return array
995 */
996 protected function get_enabled_modules_js_with_context() {
997 $module_list = Fns::get_modules_list();
998 $modules = [];
999 $context = apply_filters(
1000 'rtsb/optimizer/modules/js_context',
1001 [
1002 'checkout_fields_editor' => [ 'checkout' ],
1003 'shopify_checkout' => [ 'checkout' ],
1004 ]
1005 );
1006
1007 foreach ( $module_list as $slug => $info ) {
1008 if ( defined( 'RTWPVG_VERSION' ) && 'variation_gallery' === $slug ) {
1009 continue;
1010 }
1011 if ( defined( 'RTWPVS_VERSION' ) && 'variation_swatches' === $slug ) {
1012 continue;
1013 }
1014 $clean_slug = str_replace( '_', '-', sanitize_key( $slug ) );
1015 $is_enabled = isset( $info['active'] ) && 'on' === $info['active'];
1016
1017 $modules[ $clean_slug ] = [
1018 'enabled' => $is_enabled,
1019 'category' => ! empty( $context[ $slug ] ) ? $context[ $slug ] : [ 'global' ],
1020 ];
1021 }
1022
1023 return $modules;
1024 }
1025
1026 /**
1027 * Get enabled modules CSS with context.
1028 *
1029 * @return array
1030 */
1031 protected function get_enabled_modules_css_with_context() {
1032 $module_list = Fns::get_modules_list();
1033 $modules = [];
1034 $context = apply_filters(
1035 'rtsb/optimizer/modules/css_context',
1036 [
1037 'checkout_fields_editor' => [ 'checkout' ],
1038 'shopify_checkout' => [ 'checkout' ],
1039 ]
1040 );
1041
1042 foreach ( $module_list as $slug => $info ) {
1043 if ( defined( 'RTWPVG_VERSION' ) && 'variation_gallery' === $slug ) {
1044 continue;
1045 }
1046 if ( defined( 'RTWPVS_VERSION' ) && 'variation_swatches' === $slug ) {
1047 continue;
1048 }
1049 $clean_slug = str_replace( '_', '-', sanitize_key( $slug ) );
1050 $is_enabled = isset( $info['active'] ) && 'on' === $info['active'];
1051
1052 $modules[ $clean_slug ] = [
1053 'enabled' => $is_enabled,
1054 'category' => ! empty( $context[ $slug ] ) ? $context[ $slug ] : [ 'global' ],
1055 ];
1056 }
1057
1058 return $modules;
1059 }
1060
1061 /**
1062 * Get enabled modules.
1063 *
1064 * @return array
1065 */
1066 protected function get_enabled_el_widgets() {
1067 $widget_data = Fns::get_widgets_list();
1068 $enabled = [];
1069
1070 foreach ( $widget_data as $slug => $info ) {
1071 if ( isset( $info['active'] ) && 'on' === $info['active'] ) {
1072 $enabled[] = str_replace( '_', '-', sanitize_key( $slug ) );
1073 }
1074 }
1075
1076 return $enabled;
1077 }
1078
1079 /**
1080 * Get enabled widgets with categories.
1081 *
1082 * @return array
1083 */
1084 protected function get_enabled_widgets_with_context() {
1085 $widget_list = Fns::get_widgets_list();
1086 $widgets = [];
1087
1088 foreach ( $widget_list as $slug => $info ) {
1089 $clean_slug = str_replace( '_', '-', sanitize_key( $slug ) );
1090 $is_enabled = isset( $info['active'] ) && 'on' === $info['active'];
1091
1092 $widgets[ $clean_slug ] = [
1093 'enabled' => $is_enabled,
1094 'category' => ! empty( $widget_list[ $slug ]['category'] ) ? $widget_list[ $slug ]['category'] : 'global',
1095 ];
1096 }
1097
1098 return $widgets;
1099 }
1100
1101 /**
1102 * Populate JS/CSS file arrays based on active modules.
1103 *
1104 * @return void
1105 */
1106 protected function collect_assets() {
1107 static $ran = false;
1108
1109 if ( $ran ) {
1110 return;
1111 }
1112
1113 $ran = true;
1114
1115 $this->js_files = $this->contextual_loading ? $this->get_scripts_by_context() : array_unique( $this->get_scripts() );
1116 $this->css_files = $this->contextual_loading ? $this->get_styles_by_context() : array_unique( $this->get_styles() );
1117 }
1118
1119 /**
1120 * Build and return the final bundled and minified CSS and JS.
1121 *
1122 * @return array
1123 */
1124 public function get_bundled_assets() {
1125 if ( null !== $this->bundled_assets ) {
1126 return $this->bundled_assets;
1127 }
1128
1129 $this->collect_assets();
1130
1131 $this->bundled_assets = [
1132 'js' => $this->contextual_loading ? $this->get_bundled_js_by_context() : $this->get_bundled_js(),
1133 'css' => $this->contextual_loading ? $this->get_bundled_css_by_context() : $this->get_bundled_css(),
1134 ];
1135
1136 return $this->bundled_assets;
1137 }
1138
1139 /**
1140 * Load optimized assets and return them by type
1141 *
1142 * @param string $type 'css' or 'js'.
1143 *
1144 * @return array
1145 */
1146 public function load_optimized_assets( $type = 'css' ) {
1147 if ( isset( self::$loaded_types[ $type ] ) ) {
1148 return [];
1149 }
1150
1151 $assets = [];
1152 $bundled = $this->get_bundled_assets();
1153 $context = Fns::detect_context();
1154
1155 if ( $this->contextual_loading ) {
1156 if ( 'js' === $type && ! empty( $bundled['js'] ) ) {
1157 $ctx = isset( $bundled['js'][ $context ] ) ? $context : 'global';
1158 if ( $ctx ) {
1159 $assets = (array) $bundled['js'][ $ctx ];
1160 }
1161 }
1162 if ( 'css' === $type && ! empty( $bundled['css'] ) ) {
1163 $ctx = isset( $bundled['css'][ $context ] ) ? $context : 'global';
1164 if ( $ctx ) {
1165 $assets = (array) $bundled['css'][ $ctx ];
1166 }
1167 }
1168 } else {
1169 if ( 'js' === $type && ! empty( $bundled['js'] ) ) {
1170 $assets = (array) $this->bundled_assets['js'];
1171 }
1172
1173 if ( 'css' === $type && ! empty( $bundled['css'] ) ) {
1174 $assets = (array) $this->bundled_assets['css'];
1175 }
1176 }
1177
1178 self::$loaded_types[ $type ] = true;
1179
1180 return $assets;
1181 }
1182
1183 /**
1184 * Get bundled JS, with caching to prevent multiple builds.
1185 *
1186 * @return array|null
1187 */
1188 protected function get_bundled_js() {
1189 if ( null !== $this->bundled_js ) {
1190 return $this->bundled_js;
1191 }
1192
1193 if ( empty( $this->js_files ) ) {
1194 return null;
1195 }
1196
1197 $handle = 'rtsb-bundled';
1198 $bundler = new AssetBundler( $handle, $this->js_files, 'js' );
1199 $src = $this->build_bundle( $bundler );
1200 $deps = [ 'jquery', 'rtsb-tipsy' ];
1201
1202 $builder_page_id = BuilderFns::builder_page_id_by_page();
1203
1204 if ( Fns::is_elementor_page( $builder_page_id ) ) {
1205 $deps[] = 'imagesloaded';
1206 }
1207
1208 $this->bundled_js = [
1209 'handle' => $handle,
1210 'src' => $src,
1211 'deps' => apply_filters( 'rtsb/optimizer/scripts/deps', $deps ),
1212 'footer' => true,
1213 ];
1214
1215 return $this->bundled_js;
1216 }
1217
1218 /**
1219 * Get bundled CSS, with caching to prevent multiple builds.
1220 *
1221 * @return array|null
1222 */
1223 protected function get_bundled_css() {
1224 if ( null !== $this->bundled_css ) {
1225 return $this->bundled_css;
1226 }
1227
1228 if ( empty( $this->css_files ) ) {
1229 return null;
1230 }
1231
1232 $bundler = new AssetBundler( 'rtsb-bundled', $this->css_files, 'css' );
1233 $src = $this->build_bundle( $bundler );
1234
1235 $this->bundled_css = [
1236 'handle' => 'rtsb-bundled',
1237 'src' => $src,
1238 ];
1239
1240 return $this->bundled_css;
1241 }
1242
1243 /**
1244 * Get bundled CSS by context
1245 *
1246 * @param bool $all Build every materialised context rather than only the one
1247 * this request serves. Used by the manual regeneration path.
1248 *
1249 * @return array|null
1250 */
1251 public function get_bundled_css_by_context( $all = false ) {
1252 $css_files_by_context = $this->get_contextual_assets( 'css' );
1253 $bundled_contexts = [];
1254 $wanted = $all ? array_keys( $css_files_by_context ) : $this->contexts_to_build( $css_files_by_context );
1255
1256 foreach ( $css_files_by_context as $context => $files ) {
1257 if ( empty( $files ) || ! in_array( $context, $wanted, true ) ) {
1258 continue;
1259 }
1260
1261 $bundler = new AssetBundler( "rtsb-bundled-$context", $files, 'css' );
1262 $src = $this->build_bundle( $bundler, $context );
1263
1264 $bundled_contexts[ $context ] = [
1265 'handle' => "rtsb-bundled-$context",
1266 'src' => $src,
1267 ];
1268 }
1269
1270 return $bundled_contexts;
1271 }
1272
1273 /**
1274 * The context or contexts this request actually needs built.
1275 *
1276 * A request serves exactly one bundle per type: load_optimized_assets() and
1277 * enqueue_optimized_assets() both resolve the detected context, falling back
1278 * to `global` when it has no mapping of its own. Building the others produces
1279 * files this request will never reference.
1280 *
1281 * Only the selected context is returned, never context plus global: where a
1282 * contextual bundle exists, get_contextual_assets() has already merged the
1283 * global files into it, so global is not served alongside.
1284 *
1285 * Resolved separately for CSS and JS, because the two maps are built
1286 * independently and a context can be mapped for one type but not the other.
1287 *
1288 * @param array $map Context to file-list map for a single asset type.
1289 *
1290 * @return array List of context keys to build.
1291 */
1292 protected function contexts_to_build( array $map ) {
1293 $context = Fns::detect_context();
1294
1295 if ( isset( $map[ $context ] ) ) {
1296 return [ $context ];
1297 }
1298
1299 return isset( $map['global'] ) ? [ 'global' ] : [];
1300 }
1301
1302 /**
1303 * Build a bundle and report an unusable result to Fns.
1304 *
1305 * AssetBundler::build() returns an empty string when it cannot write a file,
1306 * for example when the cache directory is not writable or when the scoped
1307 * minifier is not loadable yet on the first request after an upgrade.
1308 * Registering a handle with an empty source makes WordPress print nothing at
1309 * all, so the request must fall back to the unbundled assets instead.
1310 *
1311 * @param AssetBundler $bundler Bundler instance.
1312 * @param string|null $context Context this bundle belongs to, null when not
1313 * using contextual loading.
1314 *
1315 * @return string Bundle URL, or an empty string on failure.
1316 */
1317 private function build_bundle( AssetBundler $bundler, $context = null ) {
1318 $src = $bundler->build();
1319
1320 if ( '' !== $src ) {
1321 return $src;
1322 }
1323
1324 // Only fall back when the bundle that would actually be served is the one
1325 // that failed. enqueue_optimized_assets() serves the detected context and
1326 // falls back to `global`, so a failure in any other context is harmless
1327 // and must not disable optimization for the whole request.
1328 if ( null === $context || 'global' === $context || Fns::detect_context() === $context ) {
1329 Fns::mark_optimization_unavailable();
1330 }
1331
1332 return $src;
1333 }
1334
1335 /**
1336 * Forcefully regenerate bundled JS and CSS assets.
1337 *
1338 * @return array
1339 */
1340 public function regenerate_bundles() {
1341 $this->bundled_js = null;
1342 $this->bundled_css = null;
1343 $this->js_files = [];
1344 $this->css_files = [];
1345
1346 $this->collect_assets();
1347
1348 // Explicitly eager: this is the manual "Purge Asset Cache" path, which is
1349 // expected to rebuild every materialised context rather than only the one
1350 // the current request happens to serve.
1351 return [
1352 'js' => $this->contextual_loading ? $this->get_bundled_js_by_context( true ) : $this->get_bundled_js(),
1353 'css' => $this->contextual_loading ? $this->get_bundled_css_by_context( true ) : $this->get_bundled_css(),
1354 ];
1355 }
1356
1357 /**
1358 * Get context.
1359 *
1360 * @return object
1361 */
1362 private function context() {
1363 return ( function_exists( 'rtsbpro' ) && rtsb()->has_pro() ) ? rtsbpro() : rtsb();
1364 }
1365
1366 /**
1367 * Get shared style mappings for modules and widgets.
1368 *
1369 * @return array
1370 */
1371 private function get_shared_style_components() {
1372 return apply_filters(
1373 'rtsb/optimizer/styles/shared/components',
1374 [
1375 'checkout' => [
1376 'file_name' => 'shared/checkout',
1377 'modules' => [ 'quick-checkout', 'shopify-checkout' ],
1378 'widgets' => [
1379 'billing-form',
1380 'shipping-form',
1381 'order-notes',
1382 'order-review',
1383 'checkout-payment',
1384 'coupon-form',
1385 'checkout-login-form',
1386 'shipping-method',
1387 ],
1388 'context' => [ 'global' ],
1389 ],
1390 'slider' => [
1391 'file_name' => 'shared/slider-core',
1392 'modules' => [ 'variation-gallery' ],
1393 'widgets' => [
1394 'products-slider',
1395 'team-member',
1396 'upsells-products-slider-custom',
1397 'related-products-slider-custom',
1398 'cross-sells-custom-slider',
1399 'hero-slider',
1400 ],
1401 'context' => [ 'global' ],
1402 ],
1403 ]
1404 );
1405 }
1406 }
1407