PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.2.3
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.2.3
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.2.3, at inc/classes/assets-loader.php

1,021 lines 40.4 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 // Base URL and paths — switch constants based on is_pro flag
373 $free_url = defined('JLTMA_URL') ? trailingslashit(JLTMA_URL) : (defined('JLTMA_PRO_URL') ? JLTMA_PRO_URL : '');
374 $free_path = defined('JLTMA_PATH') ? JLTMA_PATH : (defined('JLTMA_PRO_PATH') ? JLTMA_PRO_PATH : '');
375 $pro_url = defined('JLTMA_PRO_URL') ? JLTMA_PRO_URL : $free_url;
376 $pro_path = defined('JLTMA_PRO_PATH') ? JLTMA_PRO_PATH : $free_path;
377 $version = defined('JLTMA_VER') ? JLTMA_VER : (defined('JLTMA_PRO_VER') ? JLTMA_PRO_VER : '1.0.0');
378 $suffix = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? '' : '.min';
379
380 foreach ($this->widget_assets as $widget => $assets) {
381 // Determine asset type (addon or module) - defaults to addon for backward compatibility
382 $asset_type = isset($assets['asset_type']) ? $assets['asset_type'] : 'addon';
383 $type_folder = ($asset_type === 'module') ? 'modules' : 'addons';
384
385 // Check if this is a pro asset — use JLTMA_PRO_* constants for pro assets
386 $is_pro_asset = isset($assets['is_pro']) && $assets['is_pro'];
387 $plugin_url = $is_pro_asset ? $pro_url : $free_url;
388 $plugin_path = $is_pro_asset ? $pro_path : $free_path;
389
390 // Build paths based on asset type
391 $free_css_url = $plugin_url . 'assets/css/' . $type_folder . '/';
392 $free_js_url = $plugin_url . 'assets/js/' . $type_folder . '/';
393 $free_css_path = $plugin_path . 'assets/css/' . $type_folder . '/';
394 $free_js_path = $plugin_path . 'assets/js/' . $type_folder . '/';
395
396 // Premium asset locations live in the pro plugin's premium/ directory.
397 // The pro plugin (MasterAddons\Pro\Classes\Assets_Pro) supplies them
398 // through this filter; in a free-only install the defaults stay empty
399 // so the free plugin never references the premium/ directory itself.
400 $premium_base = wp_parse_args(
401 (array) apply_filters('master_addons/assets/premium_base', array(), $type_folder),
402 array('css_url' => '', 'js_url' => '', 'css_path' => '', 'js_path' => '')
403 );
404 $premium_css_url = $premium_base['css_url'];
405 $premium_js_url = $premium_base['js_url'];
406 $premium_css_path = $premium_base['css_path'];
407 $premium_js_path = $premium_base['js_path'];
408
409 // Register CSS files (now an array)
410 // First style is the main addon style, rest are dependencies
411 if (!empty($assets['css'])) {
412 $css_files = (array) $assets['css'];
413 $css_dependencies = [];
414
415 // Add vendor CSS as dependencies (vendors are registered by Assets_Manager).
416 // Only add the dep if the vendor style was actually registered — a pro-only
417 // vendor (e.g. prism) keeps its config but is not registered when the license
418 // is inactive, which would otherwise produce an "unregistered dependency" notice.
419 if (!empty($assets['vendors'])) {
420 foreach ((array) $assets['vendors'] as $vendor) {
421 $vendor_config = Assets_Manager::get($vendor);
422 if ($vendor_config && !empty($vendor_config['files']['css']) && wp_style_is('jltma-' . $vendor, 'registered')) {
423 $css_dependencies[] = 'jltma-' . $vendor;
424 }
425 }
426 }
427
428 // First CSS is the main addon style
429 $main_css = array_shift($css_files);
430
431 // Remaining CSS files are dependencies (already registered as vendor styles)
432 foreach ($css_files as $dep_css) {
433 $css_dependencies[] = $dep_css;
434 }
435
436 // Generate handle for main addon style
437 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $main_css);
438 $ltr_handle = 'master-addons-' . $handle_name;
439
440 // Try minified first, then unminified
441 $css_filename = $main_css . $suffix . '.css';
442 $css_fallback = $main_css . '.css';
443
444 // Determine which path to use: premium or free.
445 // The premium base is only populated when the pro plugin is
446 // active (see the premium_base filter above), so a premium
447 // version is preferred whenever one is shipped — no separate
448 // license check, which would hide premium styling from
449 // existing content on trial/expired sites.
450 $css_url = $free_css_url;
451 $css_path = $free_css_path;
452
453 if (!empty($premium_css_path)
454 && (file_exists($premium_css_path . $css_filename) || file_exists($premium_css_path . $css_fallback))) {
455 $css_url = $premium_css_url;
456 $css_path = $premium_css_path;
457 }
458
459 if (file_exists($css_path . $css_filename)) {
460 wp_register_style($ltr_handle, $css_url . $css_filename, $css_dependencies, $version);
461 } elseif (file_exists($css_path . $css_fallback)) {
462 wp_register_style($ltr_handle, $css_url . $css_fallback, $css_dependencies, $version);
463 }
464
465 // Register RTL CSS (depends on LTR version)
466 $rtl_handle = $ltr_handle . '-rtl';
467 $rtl_filename = $main_css . '.rtl' . $suffix . '.css';
468 $rtl_fallback = $main_css . '.rtl.css';
469
470 if (file_exists($css_path . $rtl_filename)) {
471 wp_register_style($rtl_handle, $css_url . $rtl_filename, [$ltr_handle], $version);
472 } elseif (file_exists($css_path . $rtl_fallback)) {
473 wp_register_style($rtl_handle, $css_url . $rtl_fallback, [$ltr_handle], $version);
474 }
475 }
476
477 // Register JS files (now an array)
478 // First script is the main addon script, rest are dependencies
479 if (!empty($assets['js'])) {
480 $js_files = (array) $assets['js'];
481 $js_dependencies = ['jquery'];
482
483 // Add vendor JS as dependencies (vendors are registered by Assets_Manager).
484 // Skip vendors that aren't actually registered (e.g. pro-only prism on an
485 // inactive license) to avoid "unregistered dependency" notices.
486 if (!empty($assets['vendors'])) {
487 foreach ((array) $assets['vendors'] as $vendor) {
488 $vendor_config = Assets_Manager::get($vendor);
489 if ($vendor_config && !empty($vendor_config['files']['js']) && wp_script_is('jltma-' . $vendor, 'registered')) {
490 $js_dependencies[] = 'jltma-' . $vendor;
491 }
492 }
493 }
494
495 // First script is the main addon script
496 $main_js = array_shift($js_files);
497
498 // Remaining scripts are dependencies (already registered as vendor scripts)
499 foreach ($js_files as $dep_js) {
500 $js_dependencies[] = $dep_js;
501 }
502
503 // Generate handle for main addon script
504 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $main_js);
505 $js_handle = 'master-addons-' . $handle_name;
506
507 // Try minified first, then unminified
508 $js_filename = $main_js . $suffix . '.js';
509 $js_fallback = $main_js . '.js';
510
511 // Determine which path to use: premium or free.
512 // Premium base is populated only when the pro plugin is active,
513 // so a premium version is preferred whenever one is shipped.
514 $js_url = $free_js_url;
515 $js_path = $free_js_path;
516
517 if (!empty($premium_js_path)
518 && (file_exists($premium_js_path . $js_filename) || file_exists($premium_js_path . $js_fallback))) {
519 $js_url = $premium_js_url;
520 $js_path = $premium_js_path;
521 }
522
523 if (file_exists($js_path . $js_filename)) {
524 wp_register_script($js_handle, $js_url . $js_filename, $js_dependencies, $version, true);
525 // Only add type="module" if the file actually uses ES module syntax (import/export)
526 // jQuery IIFEs and other non-module scripts break with type="module" in some browsers
527 if ($this->is_es_module_file($js_path . $js_filename)) {
528 $this->module_script_handles[] = $js_handle;
529 }
530 } elseif (file_exists($js_path . $js_fallback)) {
531 wp_register_script($js_handle, $js_url . $js_fallback, $js_dependencies, $version, true);
532 if ($this->is_es_module_file($js_path . $js_fallback)) {
533 $this->module_script_handles[] = $js_handle;
534 }
535 }
536 }
537
538 // Note: Vendor dependencies are registered by Assets_Manager::register_all()
539 // The 'vendors' array just contains handles to be enqueued at runtime
540 }
541 }
542
543 /**
544 * Detect widgets used on current page
545 */
546 public function detect_page_widgets($post_id = null)
547 {
548 if (!$post_id) {
549 $post_id = get_the_ID();
550 }
551
552 if (!$post_id) {
553 return [];
554 }
555
556 // Try cached post meta first (fast path)
557 $cached = get_post_meta($post_id, self::META_KEY, true);
558
559 if (!empty($cached) && is_array($cached)) {
560 return $cached;
561 }
562
563 // Fallback: Parse Elementor data at runtime
564 return $this->parse_elementor_widgets($post_id);
565 }
566
567 /**
568 * Parse Elementor data to find Master Addons widgets
569 */
570 private function parse_elementor_widgets($post_id)
571 {
572 $elementor_data = get_post_meta($post_id, '_elementor_data', true);
573
574 if (empty($elementor_data)) {
575 return [];
576 }
577
578 if (is_string($elementor_data)) {
579 $elementor_data = json_decode($elementor_data, true);
580 }
581
582 if (!is_array($elementor_data)) {
583 return [];
584 }
585
586 $widgets = [];
587 $this->extract_widgets_recursive($elementor_data, $widgets);
588
589 $unique_widgets = array_unique($widgets);
590
591 // Cache for future requests
592 update_post_meta($post_id, self::META_KEY, $unique_widgets);
593
594 return $unique_widgets;
595 }
596
597 /**
598 * Recursively extract widget names and enabled extensions from Elementor data
599 */
600 private function extract_widgets_recursive($elements, &$widgets)
601 {
602 if (!is_array($elements)) {
603 return;
604 }
605
606 foreach ($elements as $element) {
607 // Check if this is a Master Addons widget
608 if (isset($element['widgetType'])) {
609 $widget_type = $element['widgetType'];
610
611 // Track Master Addons widgets (ma-* or jltma-* prefix)
612 if (strpos($widget_type, 'ma-') === 0 || strpos($widget_type, 'jltma-') === 0) {
613 $widgets[] = $widget_type;
614 }
615 }
616
617 // Check for enabled extensions in element settings
618 // Extensions use settings like: ma_el_animated_gradient_enable, ma_el_particles_enable, etc.
619 if (!empty($element['settings']) && is_array($element['settings'])) {
620 foreach ($element['settings'] as $setting_key => $setting_value) {
621 // Match pattern: ma_el_{extension}_enable = 'yes'
622 if (preg_match('/^ma_el_(.+)_enable$/', $setting_key, $matches) && $setting_value === 'yes') {
623 // Convert underscores to hyphens: animated_gradient -> animated-gradient
624 $extension_key = str_replace('_', '-', $matches[1]);
625 $widgets[] = $extension_key;
626 }
627
628 // Match pattern: ma_el_enable_{extension} = 'yes' (e.g., ma_el_enable_particles, ma_el_enable_bg_slider)
629 if (preg_match('/^ma_el_enable_(.+)$/', $setting_key, $matches) && $setting_value === 'yes') {
630 $extension_key = str_replace('_', '-', $matches[1]);
631 $widgets[] = $extension_key;
632 }
633
634 // Match pattern: enabled_{extension} = 'yes' (e.g., enabled_rellax)
635 if (preg_match('/^enabled_(.+)$/', $setting_key, $matches) && $setting_value === 'yes') {
636 $extension_key = str_replace('_', '-', $matches[1]);
637 $widgets[] = $extension_key;
638 }
639 }
640 }
641
642 // Recurse into nested elements
643 if (!empty($element['elements'])) {
644 $this->extract_widgets_recursive($element['elements'], $widgets);
645 }
646 }
647 }
648
649 /**
650 * Enqueue only assets needed for current page (frontend)
651 */
652 public function enqueue_frontend_assets()
653 {
654 // Skip in editor - handled separately
655 if ($this->is_elementor_editor()) {
656 return;
657 }
658
659 $widgets = $this->detect_page_widgets();
660 $is_rtl = is_rtl();
661
662 // Track which files we've enqueued (to avoid duplicates)
663 $enqueued_css = [];
664 $enqueued_js = [];
665 $enqueued_vendor = [];
666
667 foreach ($widgets as $widget_name) {
668 // Try direct lookup first, then try without ma-/jltma-/ma-el- prefix
669 // This handles cases where config key is 'contact-form-7' but widget name is 'ma-contact-form-7'
670 // or widget name is 'ma-el-ninja-forms' but config key is 'ninja-forms'
671 $asset_key = $widget_name;
672 if (!isset($this->widget_assets[$asset_key])) {
673 // Try stripping ma-el-, ma- or jltma- prefix
674 $asset_key = preg_replace('/^(ma-el-|ma-|jltma-)/', '', $widget_name);
675 }
676
677 if (!isset($this->widget_assets[$asset_key])) {
678 continue;
679 }
680
681 $assets = $this->widget_assets[$asset_key];
682
683 // Enqueue CSS files (now an array)
684 if (!empty($assets['css'])) {
685 foreach ((array) $assets['css'] as $css_file) {
686 // Skip if already enqueued
687 if (isset($enqueued_css[$css_file])) {
688 continue;
689 }
690
691 // Generate handle: master-addons-flipbox (strip ma- prefix)
692 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $css_file);
693 $css_handle = 'master-addons-' . $handle_name;
694
695 // Enqueue CSS (LTR or RTL based on site setting)
696 if ($is_rtl) {
697 $rtl_handle = $css_handle . '-rtl';
698 if (wp_style_is($rtl_handle, 'registered')) {
699 wp_enqueue_style($rtl_handle);
700 $enqueued_css[$css_file] = true;
701 continue;
702 }
703 }
704
705 if (wp_style_is($css_handle, 'registered')) {
706 wp_enqueue_style($css_handle);
707 $enqueued_css[$css_file] = true;
708 }
709 }
710 }
711
712 // Enqueue JS files (now an array)
713 if (!empty($assets['js'])) {
714 foreach ((array) $assets['js'] as $js_file) {
715 if (isset($enqueued_js[$js_file])) {
716 continue;
717 }
718
719 // Generate handle: master-addons-flipbox (strip ma- prefix)
720 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $js_file);
721 $js_handle = 'master-addons-' . $handle_name;
722
723 if (wp_script_is($js_handle, 'registered')) {
724 wp_enqueue_script($js_handle);
725 $enqueued_js[$js_file] = true;
726 }
727 }
728 }
729
730 // Enqueue vendor dependencies (simple array of handles from Assets_Manager)
731 if (!empty($assets['vendors'])) {
732 foreach ((array) $assets['vendors'] as $vendor) {
733 if (isset($enqueued_vendor[$vendor])) {
734 continue;
735 }
736
737 // Use Assets_Manager to enqueue vendor and all its dependencies
738 Assets_Manager::enqueue($vendor);
739 $enqueued_vendor[$vendor] = true;
740 }
741 }
742 }
743
744 }
745
746 /**
747 * Load ALL enabled addon CSS in editor/preview
748 */
749 public function enqueue_editor_assets()
750 {
751 $enabled_widgets = $this->get_enabled_widgets();
752 $is_rtl = is_rtl();
753
754 foreach ($this->widget_assets as $widget_name => $assets) {
755 // Only load if widget is enabled
756 if (!in_array($widget_name, $enabled_widgets)) {
757 continue;
758 }
759
760 // Enqueue CSS files (now an array)
761 if (!empty($assets['css'])) {
762 foreach ((array) $assets['css'] as $css_file) {
763 // Generate handle: master-addons-flipbox (strip ma- prefix)
764 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $css_file);
765 $css_handle = 'master-addons-' . $handle_name;
766
767 // Enqueue LTR
768 if (wp_style_is($css_handle, 'registered')) {
769 wp_enqueue_style($css_handle);
770 }
771
772 // Also enqueue RTL in editor for live preview switching
773 if ($is_rtl) {
774 $rtl_handle = $css_handle . '-rtl';
775 if (wp_style_is($rtl_handle, 'registered')) {
776 wp_enqueue_style($rtl_handle);
777 }
778 }
779 }
780 }
781
782 // Enqueue vendor assets in editor
783 if (!empty($assets['vendors'])) {
784 foreach ((array) $assets['vendors'] as $vendor) {
785 Assets_Manager::enqueue($vendor);
786 }
787 }
788 }
789
790 }
791
792 /**
793 * Load ALL enabled addon JS in editor/preview
794 */
795 public function enqueue_editor_scripts()
796 {
797 $enabled_widgets = $this->get_enabled_widgets();
798
799 foreach ($this->widget_assets as $widget_name => $assets) {
800 if (!in_array($widget_name, $enabled_widgets)) {
801 continue;
802 }
803
804 // Enqueue JS files (now an array)
805 if (!empty($assets['js'])) {
806 foreach ((array) $assets['js'] as $js_file) {
807 // Generate handle: master-addons-flipbox (strip ma- prefix)
808 $handle_name = preg_replace('/^(ma-|jltma-)/', '', $js_file);
809 $js_handle = 'master-addons-' . $handle_name;
810 if (wp_script_is($js_handle, 'registered')) {
811 wp_enqueue_script($js_handle);
812 }
813 }
814 }
815
816 // Note: Vendor assets already enqueued in enqueue_editor_assets()
817 // Assets_Manager handles both CSS and JS when enqueue() is called
818 }
819 }
820
821 /**
822 * Localize addon scripts with required data
823 *
824 * Script-specific localization:
825 * - ma-data-table: DataTable translation strings (JLTMA_DATA_TABLE)
826 * - ma-bg-slider: Plugin URL for overlay images (jltma_scripts)
827 * - ma-restrict-content: AJAX URL for password verification (jltma_scripts)
828 */
829 public function localize_addon_scripts()
830 {
831 // Data Table localization
832 if (wp_script_is('master-addons-data-table', 'enqueued') || wp_script_is('master-addons-data-table', 'registered')) {
833 $jltma_data_table_vars = array(
834 'lengthMenu' => esc_html__('Display _MENU_ records per page', 'master-addons'),
835 'zeroRecords' => esc_html__('Nothing found - sorry', 'master-addons'),
836 'info' => esc_html__('Showing page _PAGE_ of _PAGES_', 'master-addons'),
837 'infoEmpty' => esc_html__('No records available', 'master-addons'),
838 'infoFiltered' => esc_html__('(filtered from _MAX_ total records)', 'master-addons'),
839 'searchPlaceholder' => esc_html__('Search...', 'master-addons'),
840 'processing' => esc_html__('Processing...', 'master-addons'),
841 'csvHtml5' => esc_html__('CSV', 'master-addons'),
842 'excelHtml5' => esc_html__('Excel', 'master-addons'),
843 'pdfHtml5' => esc_html__('PDF', 'master-addons'),
844 'print' => esc_html__('Print', 'master-addons'),
845 );
846 wp_localize_script('master-addons-data-table', 'JLTMA_DATA_TABLE', $jltma_data_table_vars);
847 }
848
849 // JLTMA_SCRIPTS is a single shared JS global used by several scripts
850 // (bg-slider needs plugin_url; restrict-content needs ajaxurl + nonce).
851 // In the Elementor editor every widget script is loaded, so localizing
852 // only a subset per handle lets one script's data overwrite another's on
853 // the shared global (e.g. bg-slider wiping the restrict-content nonce ->
854 // "Security check failed"). Localize the FULL key set on every handle so
855 // whichever wins still has everything.
856 $jltma_shared_scripts_data = array(
857 'plugin_url' => defined('JLTMA_URL') ? JLTMA_URL : (defined('JLTMA_PRO_URL') ? untrailingslashit(JLTMA_PRO_URL) : ''),
858 'assets_url' => defined('JLTMA_ASSETS') ? untrailingslashit(JLTMA_ASSETS) : (defined('JLTMA_PRO_ASSETS') ? untrailingslashit(JLTMA_PRO_ASSETS) : ''),
859 'ajaxurl' => admin_url('admin-ajax.php'),
860 'nonce' => wp_create_nonce('master-addons-elementor'),
861 );
862
863 // Background Slider localization (needs plugin_url for Vegas overlay images)
864 if (wp_script_is('master-addons-bg-slider', 'enqueued') || wp_script_is('master-addons-bg-slider', 'registered')) {
865 wp_localize_script('master-addons-bg-slider', 'JLTMA_SCRIPTS', $jltma_shared_scripts_data);
866 }
867
868 // Restrict Content localization (needs ajaxurl + nonce for AJAX calls)
869 if (wp_script_is('master-addons-restrict-content', 'enqueued') || wp_script_is('master-addons-restrict-content', 'registered')) {
870 wp_localize_script('master-addons-restrict-content', 'JLTMA_SCRIPTS', $jltma_shared_scripts_data);
871 }
872
873 // Allow extensions to add more localizations
874 do_action('jltma/assets/localize_scripts');
875 }
876
877 /**
878 * Get list of enabled widgets from settings
879 */
880 private function get_enabled_widgets()
881 {
882 $settings = \MasterAddons\Inc\Admin\Settings\Settings::get_addons() ?: [];
883
884 $enabled = [];
885 foreach ($settings as $widget_key => $is_enabled) {
886 if ($is_enabled) {
887 $enabled[] = $widget_key;
888 }
889 }
890
891 return $enabled;
892 }
893
894 /**
895 * Update widget cache when post is saved in Elementor
896 */
897 public function update_widget_cache($post_id, $editor_data)
898 {
899 $widgets = [];
900 $this->extract_widgets_recursive($editor_data, $widgets);
901
902 $unique_widgets = array_unique($widgets);
903 update_post_meta($post_id, self::META_KEY, $unique_widgets);
904
905 // Trigger cache invalidation hook for Cache Manager
906 do_action('jltma/cache/invalidate_post', $post_id);
907 }
908
909 /**
910 * Clear cached widget list for a post
911 */
912 public function clear_post_cache($post_id)
913 {
914 delete_post_meta($post_id, self::META_KEY);
915 }
916
917 /**
918 * Check if a JS file is an ES module by looking for import/export statements
919 * Only files with actual ES module syntax should get type="module"
920 * jQuery IIFEs and other traditional scripts should load as regular scripts
921 */
922 private function is_es_module_file($file_path)
923 {
924 // Read just the first 512 bytes — module indicators are always near the top
925 $content = @file_get_contents($file_path, false, null, 0, 512);
926 if ($content === false) {
927 return false;
928 }
929
930 // Check for ES module import/export syntax
931 if (preg_match('/\b(import\s*[{"\']|import\s+\w|export\s+(default|{|\w))/', $content)) {
932 return true;
933 }
934
935 // Check if file starts with top-level const/let/class declarations
936 // These indicate Vite ES module output where imports were inlined/resolved
937 // Non-module scripts start with IIFE patterns: (function, ;(function, jQuery(, !function, var
938 $trimmed = ltrim($content);
939 if (preg_match('/^(const |let |class )\w/', $trimmed)) {
940 return true;
941 }
942
943 return false;
944 }
945
946 /**
947 * Check if we're in Elementor editor or preview
948 */
949 private function is_elementor_editor()
950 {
951 if (!class_exists('\Elementor\Plugin')) {
952 return false;
953 }
954
955 $elementor = \Elementor\Plugin::$instance;
956
957 if (!$elementor || !isset($elementor->editor) || !isset($elementor->preview)) {
958 return false;
959 }
960
961 return $elementor->editor->is_edit_mode() || $elementor->preview->is_preview_mode();
962 }
963
964 /**
965 * Get widget assets map (for Cache Manager)
966 */
967 public function get_widget_assets()
968 {
969 return $this->widget_assets;
970 }
971
972 /**
973 * Manually trigger asset detection for a post
974 */
975 public function refresh_post_cache($post_id)
976 {
977 delete_post_meta($post_id, self::META_KEY);
978 return $this->parse_elementor_widgets($post_id);
979 }
980
981 /**
982 * Get stats about widget usage
983 */
984 public function get_usage_stats()
985 {
986 global $wpdb;
987
988 $meta_key = self::META_KEY;
989
990 // Get all posts with cached widget data
991 $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- direct query required for bulk postmeta scan across all posts; no core API supports this efficiently
992 $wpdb->prepare(
993 "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
994 $meta_key
995 )
996 );
997
998 $stats = [
999 'total_posts' => count($results),
1000 'widget_usage' => [],
1001 ];
1002
1003 foreach ($results as $row) {
1004 $widgets = maybe_unserialize($row->meta_value);
1005 if (is_array($widgets)) {
1006 foreach ($widgets as $widget) {
1007 if (!isset($stats['widget_usage'][$widget])) {
1008 $stats['widget_usage'][$widget] = 0;
1009 }
1010 $stats['widget_usage'][$widget]++;
1011 }
1012 }
1013 }
1014
1015 // Sort by usage count
1016 arsort($stats['widget_usage']);
1017
1018 return $stats;
1019 }
1020 }
1021