PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.2.2
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.2.2
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / inc / classes / assets-manager.php

assets-manager.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.2.2, at inc/classes/assets-manager.php

1,032 lines 34.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Master Addons - Assets Manager
5 *
6 * Central registry for all vendor libraries and asset registration.
7 * Handles dependency chains and provides organized asset management.
8 *
9 * @package MasterAddons\Inc\Classes
10 * @since 2.1.0
11 */
12
13 namespace MasterAddons\Inc\Classes;
14
15 if (!defined('ABSPATH')) {
16 exit;
17 }
18
19 class Assets_Manager
20 {
21 /**
22 * Singleton instance
23 */
24 private static $instance = null;
25
26 /**
27 * Vendor registry cache
28 */
29 private static $registry = null;
30
31 /**
32 * Get singleton instance
33 */
34 public static function get_instance()
35 {
36 if (null === self::$instance) {
37 self::$instance = new self();
38 }
39 return self::$instance;
40 }
41
42 /**
43 * Constructor
44 */
45 public function __construct()
46 {
47 // Register vendor assets early - both frontend and admin
48 add_action('wp_enqueue_scripts', [$this, 'register_all'], 5);
49 add_action('admin_enqueue_scripts', [$this, 'register_all'], 5);
50
51 // Also register in Elementor editor
52 add_action('elementor/editor/after_enqueue_scripts', [$this, 'register_all'], 5);
53 add_action('elementor/preview/enqueue_scripts', [$this, 'register_all'], 5);
54 }
55
56 /**
57 * Get vendor registry
58 *
59 * All vendor libraries with their file paths and dependencies.
60 * No 'name' key needed - the array key IS the identifier.
61 *
62 * @return array
63 */
64 public static function get_registry()
65 {
66 if (self::$registry !== null) {
67 return self::$registry;
68 }
69
70 self::$registry = [
71 // =========================================================
72 // TOOLTIP & POPOVER
73 // =========================================================
74 'popper' => [
75 'files' => [
76 'js' => 'popper.min.js',
77 ],
78 'deps' => [],
79 ],
80
81 'tippy' => [
82 'files' => [
83 'css' => 'tippyjs/css/tippy.css',
84 'js' => 'tippyjs/js/tippy.min.js',
85 ],
86 'deps' => ['popper'],
87 ],
88
89 // =========================================================
90 // LIGHTBOX & GALLERY
91 // =========================================================
92 'fancybox' => [
93 'files' => [
94 'css' => 'fancybox/jquery.fancybox.min.css',
95 'js' => 'fancybox/jquery.fancybox.min.js',
96 ],
97 'deps' => [],
98 ],
99
100 'gridder' => [
101 'files' => [
102 'css' => 'gridder/css/jquery.gridder.min.css',
103 'js' => 'gridder/js/jquery.gridder.min.js',
104 ],
105 'deps' => [],
106 ],
107
108 // =========================================================
109 // GRID & FILTERING
110 // =========================================================
111 'isotope' => [
112 'files' => [
113 'js' => 'isotope/isotope.js',
114 ],
115 'deps' => [],
116 ],
117
118 'macy' => [
119 'files' => [
120 'js' => 'macy/macy.js',
121 ],
122 'deps' => [],
123 ],
124
125 // =========================================================
126 // CAROUSEL & SLIDER
127 // =========================================================
128 'swiper' => [
129 'files' => [
130 'css' => 'swiper/css/swiper.min.css',
131 'js' => 'swiper/js/swiper-bundle.min.js',
132 ],
133 'deps' => [],
134 ],
135
136 'swiper-carousel' => [
137 'files' => [
138 'css' => 'common/swiper-carousel.min.css',
139 ],
140 'path' => 'css', // Override: assets/css/
141 'deps' => ['swiper'],
142 ],
143
144 'owl-carousel' => [
145 'files' => [
146 'css' => 'owlcarousel/owl.carousel.min.css',
147 'js' => 'owlcarousel/owl.carousel.min.js',
148 ],
149 'deps' => [],
150 'deprecated' => true,
151 ],
152
153 // =========================================================
154 // IMAGE EFFECTS
155 // =========================================================
156 'tilt' => [
157 'files' => [
158 'js' => 'tilt/tilt.jquery.min.js',
159 ],
160 'deps' => [],
161 ],
162
163 'twentytwenty' => [
164 'files' => [
165 'css' => 'twentytwenty/css/twentytwenty.css',
166 'js' => 'twentytwenty/js/jquery.twentytwenty.js',
167 ],
168 'deps' => ['jquery-event-move'],
169 ],
170
171 'jquery-event-move' => [
172 'files' => [
173 'js' => 'twentytwenty/js/jquery.event.move.js',
174 ],
175 'deps' => [],
176 ],
177
178 // =========================================================
179 // ANIMATION
180 // =========================================================
181 // Note: anime.js is now bundled via ES import in modules that need it
182 // reveal is now integrated into the reveal module with anime.js v4
183
184 'rellax' => [
185 'files' => [
186 'js' => 'rellax/rellax.min.js',
187 ],
188 'deps' => [],
189 ],
190
191 // =========================================================
192 // BACKGROUND & EFFECTS
193 // =========================================================
194 'vegas' => [
195 'files' => [
196 'css' => 'vegas/vegas.min.css',
197 'js' => 'vegas/vegas.min.js',
198 ],
199 'deps' => ['common'],
200 ],
201
202 'particles' => [
203 'files' => [
204 'js' => 'particles/particles.js',
205 ],
206 'deps' => [],
207 ],
208
209 // =========================================================
210 // COUNTDOWN & TIMER
211 // =========================================================
212 'countdown' => [
213 'files' => [
214 'js' => 'countdown/jquery.countdown.js',
215 ],
216 'deps' => [],
217 ],
218
219 // =========================================================
220 // PROGRESS & LOADING
221 // =========================================================
222 'loading-bar' => [
223 'files' => [
224 'js' => 'loading-bar/loading-bar.js',
225 ],
226 'deps' => [],
227 ],
228
229 // =========================================================
230 // SCROLLING
231 // =========================================================
232 'perfect-scrollbar' => [
233 'files' => [
234 'css' => 'perfect-scrollbar/perfect-scrollbar.min.css',
235 'js' => 'perfect-scrollbar/perfect-scrollbar.min.js',
236 ],
237 'deps' => [],
238 ],
239
240 // =========================================================
241 // LAZY LOADING
242 // =========================================================
243 'lazysizes' => [
244 'files' => [
245 'js' => 'lazysizes/lazysizes.min.js',
246 ],
247 'deps' => [],
248 ],
249
250 // =========================================================
251 // NEWS TICKER
252 // =========================================================
253 'jquery-rss' => [
254 'files' => [
255 'js' => 'newsticker/js/jquery.rss.min.js',
256 ],
257 'deps' => [],
258 ],
259
260 // =========================================================
261 // TABLE OF CONTENTS
262 // =========================================================
263 'table-of-content' => [
264 'files' => [
265 'js' => 'jltma-table-of-content/jltma-table-of-content.js',
266 ],
267 'deps' => [],
268 ],
269
270 // =========================================================
271 // TOGGLE CONTENT
272 // =========================================================
273 'toggle-content' => [
274 'files' => [
275 'js' => 'toggle-content/toggle-content.js',
276 ],
277 'deps' => [],
278 ],
279
280 // =========================================================
281 // ICON FONTS
282 // =========================================================
283 'elementor-icons' => [
284 'files' => [
285 'css' => 'elementor-icons.css',
286 ],
287 'path' => 'fonts/elementor-icon',
288 'deps' => [],
289 ],
290
291 // =========================================================
292 // COMMON/SHARED STYLES
293 // =========================================================
294 'common' => [
295 'files' => [
296 'css' => 'common/common.min.css',
297 ],
298 'path' => 'css',
299 'deps' => [],
300 ],
301
302 // =========================================================
303 // Animation Effects
304 // =========================================================
305 'animation' => [
306 'files' => [
307 'css' => 'common/animations.css',
308 ],
309 'path' => 'css',
310 'deps' => [],
311 ],
312
313 // =========================================================
314 // FORM & UI LIBRARIES
315 // =========================================================
316 'select2' => [
317 'files' => [
318 'css' => 'select2/select2.min.css',
319 'js' => 'select2/select2.min.js',
320 ],
321 'deps' => [],
322 ],
323
324 // =========================================================
325 // RECOMMENDED PLUGINS
326 // =========================================================
327 'recommended-plugins' => [
328 'files' => [
329 'css' => 'css/admin/recommended-plugins.css',
330 'js' => 'js/admin/recommended-plugins.js',
331 ],
332 'path' => '', // Root assets folder
333 'deps' => ['jquery'],
334 ],
335
336 // =========================================================
337 // ADMIN SDK
338 // =========================================================
339 'admin-sdk' => [
340 'files' => [
341 'css' => 'css/admin/master-addons-admin-sdk.css',
342 'js' => 'js/admin/master-addons-admin-sdk.js',
343 ],
344 'path' => '', // Root assets folder
345 'deps' => ['jquery'],
346 ],
347
348 // =========================================================
349 // ADMIN SETTINGS ASSETS
350 // =========================================================
351 'admin-settings' => [
352 'files' => [
353 'css' => 'css/admin/admin-settings.css',
354 'js' => 'js/admin/admin-settings.js',
355 ],
356 'path' => '', // Root assets folder
357 'deps' => ['react', 'react-dom', 'wp-element', 'wp-i18n', 'wp-api-fetch'],
358 ],
359
360 // =========================================================
361 // SETUP WIZARD ASSETS
362 // =========================================================
363 'setup-wizard' => [
364 'files' => [
365 'css' => 'css/admin/setup-wizard.css',
366 'js' => 'js/admin/setup-wizard.js',
367 ],
368 'path' => '', // Root assets folder
369 'deps' => ['react', 'react-dom', 'wp-element', 'wp-i18n', 'wp-api-fetch'],
370 ],
371
372 // =========================================================
373 // THEME BUILDER ASSETS
374 // =========================================================
375 'theme-builder' => [
376 'files' => [
377 'css' => 'css/admin/theme-builder.css',
378 'js' => 'js/admin/theme-builder.js',
379 ],
380 'path' => '', // Root assets folder
381 'deps' => ['select2'],
382 ],
383
384 // =========================================================
385 // COMMENTS BUILDER
386 // =========================================================
387 'comments' => [
388 'files' => [
389 'css' => 'css/theme-builder-comments.css',
390 'js' => 'js/theme-builder-comments.js',
391 ],
392 'path' => '', // Root assets folder
393 'deps' => [],
394 ],
395
396 // =========================================================
397 // POPUP BUILDER ASSETS
398 // =========================================================
399 'popup-builder-admin' => [
400 'files' => [
401 'css' => 'css/admin/popup-builder/popup-builder.css',
402 'js' => 'js/admin/popup-builder/popup-admin.js',
403 ],
404 'path' => '',
405 'deps' => ['select2'],
406 ],
407 'popup-builder-frontend' => [
408 'files' => [
409 'css' => 'css/admin/popup-builder/popup-frontend.css',
410 'js' => 'js/admin/popup-builder/modal-popup.js',
411 ],
412 'path' => '',
413 'deps' => ['animation'],
414 ],
415 'popup-builder-elementor' => [
416 'files' => [
417 'css' => 'css/admin/popup-builder/elementor-editor.css',
418 'js' => 'js/admin/popup-builder/elementor-editor.js',
419 ],
420 'path' => '',
421 'deps' => [],
422 ],
423
424 // =========================================================
425 // TEMPLATE LIBRARY & PAGE IMPORTER
426 // =========================================================
427 'template-library' => [
428 'files' => [
429 'css' => 'css/admin/template-library.css',
430 'js' => 'js/admin/template-library.js',
431 ],
432 'path' => '', // Root assets folder
433 'wp_deps' => ['wp-element', 'wp-i18n', 'wp-components', 'wp-api-fetch', 'wp-hooks', 'wp-util'],
434 'wp_css_deps' => ['wp-components'],
435 'deps' => [],
436 ],
437
438 'template-kits-app' => [
439 'files' => [
440 'js' => 'js/admin/template-kits-app.js',
441 'css' => 'css/admin/template-kits-app.css',
442 ],
443 'path' => '', // Root assets folder
444 'wp_deps' => ['wp-element', 'wp-i18n'],
445 'deps' => [],
446 ],
447
448 'widgets-library-app' => [
449 'files' => [
450 'js' => 'js/admin/widgets-library-app.js',
451 'css' => 'css/admin/widgets-library-app.css',
452 ],
453 'path' => '', // Root assets folder
454 'wp_deps' => ['wp-element', 'wp-i18n'],
455 'deps' => [],
456 ],
457
458 // =========================================================
459 // Wizard Builder
460 // =========================================================
461 'widget-builder' => [
462 'files' => [
463 'css' => 'css/admin/widget-builder.css',
464 ],
465 'path' => '',
466 'deps' => [],
467 ],
468
469 'widget-admin' => [
470 'files' => [
471 'js' => 'js/admin/widget-admin.js',
472 ],
473 'path' => '',
474 'deps' => [],
475 ],
476
477 'widget-builder-app' => [
478 'files' => [
479 'css' => 'css/admin/widget-builder-app.css',
480 'js' => 'js/admin/widget-builder-app.js',
481 ],
482 'path' => '',
483 'deps' => [],
484 ],
485
486 'widget-builder-tooltip' => [
487 'files' => [
488 'js' => 'js/admin/widget-builder-tooltip.js',
489 ],
490 'path' => '',
491 'deps' => ['widget-builder-app'],
492 ],
493
494 'page-importer' => [
495 'files' => [
496 'css' => 'css/admin/page-importer.css',
497 'js' => 'js/admin/page-importer.js',
498 ],
499 'path' => '', // Root assets folder
500 'deps' => ['template-library', 'widget-builder'],
501 ],
502
503 // =========================================================
504 // IMAGE OPTIMIZER (Free Version - Upsell Display)
505 // =========================================================
506 'image-optimizer-base' => [
507 'files' => [
508 'js' => 'js/admin/image-optimizer/image-optimizer-base.js',
509 ],
510 'path' => '', // Root assets folder
511 'deps' => [],
512 ],
513
514 // =========================================================
515 // ELEMENTOR EDITOR CONTROLS
516 // =========================================================
517 'module-editor' => [
518 'files' => [
519 'js' => 'js/admin/module-editor.js',
520 ],
521 'path' => '',
522 'deps' => [],
523 ],
524
525 'visual-select' => [
526 'files' => [
527 'css' => 'css/admin/master-addons-editor.css',
528 'js' => 'js/admin/visual-select.js',
529 ],
530 'path' => '',
531 'deps' => ['module-editor'],
532 ],
533
534 'file-select-control' => [
535 'files' => [
536 'js' => 'js/admin/file-select-control.js',
537 ],
538 'path' => '',
539 'deps' => [],
540 ],
541
542 // =========================================================
543 // EXTERNAL CDN LIBRARIES
544 // =========================================================
545 'google-recaptcha' => [
546 'files' => [
547 'js' => 'https://www.google.com/recaptcha/api.js',
548 ],
549 'deps' => [],
550 'cdn' => true,
551 ],
552 ];
553
554 // Allow extensions to add more vendors
555 self::$registry = apply_filters('jltma/assets/vendor_registry', self::$registry);
556
557 return self::$registry;
558 }
559
560 /**
561 * Get single vendor config
562 *
563 * @param string $handle Vendor handle
564 * @return array|null
565 */
566 public static function get($handle)
567 {
568 $registry = self::get_registry();
569 return $registry[$handle] ?? null;
570 }
571
572 /**
573 * Check if vendor exists
574 *
575 * @param string $handle Vendor handle
576 * @return bool
577 */
578 public static function exists($handle)
579 {
580 $registry = self::get_registry();
581 return isset($registry[$handle]);
582 }
583
584 /**
585 * Get all dependencies for a vendor (recursive)
586 *
587 * @param string $handle Vendor handle
588 * @return array Flat array of all dependency handles
589 */
590 public static function get_all_deps($handle)
591 {
592 $registry = self::get_registry();
593 $all_deps = [];
594
595 if (!isset($registry[$handle])) {
596 return $all_deps;
597 }
598
599 foreach ($registry[$handle]['deps'] ?? [] as $dep) {
600 // Get nested deps first (depth-first)
601 $nested = self::get_all_deps($dep);
602 $all_deps = array_merge($all_deps, $nested);
603 // Then add the dep itself
604 $all_deps[] = $dep;
605 }
606
607 return array_unique($all_deps);
608 }
609
610 /**
611 * Topological sort for correct registration order
612 * Ensures dependencies are registered before dependents
613 *
614 * @return array Sorted vendor registry
615 */
616 public static function get_sorted_registry()
617 {
618 $registry = self::get_registry();
619 $sorted = [];
620 $visited = [];
621
622 foreach (array_keys($registry) as $handle) {
623 self::visit($handle, $registry, $sorted, $visited);
624 }
625
626 return $sorted;
627 }
628
629 /**
630 * Visit node for topological sort
631 */
632 private static function visit($handle, $registry, &$sorted, &$visited)
633 {
634 if (isset($visited[$handle])) {
635 return;
636 }
637
638 $visited[$handle] = true;
639
640 if (isset($registry[$handle]['deps'])) {
641 foreach ($registry[$handle]['deps'] as $dep) {
642 if (isset($registry[$dep])) {
643 self::visit($dep, $registry, $sorted, $visited);
644 }
645 }
646 }
647
648 $sorted[$handle] = $registry[$handle];
649 }
650
651 /**
652 * Register all vendor assets
653 * Called early in wp_enqueue_scripts
654 */
655 public function register_all()
656 {
657 $sorted = self::get_sorted_registry();
658 $assets_url = defined('JLTMA_ASSETS') ? JLTMA_ASSETS : (defined('JLTMA_PRO_ASSETS') ? JLTMA_PRO_ASSETS : '');
659 $vendor_url = $assets_url . 'vendor/';
660 $pro_vendor_url = defined('JLTMA_PRO_ASSETS') ? JLTMA_PRO_ASSETS . 'vendor/' : '';
661 $version = defined('JLTMA_VER') ? JLTMA_VER : (defined('JLTMA_PRO_VER') ? JLTMA_PRO_VER : '1.0.0');
662 $suffix = defined('JLTMA_SCRIPT_SUFFIX') ? JLTMA_SCRIPT_SUFFIX : '.min';
663
664 foreach ($sorted as $handle => $vendor) {
665 // Skip pro vendors if not licensed
666 if (!empty($vendor['pro']) && !Helper::jltma_premium()) {
667 continue;
668 }
669
670 $wp_handle = 'jltma-' . $handle;
671
672 // Determine base URL based on path override and pro status
673 $is_pro = !empty($vendor['pro']);
674 $pro_assets = defined('JLTMA_PRO_ASSETS') ? JLTMA_PRO_ASSETS : $assets_url;
675 $base_assets = $is_pro ? $pro_assets : $assets_url;
676
677 if (isset($vendor['path'])) {
678 // Custom path: use pro or free assets base + custom path
679 $base_url = $base_assets . ($vendor['path'] ? $vendor['path'] . '/' : '');
680 } elseif ($is_pro && $pro_vendor_url) {
681 // Pro vendor: use pro vendor folder
682 $base_url = $pro_vendor_url;
683 } else {
684 // Free vendor: use free vendor folder
685 $base_url = $vendor_url;
686 }
687
688 // Build dependency arrays for WordPress
689 $css_deps = $vendor['wp_css_deps'] ?? [];
690 $js_deps = array_merge(['jquery'], $vendor['wp_deps'] ?? []);
691
692 foreach ($vendor['deps'] ?? [] as $dep) {
693 $dep_handle = 'jltma-' . $dep;
694 if (isset($sorted[$dep]['files']['css'])) {
695 $css_deps[] = $dep_handle;
696 }
697 if (isset($sorted[$dep]['files']['js'])) {
698 $js_deps[] = $dep_handle;
699 }
700 }
701
702 // Register CSS
703 if (!empty($vendor['files']['css'])) {
704 // Resolve to the built variant that exists on disk (.min in production,
705 // source in debug) so a missing variant never 404s.
706 $css_file = $this->resolve_built_file($vendor['files']['css'], $suffix, $vendor, $is_pro, '.css');
707
708 // Check if it's a CDN/full URL or relative path
709 $css_url = $this->is_external_url($vendor['files']['css'])
710 ? $vendor['files']['css']
711 : $base_url . $css_file;
712
713 wp_register_style(
714 $wp_handle,
715 $css_url,
716 $css_deps,
717 !empty($vendor['cdn']) ? null : $this->resolve_asset_version($css_file, $vendor, $is_pro, $version)
718 );
719 }
720
721 // Register JS
722 if (!empty($vendor['files']['js'])) {
723 $js_file = $this->resolve_built_file($vendor['files']['js'], $suffix, $vendor, $is_pro, '.js');
724
725 // Check if it's a CDN/full URL or relative path
726 $js_url = $this->is_external_url($vendor['files']['js'])
727 ? $vendor['files']['js']
728 : $base_url . $js_file;
729
730 // Load in footer by default (true), unless in_header is explicitly true
731 $in_footer = isset($vendor['in_header']) && $vendor['in_header'] === true ? false : true;
732
733 wp_register_script(
734 $wp_handle,
735 $js_url,
736 $js_deps,
737 !empty($vendor['cdn']) ? null : $this->resolve_asset_version($js_file, $vendor, $is_pro, $version),
738 $in_footer
739 );
740 }
741 }
742
743 // Register Font Awesome with global handle (if not already registered by Elementor/other plugins)
744 $this->register_global_assets();
745
746 // Allow extensions to register additional assets
747 do_action('jltma/assets/after_register_vendors');
748 }
749
750 /**
751 * Resolve the version string for an asset.
752 *
753 * Local, built assets (path override of '', non-CDN, non-external) are versioned
754 * by their file modification time so a rebuild or plugin update busts the browser
755 * cache automatically. Everything else falls back to the plugin version.
756 *
757 * @param string $relative_file Asset path relative to its assets base (already suffixed).
758 * @param array $vendor Registry entry for the asset.
759 * @param bool $is_pro Whether the asset lives in the premium assets folder.
760 * @param string $fallback Version to use when mtime is unavailable.
761 * @return string
762 */
763 /**
764 * Resolve a locally-built asset to the variant that actually exists on disk.
765 *
766 * Production zips ship only the minified (.min) build; debug/dev trees may have
767 * only the unminified source. Picking by file existence (preferring .min in
768 * production, source in debug) guarantees the registered URL never 404s — a
769 * missing variant otherwise returns the HTML error page, which the browser then
770 * parses as JS ("Unexpected token '<'") and the React panel never mounts.
771 *
772 * Non-local assets (vendors, CDN, external, custom path) are returned unchanged.
773 *
774 * @param string $relative Asset path relative to its assets base.
775 * @param string $suffix '.min' in production, '' in debug.
776 * @param array $vendor Registry entry.
777 * @param bool $is_pro Whether the asset lives in the premium assets folder.
778 * @param string $ext File extension including dot (e.g. '.js', '.css').
779 * @return string
780 */
781 private function resolve_built_file($relative, $suffix, $vendor, $is_pro, $ext)
782 {
783 $is_local = isset($vendor['path'])
784 && $vendor['path'] === ''
785 && empty($vendor['cdn'])
786 && !$this->is_external_url($relative);
787
788 if (!$is_local) {
789 return $relative;
790 }
791
792 $base_dir = $is_pro
793 ? (defined('JLTMA_PRO_PATH') ? trailingslashit(JLTMA_PRO_PATH) . 'premium/assets/' : '')
794 : (defined('JLTMA_PATH') ? trailingslashit(JLTMA_PATH) . 'assets/' : '');
795
796 $min = preg_replace('/' . preg_quote($ext, '/') . '$/', '.min' . $ext, $relative);
797
798 // Prefer minified in production; prefer source in debug.
799 $candidates = ($suffix === '.min') ? array($min, $relative) : array($relative, $min);
800
801 if ($base_dir) {
802 foreach ($candidates as $candidate) {
803 if (is_file($base_dir . $candidate)) {
804 return $candidate;
805 }
806 }
807 }
808
809 return $candidates[0];
810 }
811
812 private function resolve_asset_version($relative_file, $vendor, $is_pro, $fallback)
813 {
814 $is_local = isset($vendor['path'])
815 && $vendor['path'] === ''
816 && empty($vendor['cdn'])
817 && !$this->is_external_url($relative_file);
818
819 if (!$is_local) {
820 return $fallback;
821 }
822
823 $base_dir = $is_pro
824 ? (defined('JLTMA_PRO_PATH') ? trailingslashit(JLTMA_PRO_PATH) . 'premium/assets/' : '')
825 : (defined('JLTMA_PATH') ? trailingslashit(JLTMA_PATH) . 'assets/' : '');
826
827 if ($base_dir) {
828 $file_path = $base_dir . $relative_file;
829 if (is_file($file_path)) {
830 return (string) filemtime($file_path);
831 }
832 }
833
834 return $fallback;
835 }
836
837 /**
838 * Get global assets registry
839 *
840 * These use standard global handles shared across plugins/themes
841 * to avoid duplicate loading (e.g., Font Awesome, Google Fonts).
842 *
843 * @return array
844 */
845 private static function get_global_assets()
846 {
847 $assets = [];
848
849 // Use Font Awesome bundled with Elementor (a required dependency) instead of a CDN.
850 if (defined('ELEMENTOR_ASSETS_URL')) {
851 $assets['font-awesome-5-all-css'] = [
852 'type' => 'css',
853 'url' => ELEMENTOR_ASSETS_URL . 'lib/font-awesome/css/all.min.css',
854 'deps' => [],
855 'version' => '6.5.1',
856 ];
857 }
858
859 return $assets;
860 }
861
862 /**
863 * Register global assets with standard handles
864 *
865 * Uses standard handles (e.g., 'font-awesome-5-all-css') so assets
866 * are not duplicated if already loaded by Elementor or other plugins.
867 */
868 private function register_global_assets()
869 {
870 $global_assets = self::get_global_assets();
871
872 foreach ($global_assets as $handle => $asset) {
873 // Skip if already registered by another plugin/theme
874 if ($asset['type'] === 'css' && wp_style_is($handle, 'registered')) {
875 continue;
876 }
877 if ($asset['type'] === 'js' && wp_script_is($handle, 'registered')) {
878 continue;
879 }
880
881 // Register the asset
882 if ($asset['type'] === 'css') {
883 wp_register_style($handle, $asset['url'], $asset['deps'], $asset['version']);
884 } else {
885 wp_register_script($handle, $asset['url'], $asset['deps'], $asset['version'], true);
886 }
887 }
888 }
889
890 /**
891 * Enqueue vendor assets by handle
892 *
893 * @param string|array $handles Single handle or array of handles
894 */
895 public static function enqueue($handles)
896 {
897 $handles = (array) $handles;
898
899 foreach ($handles as $handle) {
900 // Recursively enqueue dependencies first (for CSS - JS deps are handled by WP)
901 self::enqueue_with_deps($handle);
902 }
903 }
904
905 /**
906 * Enqueue a vendor with all its dependencies (including CSS)
907 * WordPress handles JS dependencies automatically, but not CSS
908 *
909 * @param string $handle Vendor handle
910 * @param array $enqueued Track already enqueued to avoid duplicates
911 */
912 private static function enqueue_with_deps($handle, &$enqueued = [])
913 {
914 if (isset($enqueued[$handle])) {
915 return;
916 }
917
918 $vendor = self::get($handle);
919 if (!$vendor) {
920 return;
921 }
922
923 // Enqueue dependencies first
924 foreach ($vendor['deps'] ?? [] as $dep) {
925 self::enqueue_with_deps($dep, $enqueued);
926 }
927
928 $wp_handle = 'jltma-' . $handle;
929
930 // Enqueue CSS if exists
931 if (!empty($vendor['files']['css'])) {
932 wp_enqueue_style($wp_handle);
933 }
934
935 // Enqueue JS if exists
936 if (!empty($vendor['files']['js'])) {
937 wp_enqueue_script($wp_handle);
938 }
939
940 $enqueued[$handle] = true;
941 }
942
943 /**
944 * Get WordPress handle for a vendor
945 *
946 * @param string $handle Vendor handle
947 * @return string WordPress handle with jltma- prefix
948 */
949 public static function get_wp_handle($handle)
950 {
951 return 'jltma-' . $handle;
952 }
953
954 /**
955 * Check if a vendor is deprecated
956 *
957 * @param string $handle Vendor handle
958 * @return bool
959 */
960 public static function is_deprecated($handle)
961 {
962 $vendor = self::get($handle);
963 return !empty($vendor['deprecated']);
964 }
965
966 /**
967 * Check if a vendor is pro-only
968 *
969 * @param string $handle Vendor handle
970 * @return bool
971 */
972 public static function is_pro($handle)
973 {
974 $vendor = self::get($handle);
975 return !empty($vendor['pro']);
976 }
977
978 /**
979 * Get all vendor handles
980 *
981 * @return array
982 */
983 public static function get_all_handles()
984 {
985 return array_keys(self::get_registry());
986 }
987
988 /**
989 * Check if a URL is external (CDN/full URL)
990 *
991 * @param string $url URL to check
992 * @return bool
993 */
994 private function is_external_url($url)
995 {
996 return strpos($url ?? '', '//') === 0 || strpos($url ?? '', 'http') === 0;
997 }
998
999 /**
1000 * Debug: Get registration info for a vendor
1001 *
1002 * @param string $handle Vendor handle
1003 * @return array
1004 */
1005 public static function debug_vendor($handle)
1006 {
1007 $vendor = self::get($handle);
1008 if (!$vendor) {
1009 return ['error' => 'Vendor not found: ' . $handle];
1010 }
1011
1012 $wp_handle = 'jltma-' . $handle;
1013 $all_deps = self::get_all_deps($handle);
1014
1015 return [
1016 'handle' => $handle,
1017 'wp_handle' => $wp_handle,
1018 'files' => $vendor['files'] ?? [],
1019 'deps' => $vendor['deps'] ?? [],
1020 'all_deps' => $all_deps,
1021 'path' => $vendor['path'] ?? 'vendor',
1022 'deprecated' => !empty($vendor['deprecated']),
1023 'pro' => !empty($vendor['pro']),
1024 'css_registered' => wp_style_is($wp_handle, 'registered'),
1025 'js_registered' => wp_script_is($wp_handle, 'registered'),
1026 ];
1027 }
1028 }
1029
1030 // Initialize Assets Manager
1031 Assets_Manager::get_instance();
1032