PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.0.9
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.0.9
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-loader.php

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

1,003 lines 38.6 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 - Dynamic Assets Loader
5 *
6 * Loads CSS/JS assets only for widgets actually used on each page.
7 * This significantly reduces page load time by avoiding global asset loading.
8 *
9 * ## Asset File Variants
10 * The build system generates multiple variants for each addon:
11 * - file.css - Expanded CSS (development/debugging)
12 * - file.min.css - Minified CSS (production, ~20% smaller)
13 * - file.rtl.css - RTL version for right-to-left languages
14 * - file.rtl.min.css - RTL minified
15 *
16 * ## Gzipped Files (.css.gz / .js.gz)
17 * Pre-compressed files are generated for optimal delivery:
18 * - file.min.css.gz - Gzipped minified CSS (~80% smaller than original)
19 * - file.rtl.min.css.gz - Gzipped RTL CSS
20 *
21 * ### How Gzip Works:
22 * 1. Build script pre-compresses .min.css files to .min.css.gz
23 * 2. Web server (Apache/Nginx) serves .gz files automatically when:
24 * - Client sends "Accept-Encoding: gzip" header
25 * - Server has mod_deflate (Apache) or gzip module (Nginx) enabled
26 * 3. If server gzip is disabled, regular .min.css files are served
27 *
28 * ### Server Configuration Required:
29 * Apache (.htaccess):
30 * <IfModule mod_deflate.c>
31 * AddEncoding gzip .gz
32 * RewriteCond %{HTTP:Accept-Encoding} gzip
33 * RewriteCond %{REQUEST_FILENAME}.gz -f
34 * RewriteRule ^(.*)$ $1.gz [L]
35 * </IfModule>
36 *
37 * Nginx:
38 * gzip_static on;
39 *
40 * @package MasterAddons\Inc\Classes
41 * @since 2.0.0
42 * @see docs/plans/2026-01-12-vite-migration-asset-management-design.md
43 */
44
45 namespace MasterAddons\Inc\Classes;
46
47 use MasterAddons\Inc\Admin\Config;
48 use MasterAddons\Inc\Classes\Helper;
49 use MasterAddons\Inc\Classes\Assets_Manager;
50
51 if (!defined('ABSPATH')) {
52 exit;
53 }
54
55 class Assets_Loader
56 {
57 /**
58 * Singleton instance
59 */
60 private static $instance = null;
61
62 /**
63 * Widget to asset mapping
64 * Format: widget_name => ['css' => 'filename', 'js' => 'filename']
65 */
66 private $widget_assets = [];
67
68 /**
69 * Detected widgets on current page
70 */
71 private $page_widgets = [];
72
73 /**
74 * Post meta key for cached widget list
75 */
76 const META_KEY = '_jltma_used_widgets';
77
78 /**
79 * Option name for dynamic loading setting
80 */
81 const OPTION_KEY = 'jltma_dynamic_assets_enabled';
82
83 /**
84 * Get singleton instance
85 */
86 public static function get_instance()
87 {
88 if (null === self::$instance) {
89 self::$instance = new self();
90 }
91 return self::$instance;
92 }
93
94 /**
95 * Track which script handles need type="module"
96 */
97 private $module_script_handles = [];
98
99 /**
100 * Constructor
101 */
102 public function __construct()
103 {
104 // Only initialize asset loading if dynamic loading is enabled
105 if (!$this->is_enabled()) {
106 return;
107 }
108
109 // Build widget-asset map on init
110 add_action('init', [$this, 'build_asset_map'], 5);
111
112 // Register all addon assets (but don't enqueue yet) - both frontend and admin
113 add_action('wp_enqueue_scripts', [$this, 'register_addon_assets'], 5);
114 add_action('admin_enqueue_scripts', [$this, 'register_addon_assets'], 5);
115
116 // Enqueue only needed assets on frontend
117 add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets'], 100);
118
119 // Localize scripts after they are enqueued
120 add_action('wp_enqueue_scripts', [$this, 'localize_addon_scripts'], 101);
121
122 // Load all enabled assets in editor/preview
123 add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueue_editor_assets']);
124 add_action('elementor/preview/enqueue_styles', [$this, 'enqueue_editor_assets']);
125 add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueue_editor_scripts']);
126 add_action('elementor/preview/enqueue_scripts', [$this, 'enqueue_editor_scripts']);
127
128 // Localize scripts in editor/preview
129 add_action('elementor/editor/after_enqueue_scripts', [$this, 'localize_addon_scripts'], 20);
130 add_action('elementor/preview/enqueue_scripts', [$this, 'localize_addon_scripts'], 20);
131
132 // Update cache on post save in Elementor
133 add_action('elementor/editor/after_save', [$this, 'update_widget_cache'], 10, 2);
134
135 // Clear cache when post is trashed or deleted
136 add_action('trashed_post', [$this, 'clear_post_cache']);
137 add_action('deleted_post', [$this, 'clear_post_cache']);
138
139 // Add type="module" to ES module scripts (for WordPress < 6.5 compatibility)
140 add_filter('script_loader_tag', [$this, 'add_module_type_attribute'], 10, 3);
141
142 // Master Addons button into Elementor editor promo panel
143 add_action('elementor/editor/after_enqueue_scripts', [$this, 'inject_promo_button_script']);
144
145 }
146
147 /**
148 * Inject Master Addons button into Elementor editor promo panel
149 * Adds our button below Elementor's existing promo button via JavaScript
150 *
151 * @return void
152 */
153 public function inject_promo_button_script()
154 {
155 // Show settings for premium users, pricing page for free users
156 if (Helper::jltma_premium()) {
157 $jltma_link = esc_url(admin_url('admin.php?page=master-addons-settings'));
158 } else {
159 $jltma_link = esc_url('https://master-addons.com/pricing/?utm_source=starter-user&utm_medium=elementor-editor&utm_campaign=editor-promo-panel');
160 }
161 $button_text = esc_js(__('Explore Master Addons', 'master-addons'));
162
163 $script = "(function() {
164 function injectMasterAddonsButton() {
165 var promoPanel = document.getElementById('elementor-panel-get-pro-elements');
166 if (!promoPanel) return;
167 if (promoPanel.querySelector('.jltma-promo-button')) return;
168 var existingButton = promoPanel.querySelector('.elementor-button.go-pro');
169 if (!existingButton) return;
170 var maButton = document.createElement('a');
171 maButton.href = '{$jltma_link}';
172 maButton.className = 'elementor-button jltma-promo-button';
173 maButton.target = '_blank';
174 maButton.textContent = '{$button_text}';
175 maButton.style.cssText = 'display: inline-flex; margin-top: 10px; background: linear-gradient(135deg, #6f42c1, #9c27b0); color: #fff;';
176 existingButton.insertAdjacentElement('afterend', maButton);
177 }
178 var observer = new MutationObserver(function() { injectMasterAddonsButton(); });
179 observer.observe(document.body, { childList: true, subtree: true });
180 window.addEventListener('load', function() {
181 setTimeout(injectMasterAddonsButton, 500);
182 setTimeout(injectMasterAddonsButton, 2000);
183 });
184 })();";
185
186 wp_add_inline_script('elementor-editor', $script);
187 }
188
189 /**
190 * Add type="module" attribute to ES module scripts
191 * Required for scripts built with Vite/Rollup ES format
192 *
193 * @param string $tag Script HTML tag
194 * @param string $handle Script handle
195 * @param string $src Script source URL
196 * @return string Modified script tag
197 */
198 public function add_module_type_attribute($tag, $handle, $src)
199 {
200 // Only modify addon scripts that we registered as modules
201 if (!in_array($handle, $this->module_script_handles, true)) {
202 return $tag;
203 }
204
205 // Don't add if already has type attribute
206 if (strpos($tag, 'type=') !== false) {
207 return $tag;
208 }
209
210 // Add type="module" attribute
211 return str_replace(' src=', ' type="module" src=', $tag);
212 }
213
214 /**
215 * Check if dynamic asset loading is enabled
216 * Default: true (enabled) - per-addon CSS/JS loading active by default
217 */
218 public function is_enabled()
219 {
220 return (bool) get_option(self::OPTION_KEY, true);
221 }
222
223 /**
224 * Enable dynamic asset loading
225 */
226 public static function enable()
227 {
228 update_option(self::OPTION_KEY, true);
229 }
230
231 /**
232 * Disable dynamic asset loading
233 */
234 public static function disable()
235 {
236 update_option(self::OPTION_KEY, false);
237 }
238
239 /**
240 * Build mapping of widget names to their asset files
241 * Uses JLTMA_Config as the single source of truth
242 * Maps by Elementor widget name (from widget_name field or config key)
243 * Includes both addons and extensions/modules
244 *
245 * New simplified format supported:
246 * - 'css' => true → auto-maps to ma-{widget-key}.css
247 * - 'js' => true → auto-maps to ma-{widget-key}.js
248 * - 'vendors' => ['fancybox', 'tippy'] → resolves from Assets_Manager registry
249 */
250 public function build_asset_map()
251 {
252 $this->widget_assets = [];
253
254 // Get all addons from unified config
255 $addons = Config::get_addons();
256
257 foreach ($addons as $key => $addon) {
258 // Get assets defined in config
259 $assets = isset($addon['assets']) ? $addon['assets'] : [];
260
261 // Build CSS array - handle both true and explicit array
262 $css_files = [];
263 if (isset($assets['css'])) {
264 if ($assets['css'] === true) {
265 // Auto-map to widget key filename
266 $css_files = [$key];
267 } elseif (is_array($assets['css'])) {
268 $css_files = $assets['css'];
269 } elseif (is_string($assets['css'])) {
270 $css_files = [$assets['css']];
271 }
272 }
273
274 // Build JS array - handle both true and explicit array
275 $js_files = [];
276 if (isset($assets['js'])) {
277 if ($assets['js'] === true) {
278 // Auto-map to widget key filename
279 $js_files = [$key];
280 } elseif (is_array($assets['js'])) {
281 $js_files = $assets['js'];
282 } elseif (is_string($assets['js'])) {
283 $js_files = [$assets['js']];
284 }
285 }
286
287 // Build vendor dependencies - support both 'vendor' (legacy) and 'vendors' (new)
288 $vendors = isset($assets['vendors']) ? $assets['vendors'] : (isset($assets['vendor']) ? $assets['vendor'] : []);
289
290 // Track if this is a premium addon
291 $is_pro = isset($addon['is_pro']) ? (bool) $addon['is_pro'] : false;
292
293 // Use config key directly - widgets declare their own dependencies
294 // via get_style_depends() and get_script_depends() methods
295 $this->widget_assets[$key] = [
296 'css' => $css_files,
297 'js' => $js_files,
298 'vendors' => $vendors,
299 'is_pro' => $is_pro,
300 'asset_type' => 'addon', // Mark as addon for path resolution
301 ];
302 }
303
304 // Get all extensions/modules from unified config
305 $extensions = Config::get_extensions();
306
307 foreach ($extensions as $key => $extension) {
308 // Get assets defined in config
309 $assets = isset($extension['assets']) ? $extension['assets'] : [];
310
311 // Build CSS array - handle both true and explicit array
312 $css_files = [];
313 if (isset($assets['css'])) {
314 if ($assets['css'] === true) {
315 $css_files = [$key];
316 } elseif (is_array($assets['css'])) {
317 $css_files = $assets['css'];
318 } elseif (is_string($assets['css'])) {
319 $css_files = [$assets['css']];
320 }
321 }
322
323 // Build JS array - handle both true and explicit array
324 $js_files = [];
325 if (isset($assets['js'])) {
326 if ($assets['js'] === true) {
327 $js_files = [$key];
328 } elseif (is_array($assets['js'])) {
329 $js_files = $assets['js'];
330 } elseif (is_string($assets['js'])) {
331 $js_files = [$assets['js']];
332 }
333 }
334
335 // Build vendor dependencies - support both 'vendor' (legacy) and 'vendors' (new)
336 $vendors = isset($assets['vendors']) ? $assets['vendors'] : (isset($assets['vendor']) ? $assets['vendor'] : []);
337
338 // Track if this is a premium extension
339 $is_pro = isset($extension['is_pro']) ? (bool) $extension['is_pro'] : false;
340
341 $this->widget_assets[$key] = [
342 'css' => $css_files,
343 'js' => $js_files,
344 'vendors' => $vendors,
345 'is_pro' => $is_pro,
346 'asset_type' => 'module', // Mark as module for path resolution
347 ];
348 }
349
350 // Note: Common swiper-carousel styles are now loaded via vendor dependency
351 // Each swiper widget declares 'swiper-carousel' in its vendors config
352
353 // Allow filtering to add more widget mappings or modify existing
354 $this->widget_assets = apply_filters('jltma/assets/widget_map', $this->widget_assets);
355 }
356
357 /**
358 * Register all addon and module assets without enqueuing
359 * Supports SCRIPT_DEBUG for unminified files
360 * Handles separate paths for:
361 * - Free addons: assets/css/addons/, assets/js/addons/
362 * - Premium addons: premium/assets/css/addons/, premium/assets/js/addons/
363 * - Free modules: assets/css/modules/, assets/js/modules/
364 * - Premium modules: premium/assets/css/modules/, premium/assets/js/modules/
365 * Premium assets only load if user has valid license
366 */
367 public function register_addon_assets()
368 {
369 // Register all vendor assets from central registry
370 Assets_Manager::get_instance()->register_all();
371
372 // Check if user has premium license
373 $has_premium_license = Helper::jltma_premium();
374
375 // Base URL and paths — switch constants based on is_pro flag
376 $free_url = defined('JLTMA_URL') ? trailingslashit(JLTMA_URL) : (defined('JLTMA_PRO_URL') ? JLTMA_PRO_URL : '');
377 $free_path = defined('JLTMA_PATH') ? JLTMA_PATH : (defined('JLTMA_PRO_PATH') ? JLTMA_PRO_PATH : '');
378 $pro_url = defined('JLTMA_PRO_URL') ? JLTMA_PRO_URL : $free_url;
379 $pro_path = defined('JLTMA_PRO_PATH') ? JLTMA_PRO_PATH : $free_path;
380 $version = defined('JLTMA_VER') ? JLTMA_VER : (defined('JLTMA_PRO_VER') ? JLTMA_PRO_VER : '1.0.0');
381 $suffix = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? '' : '.min';
382
383 foreach ($this->widget_assets as $widget => $assets) {
384 // Determine asset type (addon or module) - defaults to addon for backward compatibility
385 $asset_type = isset($assets['asset_type']) ? $assets['asset_type'] : 'addon';
386 $type_folder = ($asset_type === 'module') ? 'modules' : 'addons';
387
388 // Check if this is a pro asset — use JLTMA_PRO_* constants for pro assets
389 $is_pro_asset = isset($assets['is_pro']) && $assets['is_pro'];
390 $plugin_url = $is_pro_asset ? $pro_url : $free_url;
391 $plugin_path = $is_pro_asset ? $pro_path : $free_path;
392
393 // Build paths based on asset type
394 $free_css_url = $plugin_url . 'assets/css/' . $type_folder . '/';
395 $free_js_url = $plugin_url . 'assets/js/' . $type_folder . '/';
396 $premium_css_url = $plugin_url . 'premium/assets/css/' . $type_folder . '/';
397 $premium_js_url = $plugin_url . 'premium/assets/js/' . $type_folder . '/';
398
399 $free_css_path = $plugin_path . 'assets/css/' . $type_folder . '/';
400 $free_js_path = $plugin_path . 'assets/js/' . $type_folder . '/';
401 $premium_css_path = $plugin_path . 'premium/assets/css/' . $type_folder . '/';
402 $premium_js_path = $plugin_path . 'premium/assets/js/' . $type_folder . '/';
403
404 // Register CSS files (now an array)
405 // First style is the main addon style, rest are dependencies
406 if (!empty($assets['css'])) {
407 $css_files = (array) $assets['css'];
408 $css_dependencies = [];
409
410 // Add vendor CSS as dependencies (vendors are registered by Assets_Manager)
411 if (!empty($assets['vendors'])) {
412 foreach ((array) $assets['vendors'] as $vendor) {
413 $vendor_config = Assets_Manager::get($vendor);
414 if ($vendor_config && !empty($vendor_config['files']['css'])) {
415 $css_dependencies[] = 'jltma-' . $vendor;
416 }
417 }
418 }
419
420 // First CSS is the main addon style
421 $main_css = array_shift($css_files);
422
423 // Remaining CSS files are dependencies (already registered as vendor styles)
424 foreach ($css_files as $dep_css) {
425 $css_dependencies[] = $dep_css;
426 }
427
428 // Generate handle for main addon style
429 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $main_css);
430 $ltr_handle = 'master-addons-' . $handle_name;
431
432 // Try minified first, then unminified
433 $css_filename = $main_css . $suffix . '.css';
434 $css_fallback = $main_css . '.css';
435
436 // Determine which path to use: premium or free
437 // For pro extensions/addons, check premium path first (if extension is in config, premium is active)
438 $css_url = $free_css_url;
439 $css_path = $free_css_path;
440
441 if ($has_premium_license || $is_pro_asset) {
442 // Check if premium version exists
443 if (file_exists($premium_css_path . $css_filename) || file_exists($premium_css_path . $css_fallback)) {
444 $css_url = $premium_css_url;
445 $css_path = $premium_css_path;
446 }
447 }
448
449 if (file_exists($css_path . $css_filename)) {
450 wp_register_style($ltr_handle, $css_url . $css_filename, $css_dependencies, $version);
451 } elseif (file_exists($css_path . $css_fallback)) {
452 wp_register_style($ltr_handle, $css_url . $css_fallback, $css_dependencies, $version);
453 }
454
455 // Register RTL CSS (depends on LTR version)
456 $rtl_handle = $ltr_handle . '-rtl';
457 $rtl_filename = $main_css . '.rtl' . $suffix . '.css';
458 $rtl_fallback = $main_css . '.rtl.css';
459
460 if (file_exists($css_path . $rtl_filename)) {
461 wp_register_style($rtl_handle, $css_url . $rtl_filename, [$ltr_handle], $version);
462 } elseif (file_exists($css_path . $rtl_fallback)) {
463 wp_register_style($rtl_handle, $css_url . $rtl_fallback, [$ltr_handle], $version);
464 }
465 }
466
467 // Register JS files (now an array)
468 // First script is the main addon script, rest are dependencies
469 if (!empty($assets['js'])) {
470 $js_files = (array) $assets['js'];
471 $js_dependencies = ['jquery'];
472
473 // Add vendor JS as dependencies (vendors are registered by Assets_Manager)
474 if (!empty($assets['vendors'])) {
475 foreach ((array) $assets['vendors'] as $vendor) {
476 $vendor_config = Assets_Manager::get($vendor);
477 if ($vendor_config && !empty($vendor_config['files']['js'])) {
478 $js_dependencies[] = 'jltma-' . $vendor;
479 }
480 }
481 }
482
483 // First script is the main addon script
484 $main_js = array_shift($js_files);
485
486 // Remaining scripts are dependencies (already registered as vendor scripts)
487 foreach ($js_files as $dep_js) {
488 $js_dependencies[] = $dep_js;
489 }
490
491 // Generate handle for main addon script
492 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $main_js);
493 $js_handle = 'master-addons-' . $handle_name;
494
495 // Try minified first, then unminified
496 $js_filename = $main_js . $suffix . '.js';
497 $js_fallback = $main_js . '.js';
498
499 // Determine which path to use: premium or free
500 // For pro extensions/addons, check premium path first (if extension is in config, premium is active)
501 $js_url = $free_js_url;
502 $js_path = $free_js_path;
503
504 if ($has_premium_license || $is_pro_asset) {
505 // Check if premium version exists
506 if (file_exists($premium_js_path . $js_filename) || file_exists($premium_js_path . $js_fallback)) {
507 $js_url = $premium_js_url;
508 $js_path = $premium_js_path;
509 }
510 }
511
512 if (file_exists($js_path . $js_filename)) {
513 wp_register_script($js_handle, $js_url . $js_filename, $js_dependencies, $version, true);
514 // Only add type="module" if the file actually uses ES module syntax (import/export)
515 // jQuery IIFEs and other non-module scripts break with type="module" in some browsers
516 if ($this->is_es_module_file($js_path . $js_filename)) {
517 $this->module_script_handles[] = $js_handle;
518 }
519 } elseif (file_exists($js_path . $js_fallback)) {
520 wp_register_script($js_handle, $js_url . $js_fallback, $js_dependencies, $version, true);
521 if ($this->is_es_module_file($js_path . $js_fallback)) {
522 $this->module_script_handles[] = $js_handle;
523 }
524 }
525 }
526
527 // Note: Vendor dependencies are registered by Assets_Manager::register_all()
528 // The 'vendors' array just contains handles to be enqueued at runtime
529 }
530 }
531
532 /**
533 * Detect widgets used on current page
534 */
535 public function detect_page_widgets($post_id = null)
536 {
537 if (!$post_id) {
538 $post_id = get_the_ID();
539 }
540
541 if (!$post_id) {
542 return [];
543 }
544
545 // Try cached post meta first (fast path)
546 $cached = get_post_meta($post_id, self::META_KEY, true);
547
548 if (!empty($cached) && is_array($cached)) {
549 return $cached;
550 }
551
552 // Fallback: Parse Elementor data at runtime
553 return $this->parse_elementor_widgets($post_id);
554 }
555
556 /**
557 * Parse Elementor data to find Master Addons widgets
558 */
559 private function parse_elementor_widgets($post_id)
560 {
561 $elementor_data = get_post_meta($post_id, '_elementor_data', true);
562
563 if (empty($elementor_data)) {
564 return [];
565 }
566
567 if (is_string($elementor_data)) {
568 $elementor_data = json_decode($elementor_data, true);
569 }
570
571 if (!is_array($elementor_data)) {
572 return [];
573 }
574
575 $widgets = [];
576 $this->extract_widgets_recursive($elementor_data, $widgets);
577
578 $unique_widgets = array_unique($widgets);
579
580 // Cache for future requests
581 update_post_meta($post_id, self::META_KEY, $unique_widgets);
582
583 return $unique_widgets;
584 }
585
586 /**
587 * Recursively extract widget names and enabled extensions from Elementor data
588 */
589 private function extract_widgets_recursive($elements, &$widgets)
590 {
591 if (!is_array($elements)) {
592 return;
593 }
594
595 foreach ($elements as $element) {
596 // Check if this is a Master Addons widget
597 if (isset($element['widgetType'])) {
598 $widget_type = $element['widgetType'];
599
600 // Track Master Addons widgets (ma-* or jltma-* prefix)
601 if (strpos($widget_type, 'ma-') === 0 || strpos($widget_type, 'jltma-') === 0) {
602 $widgets[] = $widget_type;
603 }
604 }
605
606 // Check for enabled extensions in element settings
607 // Extensions use settings like: ma_el_animated_gradient_enable, ma_el_particles_enable, etc.
608 if (!empty($element['settings']) && is_array($element['settings'])) {
609 foreach ($element['settings'] as $setting_key => $setting_value) {
610 // Match pattern: ma_el_{extension}_enable = 'yes'
611 if (preg_match('/^ma_el_(.+)_enable$/', $setting_key, $matches) && $setting_value === 'yes') {
612 // Convert underscores to hyphens: animated_gradient -> animated-gradient
613 $extension_key = str_replace('_', '-', $matches[1]);
614 $widgets[] = $extension_key;
615 }
616
617 // Match pattern: ma_el_enable_{extension} = 'yes' (e.g., ma_el_enable_particles, ma_el_enable_bg_slider)
618 if (preg_match('/^ma_el_enable_(.+)$/', $setting_key, $matches) && $setting_value === 'yes') {
619 $extension_key = str_replace('_', '-', $matches[1]);
620 $widgets[] = $extension_key;
621 }
622
623 // Match pattern: enabled_{extension} = 'yes' (e.g., enabled_rellax)
624 if (preg_match('/^enabled_(.+)$/', $setting_key, $matches) && $setting_value === 'yes') {
625 $extension_key = str_replace('_', '-', $matches[1]);
626 $widgets[] = $extension_key;
627 }
628 }
629 }
630
631 // Recurse into nested elements
632 if (!empty($element['elements'])) {
633 $this->extract_widgets_recursive($element['elements'], $widgets);
634 }
635 }
636 }
637
638 /**
639 * Enqueue only assets needed for current page (frontend)
640 */
641 public function enqueue_frontend_assets()
642 {
643 // Skip in editor - handled separately
644 if ($this->is_elementor_editor()) {
645 return;
646 }
647
648 $widgets = $this->detect_page_widgets();
649 $is_rtl = is_rtl();
650
651 // Track which files we've enqueued (to avoid duplicates)
652 $enqueued_css = [];
653 $enqueued_js = [];
654 $enqueued_vendor = [];
655
656 foreach ($widgets as $widget_name) {
657 // Try direct lookup first, then try without ma-/jltma-/ma-el- prefix
658 // This handles cases where config key is 'contact-form-7' but widget name is 'ma-contact-form-7'
659 // or widget name is 'ma-el-ninja-forms' but config key is 'ninja-forms'
660 $asset_key = $widget_name;
661 if (!isset($this->widget_assets[$asset_key])) {
662 // Try stripping ma-el-, ma- or jltma- prefix
663 $asset_key = preg_replace('/^(ma-el-|ma-|jltma-)/', '', $widget_name);
664 }
665
666 if (!isset($this->widget_assets[$asset_key])) {
667 continue;
668 }
669
670 $assets = $this->widget_assets[$asset_key];
671
672 // Enqueue CSS files (now an array)
673 if (!empty($assets['css'])) {
674 foreach ((array) $assets['css'] as $css_file) {
675 // Skip if already enqueued
676 if (isset($enqueued_css[$css_file])) {
677 continue;
678 }
679
680 // Generate handle: master-addons-flipbox (strip ma- prefix)
681 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $css_file);
682 $css_handle = 'master-addons-' . $handle_name;
683
684 // Enqueue CSS (LTR or RTL based on site setting)
685 if ($is_rtl) {
686 $rtl_handle = $css_handle . '-rtl';
687 if (wp_style_is($rtl_handle, 'registered')) {
688 wp_enqueue_style($rtl_handle);
689 $enqueued_css[$css_file] = true;
690 continue;
691 }
692 }
693
694 if (wp_style_is($css_handle, 'registered')) {
695 wp_enqueue_style($css_handle);
696 $enqueued_css[$css_file] = true;
697 }
698 }
699 }
700
701 // Enqueue JS files (now an array)
702 if (!empty($assets['js'])) {
703 foreach ((array) $assets['js'] as $js_file) {
704 if (isset($enqueued_js[$js_file])) {
705 continue;
706 }
707
708 // Generate handle: master-addons-flipbox (strip ma- prefix)
709 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $js_file);
710 $js_handle = 'master-addons-' . $handle_name;
711
712 if (wp_script_is($js_handle, 'registered')) {
713 wp_enqueue_script($js_handle);
714 $enqueued_js[$js_file] = true;
715 }
716 }
717 }
718
719 // Enqueue vendor dependencies (simple array of handles from Assets_Manager)
720 if (!empty($assets['vendors'])) {
721 foreach ((array) $assets['vendors'] as $vendor) {
722 if (isset($enqueued_vendor[$vendor])) {
723 continue;
724 }
725
726 // Use Assets_Manager to enqueue vendor and all its dependencies
727 Assets_Manager::enqueue($vendor);
728 $enqueued_vendor[$vendor] = true;
729 }
730 }
731 }
732
733 }
734
735 /**
736 * Load ALL enabled addon CSS in editor/preview
737 */
738 public function enqueue_editor_assets()
739 {
740 $enabled_widgets = $this->get_enabled_widgets();
741 $is_rtl = is_rtl();
742
743 foreach ($this->widget_assets as $widget_name => $assets) {
744 // Only load if widget is enabled
745 if (!in_array($widget_name, $enabled_widgets)) {
746 continue;
747 }
748
749 // Enqueue CSS files (now an array)
750 if (!empty($assets['css'])) {
751 foreach ((array) $assets['css'] as $css_file) {
752 // Generate handle: master-addons-flipbox (strip ma- prefix)
753 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $css_file);
754 $css_handle = 'master-addons-' . $handle_name;
755
756 // Enqueue LTR
757 if (wp_style_is($css_handle, 'registered')) {
758 wp_enqueue_style($css_handle);
759 }
760
761 // Also enqueue RTL in editor for live preview switching
762 if ($is_rtl) {
763 $rtl_handle = $css_handle . '-rtl';
764 if (wp_style_is($rtl_handle, 'registered')) {
765 wp_enqueue_style($rtl_handle);
766 }
767 }
768 }
769 }
770
771 // Enqueue vendor assets in editor
772 if (!empty($assets['vendors'])) {
773 foreach ((array) $assets['vendors'] as $vendor) {
774 Assets_Manager::enqueue($vendor);
775 }
776 }
777 }
778
779 }
780
781 /**
782 * Load ALL enabled addon JS in editor/preview
783 */
784 public function enqueue_editor_scripts()
785 {
786 $enabled_widgets = $this->get_enabled_widgets();
787
788 foreach ($this->widget_assets as $widget_name => $assets) {
789 if (!in_array($widget_name, $enabled_widgets)) {
790 continue;
791 }
792
793 // Enqueue JS files (now an array)
794 if (!empty($assets['js'])) {
795 foreach ((array) $assets['js'] as $js_file) {
796 // Generate handle: master-addons-flipbox (strip ma- prefix)
797 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $js_file);
798 $js_handle = 'master-addons-' . $handle_name;
799 if (wp_script_is($js_handle, 'registered')) {
800 wp_enqueue_script($js_handle);
801 }
802 }
803 }
804
805 // Note: Vendor assets already enqueued in enqueue_editor_assets()
806 // Assets_Manager handles both CSS and JS when enqueue() is called
807 }
808 }
809
810 /**
811 * Localize addon scripts with required data
812 *
813 * Script-specific localization:
814 * - ma-data-table: DataTable translation strings (JLTMA_DATA_TABLE)
815 * - ma-bg-slider: Plugin URL for overlay images (jltma_scripts)
816 * - ma-restrict-content: AJAX URL for password verification (jltma_scripts)
817 */
818 public function localize_addon_scripts()
819 {
820 // Data Table localization
821 if (wp_script_is('master-addons-data-table', 'enqueued') || wp_script_is('master-addons-data-table', 'registered')) {
822 $jltma_data_table_vars = array(
823 'lengthMenu' => esc_html__('Display _MENU_ records per page', 'master-addons'),
824 'zeroRecords' => esc_html__('Nothing found - sorry', 'master-addons'),
825 'info' => esc_html__('Showing page _PAGE_ of _PAGES_', 'master-addons'),
826 'infoEmpty' => esc_html__('No records available', 'master-addons'),
827 'infoFiltered' => esc_html__('(filtered from _MAX_ total records)', 'master-addons'),
828 'searchPlaceholder' => esc_html__('Search...', 'master-addons'),
829 'processing' => esc_html__('Processing...', 'master-addons'),
830 'csvHtml5' => esc_html__('CSV', 'master-addons'),
831 'excelHtml5' => esc_html__('Excel', 'master-addons'),
832 'pdfHtml5' => esc_html__('PDF', 'master-addons'),
833 'print' => esc_html__('Print', 'master-addons'),
834 );
835 wp_localize_script('master-addons-data-table', 'JLTMA_DATA_TABLE', $jltma_data_table_vars);
836 }
837
838 // Background Slider localization (needs plugin_url for Vegas overlay images)
839 if (wp_script_is('master-addons-bg-slider', 'enqueued') || wp_script_is('master-addons-bg-slider', 'registered')) {
840 $jltma_bg_slider_data = array(
841 'plugin_url' => defined('JLTMA_URL') ? JLTMA_URL : (defined('JLTMA_PRO_URL') ? untrailingslashit(JLTMA_PRO_URL) : ''),
842 );
843 wp_localize_script('master-addons-bg-slider', 'JLTMA_SCRIPTS', $jltma_bg_slider_data);
844 }
845
846 // Restrict Content localization (needs ajaxurl for AJAX calls)
847 if (wp_script_is('master-addons-restrict-content', 'enqueued') || wp_script_is('master-addons-restrict-content', 'registered')) {
848 $jltma_restrict_content_data = array(
849 'ajaxurl' => admin_url('admin-ajax.php'),
850 'nonce' => wp_create_nonce('master-addons-elementor'),
851 );
852 wp_localize_script('master-addons-restrict-content', 'JLTMA_SCRIPTS', $jltma_restrict_content_data);
853 }
854
855 // Allow extensions to add more localizations
856 do_action('jltma/assets/localize_scripts');
857 }
858
859 /**
860 * Get list of enabled widgets from settings
861 */
862 private function get_enabled_widgets()
863 {
864 $settings = \MasterAddons\Inc\Admin\Settings\Settings::get_addons() ?: [];
865
866 $enabled = [];
867 foreach ($settings as $widget_key => $is_enabled) {
868 if ($is_enabled) {
869 $enabled[] = $widget_key;
870 }
871 }
872
873 return $enabled;
874 }
875
876 /**
877 * Update widget cache when post is saved in Elementor
878 */
879 public function update_widget_cache($post_id, $editor_data)
880 {
881 $widgets = [];
882 $this->extract_widgets_recursive($editor_data, $widgets);
883
884 $unique_widgets = array_unique($widgets);
885 update_post_meta($post_id, self::META_KEY, $unique_widgets);
886
887 // Trigger cache invalidation hook for Cache Manager
888 do_action('jltma/cache/invalidate_post', $post_id);
889 }
890
891 /**
892 * Clear cached widget list for a post
893 */
894 public function clear_post_cache($post_id)
895 {
896 delete_post_meta($post_id, self::META_KEY);
897 }
898
899 /**
900 * Check if a JS file is an ES module by looking for import/export statements
901 * Only files with actual ES module syntax should get type="module"
902 * jQuery IIFEs and other traditional scripts should load as regular scripts
903 */
904 private function is_es_module_file($file_path)
905 {
906 // Read just the first 512 bytes — module indicators are always near the top
907 $content = @file_get_contents($file_path, false, null, 0, 512);
908 if ($content === false) {
909 return false;
910 }
911
912 // Check for ES module import/export syntax
913 if (preg_match('/\b(import\s*[{"\']|import\s+\w|export\s+(default|{|\w))/', $content)) {
914 return true;
915 }
916
917 // Check if file starts with top-level const/let/class declarations
918 // These indicate Vite ES module output where imports were inlined/resolved
919 // Non-module scripts start with IIFE patterns: (function, ;(function, jQuery(, !function, var
920 $trimmed = ltrim($content);
921 if (preg_match('/^(const |let |class )\w/', $trimmed)) {
922 return true;
923 }
924
925 return false;
926 }
927
928 /**
929 * Check if we're in Elementor editor or preview
930 */
931 private function is_elementor_editor()
932 {
933 if (!class_exists('\Elementor\Plugin')) {
934 return false;
935 }
936
937 $elementor = \Elementor\Plugin::$instance;
938
939 if (!$elementor || !isset($elementor->editor) || !isset($elementor->preview)) {
940 return false;
941 }
942
943 return $elementor->editor->is_edit_mode() || $elementor->preview->is_preview_mode();
944 }
945
946 /**
947 * Get widget assets map (for Cache Manager)
948 */
949 public function get_widget_assets()
950 {
951 return $this->widget_assets;
952 }
953
954 /**
955 * Manually trigger asset detection for a post
956 */
957 public function refresh_post_cache($post_id)
958 {
959 delete_post_meta($post_id, self::META_KEY);
960 return $this->parse_elementor_widgets($post_id);
961 }
962
963 /**
964 * Get stats about widget usage
965 */
966 public function get_usage_stats()
967 {
968 global $wpdb;
969
970 $meta_key = self::META_KEY;
971
972 // Get all posts with cached widget data
973 $results = $wpdb->get_results(
974 $wpdb->prepare(
975 "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
976 $meta_key
977 )
978 );
979
980 $stats = [
981 'total_posts' => count($results),
982 'widget_usage' => [],
983 ];
984
985 foreach ($results as $row) {
986 $widgets = maybe_unserialize($row->meta_value);
987 if (is_array($widgets)) {
988 foreach ($widgets as $widget) {
989 if (!isset($stats['widget_usage'][$widget])) {
990 $stats['widget_usage'][$widget] = 0;
991 }
992 $stats['widget_usage'][$widget]++;
993 }
994 }
995 }
996
997 // Sort by usage count
998 arsort($stats['widget_usage']);
999
1000 return $stats;
1001 }
1002 }
1003