PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.1.2
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.1.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 / admin / widget-builder / class-widget-admin.php

class-widget-admin.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.1.2, at inc/admin/widget-builder/class-widget-admin.php

973 lines 48.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Master Addons Widget Builder Admin Page
4 * Similar to Popup Builder Admin
5 *
6 * @package MasterAddons
7 * @subpackage WidgetBuilder
8 */
9
10 namespace MasterAddons\Inc\Admin\WidgetBuilder;
11
12 use MasterAddons\Inc\Classes\Helper;
13 use MasterAddons\Inc\Classes\Assets_Manager;
14
15 if (!defined('ABSPATH')) {
16 exit;
17 }
18
19 class Widget_Admin {
20
21 private static $instance = null;
22 private $widget_cpt;
23
24 public static function get_instance() {
25 if (self::$instance === null) {
26 self::$instance = new self();
27 }
28 return self::$instance;
29 }
30
31 public function __construct() {
32 $this->widget_cpt = Widget_CPT::get_instance();
33 $this->init_hooks();
34
35 }
36
37 private function init_hooks() {
38 add_action('admin_menu', [$this, 'add_admin_menu'], 56);
39 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
40 add_action('admin_footer', [$this, 'render_widget_modals_in_footer']);
41 add_action('wp_ajax_jltma_widget_get_data', [$this, 'get_widget_data']);
42 add_action('wp_ajax_jltma_widget_save_data', [$this, 'save_widget_data']);
43 add_action('wp_ajax_jltma_widget_delete', [$this, 'delete_widget']);
44 add_action('wp_ajax_jltma_widget_update_category', [$this, 'update_widget_category']);
45 add_action('wp_ajax_jltma_widget_get_conditions', [$this, 'get_widget_conditions']);
46 add_action('wp_ajax_jltma_widget_save_conditions', [$this, 'save_widget_conditions']);
47 add_action('wp_ajax_jltma_widget_render_preview', [$this, 'render_preview']);
48 add_filter('submenu_file', [$this, 'highlight_widget_menu'], 10, 2);
49 add_action('admin_print_styles', [$this, 'hide_admin_notices']);
50 add_action('current_screen', [$this, 'set_editor_page_title']);
51 }
52
53 /**
54 * Give the hidden editor page a title.
55 *
56 * The editor is a hidden submenu (parent ''), so WordPress can't resolve a
57 * title and leaves the global $title null — which trips a strip_tags(null)
58 * deprecation in wp-admin/admin-header.php on PHP 8.1+. current_screen fires
59 * before admin-header renders, so set it here.
60 *
61 * @param \WP_Screen $screen Current admin screen.
62 */
63 public function set_editor_page_title($screen) {
64 if ($screen && 'admin_page_jltma-widget-editor' === $screen->id && empty($GLOBALS['title'])) {
65 $GLOBALS['title'] = esc_html__('Edit Widget', 'master-addons');
66 }
67 }
68
69 public function add_admin_menu() {
70 // Main list → native CPT list (no callback)
71 add_submenu_page(
72 'master-addons-settings',
73 __('Widget Builder', 'master-addons'),
74 __('Widget Builder', 'master-addons'),
75 'manage_options',
76 'edit.php?post_type=jltma_widget'
77 );
78
79 // Hidden editor page for React app
80 add_submenu_page(
81 '',
82 __('Edit Widget', 'master-addons'),
83 __('Edit Widget', 'master-addons'),
84 'manage_options',
85 'jltma-widget-editor',
86 [$this, 'render_widget_editor_page']
87 );
88 }
89
90 /**
91 * Full catalog of Pro controls listed in the Widget Builder sidebar.
92 *
93 * Single source of truth. Each item is flagged `isPro => true` so the free
94 * UI shows every control with a Pro badge (locked). Pro unlocks them through
95 * the `jltma_widget_builder_pro_controls` filter (which flips isPro to false)
96 * — it must not redefine this list.
97 *
98 * @return array
99 */
100 public function get_pro_controls_catalog() {
101 return [
102 ['type' => 'number', 'label' => 'Number', 'icon' => 'eicon-number-field', 'category' => 'basic', 'description' => 'Numeric input field', 'isPro' => true],
103 ['type' => 'switcher', 'label' => 'Switcher', 'icon' => 'eicon-toggle', 'category' => 'basic', 'description' => 'Toggle switch (yes/no)', 'isPro' => true],
104 ['type' => 'select', 'label' => 'Select', 'icon' => 'eicon-select', 'category' => 'basic', 'description' => 'Dropdown select field', 'isPro' => true],
105 ['type' => 'select2', 'label' => 'Select2', 'icon' => 'eicon-select', 'category' => 'basic', 'description' => 'Advanced select with search', 'isPro' => true],
106 ['type' => 'choose', 'label' => 'Choose', 'icon' => 'eicon-checkbox', 'category' => 'basic', 'description' => 'Icon-based choice control', 'isPro' => true],
107 ['type' => 'wysiwyg', 'label' => 'WYSIWYG', 'icon' => 'eicon-editor-paragraph', 'category' => 'basic', 'description' => 'Rich text editor', 'isPro' => true],
108 ['type' => 'code', 'label' => 'Code', 'icon' => 'eicon-code', 'category' => 'basic', 'description' => 'Code editor field', 'isPro' => true],
109 ['type' => 'date_time', 'label' => 'Date Time', 'icon' => 'eicon-calendar', 'category' => 'basic', 'description' => 'Date and time picker', 'isPro' => true],
110 ['type' => 'media', 'label' => 'Media', 'icon' => 'eicon-image', 'category' => 'basic', 'description' => 'Image/video uploader', 'isPro' => true],
111 ['type' => 'gallery', 'label' => 'Gallery', 'icon' => 'eicon-gallery-grid', 'category' => 'basic', 'description' => 'Image gallery uploader', 'isPro' => true],
112 ['type' => 'icons', 'label' => 'Icons', 'icon' => 'eicon-star', 'category' => 'basic', 'description' => 'Icon picker control', 'isPro' => true],
113 ['type' => 'slider', 'label' => 'Slider', 'icon' => 'eicon-slider-device', 'category' => 'basic', 'description' => 'Range slider with min/max/step', 'isPro' => true],
114 ['type' => 'popover_toggle', 'label' => 'Popover Toggle', 'icon' => 'eicon-edit', 'category' => 'basic', 'description' => 'Popover toggle button', 'isPro' => true],
115 ['type' => 'visual_choice', 'label' => 'Visual Choice', 'icon' => 'eicon-photo-library', 'category' => 'basic', 'description' => 'Visual choice with images', 'isPro' => true],
116 ['type' => 'font', 'label' => 'Font', 'icon' => 'eicon-text', 'category' => 'style', 'description' => 'Font selector', 'isPro' => true],
117 ['type' => 'typography', 'label' => 'Typography', 'icon' => 'eicon-typography', 'category' => 'style', 'description' => 'Typography group control', 'isPro' => true],
118 ['type' => 'dimensions', 'label' => 'Dimensions', 'icon' => 'eicon-cursor-move', 'category' => 'style', 'description' => 'Margin/padding control', 'isPro' => true],
119 ['type' => 'box_shadow', 'label' => 'Box Shadow', 'icon' => 'eicon-lightbox', 'category' => 'style', 'description' => 'Box shadow control', 'isPro' => true],
120 ['type' => 'background', 'label' => 'Background', 'icon' => 'eicon-paint-brush', 'category' => 'style', 'description' => 'Background control', 'isPro' => true],
121 ['type' => 'border', 'label' => 'Border', 'icon' => 'eicon-border', 'category' => 'style', 'description' => 'Border group control', 'isPro' => true],
122 ['type' => 'text_shadow', 'label' => 'Text Shadow', 'icon' => 'eicon-typography', 'category' => 'style', 'description' => 'Text shadow control', 'isPro' => true],
123 ['type' => 'divider', 'label' => 'Divider', 'icon' => 'eicon-divider', 'category' => 'layout', 'description' => 'Visual divider between controls', 'isPro' => true],
124 ['type' => 'repeater', 'label' => 'Repeater', 'icon' => 'eicon-sync', 'category' => 'advanced', 'description' => 'Repeater for multiple items', 'isPro' => true],
125 ['type' => 'tabs', 'label' => 'Tabs', 'icon' => 'eicon-tabs', 'category' => 'advanced', 'description' => 'Tabs control', 'isPro' => true],
126 ];
127 }
128
129 public function highlight_widget_menu($submenu_file, $parent_file) {
130 global $current_screen;
131
132 if ($current_screen && (
133 $current_screen->post_type === $this->widget_cpt->get_post_type() ||
134 $current_screen->id === 'admin_page_jltma-widget-editor'
135 )) {
136 $submenu_file = 'edit.php?post_type=jltma_widget';
137 }
138
139 return $submenu_file;
140 }
141
142 /**
143 * Render widget editor page (React app) — hidden submenu page
144 */
145 public function render_widget_editor_page() {
146 $widget_id = isset($_GET['widget_id']) ? absint( $_GET['widget_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display routing, no state change
147
148 if (!$widget_id) {
149 wp_safe_redirect(admin_url('edit.php?post_type=jltma_widget'));
150 exit;
151 }
152
153 ?>
154 <div class="wrap jltma-widget-builder-wrap">
155 <div id="jltma-widget-builder-app"></div>
156 </div>
157 <?php
158 }
159
160 /**
161 * Render widget modals in footer
162 */
163 public function render_widget_modals_in_footer() {
164 $screen = get_current_screen();
165 if ($screen && ($screen->id === 'edit-jltma_widget' || $screen->id === 'admin_page_jltma-widget-editor')) {
166 $this->render_widget_modal();
167 }
168 }
169
170 private function render_widget_modal() {
171 // Get Elementor categories
172 $elementor_categories = $this->get_elementor_categories();
173
174 ?>
175 <!-- Widget Settings Modal -->
176 <div id="jltma_widget_builder_modal" class="jltma_widget_builder_modal jltma-modal">
177 <div class="jltma-modal-backdrop"></div>
178 <div class="jltma-modal-dialog">
179 <div class="jltma-modal-content">
180 <div class="jltma-pop-contents-head">
181 <div class="jltma-header-top">
182 <div class="jltma-popup-head-content" style="display: flex; align-items:center; gap:8px">
183 <span>
184 <img src="<?php echo esc_url( JLTMA_IMAGE_DIR . 'logo.svg' ); ?>" style="width: 30px;">
185 </span>
186 <h3 class="jltma-modal-title"><?php echo esc_html__('Edit Widget', 'master-addons'); ?></h3>
187 </div>
188 <div class="jltma-pop-close">
189 <button class="close-btn" data-dismiss="modal"><span class="dashicons dashicons-no-alt"></span></button>
190 </div>
191 </div>
192 </div>
193
194 <div class="jltma-pop-contents-body" id="jltma_widget_modal_body">
195 <div class="jltma-spinner"></div>
196
197 <div class="jltma-pop-contents-padding">
198 <form id="jltma-widget-form">
199 <input type="hidden" name="widget_id" id="jltma_widget_id" value="">
200
201 <div class="jltma-modal-content-area">
202 <div class="jltma-tab-content jltma-tab-general active">
203 <div class="jltma-label-container">
204 <div class="jltma-row">
205
206 <div class="jltma-form-group mb-2 jltma-col-6">
207 <label for="jltma_widget_title" style="font-size: 16px;">
208 <strong><?php esc_html_e('Widget Title', 'master-addons'); ?> <span style="color: red;">*</span></strong>
209 </label>
210 </div>
211 <div class="jltma-form-group mb-2 jltma-col-6">
212 <input required type="text" name="widget_title" id="jltma_widget_title" class="jltma-form-control" placeholder="<?php echo esc_attr__('Enter a descriptive name for your widget', 'master-addons'); ?>">
213 </div>
214
215 <div class="jltma-form-group mb-2 jltma-col-6">
216 <label for="jltma_widget_category" style="font-size: 16px;">
217 <strong><?php esc_html_e('Widget Category', 'master-addons'); ?></strong>
218 </label>
219 </div>
220 <div class="jltma-form-group mb-2 jltma-col-6">
221 <select name="widget_category" id="jltma_widget_category" class="jltma-form-control">
222 <?php if (!empty($elementor_categories)) : ?>
223 <?php foreach ($elementor_categories as $category_slug => $category_data) : ?>
224 <option value="<?php echo esc_attr($category_slug); ?>">
225 <?php echo esc_html($category_data['title']); ?>
226 </option>
227 <?php endforeach; ?>
228 <?php else : ?>
229 <option value="general"><?php esc_html_e('General', 'master-addons'); ?></option>
230 <option value="basic"><?php esc_html_e('Basic', 'master-addons'); ?></option>
231 <option value="pro"><?php esc_html_e('Pro', 'master-addons'); ?></option>
232 <?php endif; ?>
233 <option value="__add_new__" data-is-pro="<?php echo Helper::jltma_premium() ? '0' : '1'; ?>">
234 <?php echo Helper::jltma_premium()
235 ? esc_html__('+ Add New Category', 'master-addons')
236 : esc_html__('+ Add New Category (Pro)', 'master-addons');
237 ?>
238 </option>
239 </select>
240 <p class="description" style="margin-top: 8px; font-size: 12px; color: #6c757d;">
241 <?php esc_html_e('Select an Elementor category for this widget.', 'master-addons'); ?>
242 </p>
243
244 <!-- Inline Add New Category -->
245 <div id="jltma-inline-category-add" style="display: none; margin-top: 12px;">
246 <input type="text" id="jltma_new_category_title" placeholder="<?php esc_attr_e('Enter Category name', 'master-addons'); ?>" style="width: 100%; padding: 6px 12px; border: 1px solid #5b6aff; border-radius: 6px; font-size: 14px; outline: none; margin-bottom: 12px; transition: border-color 0.2s ease;">
247 <div style="display: flex; gap: 12px; align-items: center; justify-content: flex-start;">
248 <button type="button" class="button button-primary jltma-save-inline-category" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 20px; background: var(--jltma-primary-glow); color: #fff; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s ease;">
249 <span class="dashicons dashicons-plus-alt"></span>
250 <?php esc_html_e('Add Category', 'master-addons'); ?>
251 </button>
252 <button type="button" class="button jltma-cancel-inline-category" style="background: transparent; color: #ff4757; border: 2px solid #ff4757; padding: 2px 20px; border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: none; text-shadow: none; white-space: nowrap;"><?php esc_html_e('Cancel', 'master-addons'); ?></button>
253 </div>
254 <p style="margin-top: 12px; font-size: 13px; color: #8a909a; font-style: italic; margin-bottom: 0;">
255 <?php esc_html_e('Enter a name for the new category and click Add Category.', 'master-addons'); ?>
256 </p>
257 </div>
258 </div>
259
260 </div>
261 </div>
262 </div>
263 </div>
264
265 <!-- Modal Footer -->
266 <div class="jltma-modal-footer">
267 <button type="submit" class="jltma-save-widget jltma-save-btn jltma-color-two">
268 <?php esc_html_e('Save Changes', 'master-addons'); ?>
269 </button>
270 </div>
271 </form>
272 </div>
273 </div>
274 </div>
275 </div>
276 </div>
277
278 <!-- Import Widget Dropzone Modal -->
279 <div id="jltma_import_widget_modal" class="jltma-import-modal" style="display: none;">
280 <div class="jltma-import-backdrop"></div>
281 <div class="jltma-import-dialog">
282 <div class="jltma-import-header">
283 <h3><?php esc_html_e('Import Widget', 'master-addons'); ?></h3>
284 <button class="jltma-import-close">
285 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
286 </button>
287 </div>
288 <div class="jltma-import-body">
289 <div class="jltma-dropzone" id="jltma-dropzone">
290 <div class="jltma-dropzone-content">
291 <div class="jltma-upload-icon-circle">
292 <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
293 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
294 <polyline points="17 8 12 3 7 8"></polyline>
295 <line x1="12" y1="3" x2="12" y2="15"></line>
296 </svg>
297 </div>
298 <h4><?php esc_html_e('Import Widget', 'master-addons'); ?></h4>
299 <p class="jltma-dropzone-subtitle"><?php esc_html_e('Drag & drop your .json file here', 'master-addons'); ?></p>
300 <label class="jltma-browse-btn">
301 <?php esc_html_e('Browse files', 'master-addons'); ?>
302 <input type="file" id="jltma-import-file" accept=".json,application/json" style="display: none;" />
303 </label>
304 <p class="jltma-supported-format"><?php esc_html_e('Only .json files are supported', 'master-addons'); ?></p>
305 </div>
306 <div class="jltma-importing" style="display: none;">
307 <div class="jltma-import-progress">
308 <div class="jltma-progress-info">
309 <span class="jltma-progress-label"><?php esc_html_e('Importing...', 'master-addons'); ?></span>
310 </div>
311 <div class="jltma-progress-bar">
312 <div class="jltma-progress-fill"></div>
313 </div>
314 <p class="jltma-progress-hint"><?php esc_html_e('Do not close this window', 'master-addons'); ?></p>
315 </div>
316 </div>
317 </div>
318 </div>
319 </div>
320 </div>
321
322 <style>
323 /* Import Widget Modal - shadcn/ui style (matches Template Kit upload modal) */
324 .jltma-import-modal {
325 position: fixed;
326 top: 0;
327 left: 0;
328 right: 0;
329 bottom: 0;
330 z-index: 999999;
331 display: flex;
332 justify-content: center;
333 align-items: center;
334 }
335 .jltma-import-backdrop {
336 position: absolute;
337 top: 0;
338 left: 0;
339 right: 0;
340 bottom: 0;
341 background: rgba(0, 0, 0, 0.5);
342 backdrop-filter: blur(4px);
343 }
344 .jltma-import-dialog {
345 position: relative;
346 max-width: 520px;
347 width: 90%;
348 background: #ffffff;
349 border: 1px solid hsl(214.3 31.8% 91.4%);
350 border-radius: 12px;
351 box-shadow: 0 16px 70px -12px rgba(0, 0, 0, 0.25);
352 z-index: 1;
353 }
354 .jltma-import-header {
355 padding: 16px;
356 border-bottom: 1px solid hsl(220 13% 91%);
357 display: flex;
358 align-items: center;
359 justify-content: space-between;
360 }
361 .jltma-import-header h3 {
362 margin: 0;
363 font-size: 16px;
364 font-weight: 600;
365 color: hsl(222.2 84% 4.9%);
366 letter-spacing: -0.025em;
367 }
368 .jltma-import-close {
369 background: transparent;
370 border: none;
371 color: hsl(215.4 16.3% 46.9%);
372 cursor: pointer;
373 padding: 0;
374 width: 32px;
375 height: 32px;
376 display: flex;
377 align-items: center;
378 justify-content: center;
379 border-radius: 6px;
380 transition: all 0.15s ease;
381 }
382 .jltma-import-close:hover {
383 background: hsl(210 40% 96.1%);
384 color: hsl(222.2 84% 4.9%);
385 }
386 .jltma-import-body {
387 padding: 16px;
388 }
389 .jltma-dropzone {
390 border: 1.5px dashed hsl(214.3 31.8% 91.4%);
391 border-radius: 10px;
392 padding: 40px 24px;
393 text-align: center;
394 background: hsl(210 40% 98.5%);
395 transition: all 0.2s ease;
396 cursor: pointer;
397 }
398 .jltma-dropzone:hover,
399 .jltma-dropzone.dragging {
400 border-color: hsl(222.2 84% 4.9% / 0.3);
401 background: hsl(210 40% 97%);
402 }
403 .jltma-dropzone.dragging {
404 border-color: hsl(222.2 84% 4.9% / 0.5);
405 background: hsl(210 40% 96%);
406 }
407 .jltma-dropzone-content {
408 display: flex;
409 flex-direction: column;
410 align-items: center;
411 gap: 0;
412 }
413 .jltma-upload-icon-circle {
414 width: 48px;
415 height: 48px;
416 border-radius: 50%;
417 border: 1px solid hsl(214.3 31.8% 91.4%);
418 background: #ffffff;
419 display: flex;
420 align-items: center;
421 justify-content: center;
422 color: hsl(215.4 16.3% 46.9%);
423 margin-bottom: 16px;
424 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
425 }
426 .jltma-dropzone h4 {
427 color: hsl(222.2 84% 4.9%);
428 font-size: 14px;
429 font-weight: 600;
430 letter-spacing: -0.01em;
431 margin: 0;
432 }
433 .jltma-dropzone-subtitle {
434 margin: 4px 0 0 !important;
435 font-size: 13px !important;
436 color: hsl(215.4 16.3% 46.9%) !important;
437 }
438 .jltma-browse-btn {
439 display: inline-block;
440 margin-top: 16px;
441 padding: 8px 16px;
442 font-size: 13px;
443 font-weight: 500;
444 color: hsl(222.2 84% 4.9%);
445 background: #ffffff;
446 border: 1px solid hsl(214.3 31.8% 91.4%);
447 border-radius: 6px;
448 cursor: pointer;
449 transition: all 0.15s ease;
450 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
451 text-transform: none;
452 letter-spacing: normal;
453 }
454 .jltma-browse-btn:hover {
455 background: hsl(210 40% 96.1%);
456 border-color: hsl(214.3 31.8% 85%);
457 transform: none;
458 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
459 }
460 .jltma-supported-format {
461 color: hsl(215.4 16.3% 60%) !important;
462 font-size: 12px !important;
463 margin: 12px 0 0 0 !important;
464 }
465 .jltma-importing {
466 text-align: center;
467 }
468 .jltma-import-progress {
469 padding: 8px 0;
470 }
471 .jltma-progress-info {
472 display: flex;
473 justify-content: center;
474 margin-bottom: 12px;
475 }
476 .jltma-progress-label {
477 font-size: 13px;
478 font-weight: 500;
479 color: hsl(222.2 84% 4.9%);
480 }
481 .jltma-progress-bar {
482 height: 6px;
483 background: hsl(214.3 31.8% 91.4%);
484 border-radius: 100px;
485 overflow: hidden;
486 }
487 .jltma-progress-fill {
488 height: 100%;
489 width: 100%;
490 background: hsl(222.2 84% 4.9%);
491 border-radius: 100px;
492 animation: jltma-progress-indeterminate 1.5s ease-in-out infinite;
493 }
494 .jltma-progress-hint {
495 margin: 8px 0 0 !important;
496 font-size: 12px !important;
497 color: hsl(215.4 16.3% 60%) !important;
498 }
499 @keyframes jltma-progress-indeterminate {
500 0% { transform: translateX(-100%); }
501 50% { transform: translateX(0%); }
502 100% { transform: translateX(100%); }
503 }
504 </style>
505 <?php
506 }
507
508 public function enqueue_admin_assets($hook) {
509 $screen = get_current_screen();
510 if (!$screen) {
511 return;
512 }
513
514 $is_list = ($screen->id === 'edit-jltma_widget');
515 $is_editor = ($screen->id === 'admin_page_jltma-widget-editor');
516
517 if (!$is_list && !$is_editor) {
518 return;
519 }
520
521 // Enqueue WordPress Media Library
522 if (!did_action('wp_enqueue_media')) {
523 wp_enqueue_media();
524 }
525
526 // Enqueue Icon Library CSS Files
527 $icon_library_helper = Icon_Library_Helper::get_instance();
528 $icon_library_helper->enqueue_icon_libraries();
529
530 // Enqueue all Widget Builder assets via Assets_Manager
531 Assets_Manager::enqueue(['widget-builder', 'widget-admin', 'widget-builder-app']);
532
533 // Localize script for list view
534 wp_localize_script('jltma-widget-admin', 'jltmaWidgetAdmin', [
535 'ajax_url' => admin_url('admin-ajax.php'),
536 'admin_url' => admin_url(),
537 'rest_url' => rest_url('jltma/v1'),
538 'widget_nonce' => wp_create_nonce('jltma_widget_nonce'),
539 'rest_nonce' => wp_create_nonce('wp_rest'),
540 'strings' => [
541 'confirm_delete' => __('Are you sure you want to delete this widget?', 'master-addons'),
542 'saving' => __('Saving...', 'master-addons'),
543 'saved' => __('Widget saved successfully!', 'master-addons'),
544 'error' => __('An error occurred. Please try again.', 'master-addons'),
545 'widget_title_required' => __('Widget title is required.', 'master-addons'),
546 'category_added' => __('Category added successfully!', 'master-addons'),
547 'copied' => __('Copied to clipboard!', 'master-addons'),
548 ]
549 ]);
550
551 // Localize script for React app
552 $widget_id = isset($_GET['widget_id']) ? absint( $_GET['widget_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only localize data, no state change
553 $localize_data = [
554 'ajaxurl' => admin_url('admin-ajax.php'),
555 'nonce' => wp_create_nonce('wp_rest'),
556 'widget_id' => $widget_id,
557 'rest_url' => rest_url('jltma/v1'),
558 'apiBase' => rest_url('jltma/v1'),
559 'pluginUrl' => JLTMA_URL,
560 'aceBaseUrl' => untrailingslashit(JLTMA_ASSETS) . '/vendor/ace',
561 'isPro' => Helper::jltma_premium(),
562 // Full Pro controls catalog (single source). Each item is flagged
563 // isPro => true so the free UI shows every control with a Pro badge
564 // (locked). Pro unlocks them via the jltma_widget_builder_pro_controls
565 // filter (sets isPro => false); it does not redefine the list.
566 'proControls' => apply_filters('jltma_widget_builder_pro_controls', $this->get_pro_controls_catalog()),
567 // Documentation links surfaced in the editor UI. docsUrl is the base
568 // (used for the sidebar "Documentation" link); each control type maps
569 // to its own page below. Edit any URL freely.
570 'docsUrl' => 'https://master-addons.com/docs/widget-builder',
571 'controlDocs' => [
572 'heading' => 'https://master-addons.com/docs/widget-builder/heading/',
573 'hidden' => 'https://master-addons.com/docs/widget-builder/hidden/',
574 'text' => 'https://master-addons.com/docs/widget-builder/text/',
575 'textarea' => 'https://master-addons.com/docs/widget-builder/text-area/',
576 'url' => 'https://master-addons.com/docs/widget-builder/url/',
577 'color' => 'https://master-addons.com/docs/widget-builder/color/',
578 'number' => 'https://master-addons.com/docs/widget-builder/number/',
579 'switcher' => 'https://master-addons.com/docs/widget-builder/switcher/',
580 'select' => 'https://master-addons.com/docs/widget-builder/select/',
581 'select2' => 'https://master-addons.com/docs/widget-builder/select2/',
582 'choose' => 'https://master-addons.com/docs/widget-builder/choose/',
583 'wysiwyg' => 'https://master-addons.com/docs/widget-builder/wysiwyg/',
584 'code' => 'https://master-addons.com/docs/widget-builder/code/',
585 'date_time' => 'https://master-addons.com/docs/widget-builder/date-time/',
586 'media' => 'https://master-addons.com/docs/widget-builder/media/',
587 'gallery' => 'https://master-addons.com/docs/widget-builder/gallery/',
588 'icons' => 'https://master-addons.com/docs/widget-builder/icons/',
589 'slider' => 'https://master-addons.com/docs/widget-builder/slider/',
590 'popover_toggle' => 'https://master-addons.com/docs/widget-builder/popover-toggle/',
591 'visual_choice' => 'https://master-addons.com/docs/widget-builder/visual-choice/',
592 'font' => 'https://master-addons.com/docs/widget-builder/font/',
593 'typography' => 'https://master-addons.com/docs/widget-builder/typography/',
594 'dimensions' => 'https://master-addons.com/docs/widget-builder/dimensions/',
595 'box_shadow' => 'https://master-addons.com/docs/widget-builder/box-shadow/',
596 'background' => 'https://master-addons.com/docs/widget-builder/background/',
597 'border' => 'https://master-addons.com/docs/widget-builder/border/',
598 'text_shadow' => 'https://master-addons.com/docs/widget-builder/text-shadow/',
599 'divider' => 'https://master-addons.com/docs/widget-builder/divider/',
600 'repeater' => 'https://master-addons.com/docs/widget-builder/repeater/',
601 'tabs' => 'https://master-addons.com/docs/widget-builder/tabs/',
602 ],
603 ];
604
605 // Allow Pro version to add controls
606 $localize_data = apply_filters('jltma_widget_builder_localize_data', $localize_data);
607
608 wp_localize_script('jltma-widget-builder-app', 'JLTMAWidgetBuilder', $localize_data);
609
610 // Localize Icon Library Configuration
611 $icon_library_helper->localize_icon_library();
612 }
613
614 /**
615 * Hide third-party admin notices on Widget Builder pages
616 * Only show Master Addons notices
617 */
618 public function hide_admin_notices() {
619 $screen = get_current_screen();
620
621 // Only hide notices on the widget editor page (not the native CPT list)
622 if (!$screen || $screen->id !== 'admin_page_jltma-widget-editor') {
623 return;
624 }
625
626 // Hide all admin notices except Master Addons notices
627 ?>
628 <style>
629 .notice:not(.jltma-notice):not(.master-addons-notice),
630 .update-nag:not(.jltma-notice):not(.master-addons-notice),
631 .updated:not(.jltma-notice):not(.master-addons-notice),
632 .error:not(.jltma-notice):not(.master-addons-notice) {
633 display: none !important;
634 }
635 </style>
636 <?php
637 }
638
639 public function get_widget_data() {
640 if (!current_user_can('manage_options')) {
641 wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
642 }
643
644 check_ajax_referer('jltma_widget_nonce', '_nonce');
645
646 $widget_id = isset( $_GET['widget_id'] ) ? absint( $_GET['widget_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
647
648 if (!$widget_id) {
649 wp_send_json_error(['message' => 'Invalid widget ID']);
650 }
651
652 $widget = get_post($widget_id);
653 if (!$widget || $widget->post_type !== $this->widget_cpt->get_post_type()) {
654 wp_send_json_error(['message' => 'Widget not found']);
655 }
656
657 $category = get_post_meta($widget_id, '_jltma_widget_category', true) ?: 'general';
658
659 $data = [
660 'id' => $widget_id,
661 'title' => $widget->post_title,
662 'widget_name' => get_post_meta($widget_id, '_jltma_widget_name', true),
663 'category' => $category,
664 'widget_data' => get_post_meta($widget_id, '_jltma_widget_data', true) ?: [],
665 ];
666
667 wp_send_json_success($data);
668 }
669
670 public function save_widget_data() {
671 if (!current_user_can('manage_options')) {
672 wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
673 }
674
675 check_ajax_referer('jltma_widget_nonce', '_nonce');
676
677 $widget_id = isset( $_POST['widget_id'] ) ? absint( $_POST['widget_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
678 $title = isset( $_POST['widget_title'] ) ? sanitize_text_field( wp_unslash( $_POST['widget_title'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
679 $category = isset( $_POST['widget_category'] ) ? sanitize_text_field( wp_unslash( $_POST['widget_category'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
680
681 // Generate widget name from title if creating new
682 $widget_name = !$widget_id ? sanitize_title($title) : get_post_meta($widget_id, '_jltma_widget_name', true);
683 if (empty($widget_name)) {
684 $widget_name = sanitize_title($title);
685 }
686
687 if ($widget_id) {
688 // Update existing widget
689 wp_update_post([
690 'ID' => $widget_id,
691 'post_title' => $title,
692 ]);
693 } else {
694 // Create new widget
695 $widget_id = wp_insert_post([
696 'post_title' => $title,
697 'post_type' => $this->widget_cpt->get_post_type(),
698 'post_status' => 'publish',
699 'post_content' => '',
700 ]);
701
702 if (is_wp_error($widget_id)) {
703 return;
704 }
705
706 // Verify widget was created
707 $verify_widget = get_post($widget_id);
708 }
709
710 // Save meta data
711 update_post_meta($widget_id, '_jltma_widget_name', $widget_name);
712 update_post_meta($widget_id, '_jltma_widget_category', $category);
713
714 // Clear post cache to ensure REST API can fetch it immediately
715 clean_post_cache($widget_id);
716
717 // Generate widget files
718 $this->generate_widget_files($widget_id);
719
720 $edit_url = admin_url('admin.php?page=jltma-widget-editor&widget_id=' . $widget_id);
721
722 $response_data = [
723 'id' => $widget_id,
724 'title' => $title,
725 'widget_name' => $widget_name,
726 'category' => $category,
727 'edit_url' => $edit_url,
728 ];
729
730 wp_send_json_success($response_data);
731 }
732
733 /**
734 * Generate widget files using the generator
735 *
736 * @param int $widget_id
737 */
738 private function generate_widget_files($widget_id) {
739 $generator = new Widget_Generator($widget_id);
740 $result = $generator->generate();
741 }
742
743 public function delete_widget() {
744 if (!current_user_can('manage_options')) {
745 wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
746 }
747
748 check_ajax_referer('jltma_widget_nonce', '_nonce');
749
750 $widget_id = isset( $_POST['widget_id'] ) ? absint( $_POST['widget_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
751
752 if (!$widget_id) {
753 wp_send_json_error(['message' => 'Invalid widget ID']);
754 }
755
756 // Delete widget files first
757 Widget_Generator::delete_widget_files($widget_id);
758
759 $result = wp_delete_post($widget_id, true);
760
761 if ($result) {
762 wp_send_json_success(['message' => 'Widget deleted successfully']);
763 } else {
764 wp_send_json_error(['message' => 'Failed to delete widget']);
765 }
766 }
767
768 public function update_widget_category() {
769 if (!current_user_can('manage_options')) {
770 wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
771 }
772
773 check_ajax_referer('jltma_widget_nonce', '_nonce');
774
775 $widget_id = isset( $_POST['widget_id'] ) ? absint( $_POST['widget_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
776 $category = isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
777
778 if (!$widget_id) {
779 wp_send_json_error(['message' => 'Invalid widget ID']);
780 }
781
782 update_post_meta($widget_id, '_jltma_widget_category', $category);
783
784 wp_send_json_success(['message' => 'Category updated successfully']);
785 }
786
787 /**
788 * Get all registered Elementor categories
789 */
790 private function get_elementor_categories() {
791 if (!defined('ELEMENTOR_VERSION')) {
792 return [];
793 }
794
795 $categories = [];
796
797 try {
798 $elementor_categories = \Elementor\Plugin::$instance->elements_manager->get_categories();
799
800 foreach ($elementor_categories as $category_slug => $category_data) {
801 $categories[$category_slug] = [
802 'title' => $category_data['title'],
803 'icon' => $category_data['icon'] ?? '',
804 ];
805 }
806 } catch (\Exception $e) {
807 // Fallback categories if Elementor is not available
808 $categories = [
809 'general' => ['title' => 'General', 'icon' => ''],
810 'basic' => ['title' => 'Basic', 'icon' => ''],
811 'pro' => ['title' => 'Pro', 'icon' => ''],
812 ];
813 }
814
815 // Add custom categories from options (same as REST controller)
816 $custom_categories = get_option('jltma_custom_widget_categories', []);
817 if (!empty($custom_categories) && is_array($custom_categories)) {
818 foreach ($custom_categories as $slug => $title) {
819 // Check if not already in list (Elementor categories take precedence)
820 if (!isset($categories[$slug])) {
821 $categories[$slug] = [
822 'title' => $title,
823 'icon' => '',
824 ];
825 }
826 }
827 }
828
829 return $categories;
830 }
831
832 /**
833 * Get widget conditions via AJAX
834 */
835 public function get_widget_conditions() {
836 if (!current_user_can('manage_options')) {
837 wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
838 }
839
840 check_ajax_referer('jltma_widget_nonce', '_nonce');
841
842 $widget_id = isset( $_GET['widget_id'] ) ? absint( $_GET['widget_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
843
844 if (!$widget_id) {
845 wp_send_json_error(['message' => 'Invalid widget ID']);
846 }
847
848 $conditions = get_post_meta($widget_id, '_jltma_widget_conditions', true);
849
850 if (empty($conditions)) {
851 $conditions = [
852 'enabled' => false,
853 'user_roles' => [],
854 'device' => '',
855 'page_type' => [],
856 'date_start' => '',
857 'date_end' => ''
858 ];
859 }
860
861 wp_send_json_success(['conditions' => $conditions]);
862 }
863
864 /**
865 * Save widget conditions via AJAX
866 */
867 public function save_widget_conditions() {
868 if (!current_user_can('manage_options')) {
869 wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
870 }
871
872 check_ajax_referer('jltma_widget_nonce', '_nonce');
873
874 $widget_id = isset( $_POST['widget_id'] ) ? absint( $_POST['widget_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
875
876 if (!$widget_id) {
877 wp_send_json_error(['message' => 'Invalid widget ID']);
878 }
879
880 $conditions = [
881 'enabled' => !empty($_POST['enabled']),
882 'user_roles' => isset( $_POST['user_roles'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['user_roles'] ) ) : [], // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
883 'device' => sanitize_text_field( wp_unslash( $_POST['device'] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
884 'page_type' => isset( $_POST['page_type'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['page_type'] ) ) : [], // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
885 'date_start' => sanitize_text_field( wp_unslash( $_POST['date_start'] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
886 'date_end' => sanitize_text_field( wp_unslash( $_POST['date_end'] ?? '' ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified above
887 ];
888
889 update_post_meta($widget_id, '_jltma_widget_conditions', $conditions);
890
891 wp_send_json_success(['message' => 'Conditions saved successfully']);
892 }
893
894 /**
895 * Render a static widget preview.
896 * Handles the AJAX request to render widget HTML with mock control values.
897 * No user-supplied PHP or JavaScript is ever executed — placeholders are
898 * replaced with escaped default values and the markup is escaped on output.
899 */
900 public function render_preview() {
901 // Capability check: only administrators can request a widget preview.
902 if (!current_user_can('manage_options')) {
903 wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
904 return;
905 }
906
907 $nonce = isset($_POST['_nonce']) ? sanitize_text_field( wp_unslash( $_POST['_nonce'] ) ) : ( isset($_GET['_nonce']) ? sanitize_text_field( wp_unslash( $_GET['_nonce'] ) ) : '' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- these are the nonce values being collected for verification on the very next line
908 if (!wp_verify_nonce($nonce, 'wp_rest')) {
909 wp_send_json_error(['message' => 'Nonce verification failed']);
910 return;
911 }
912
913 $html_code = isset($_POST['html_code']) ? wp_unslash( $_POST['html_code'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified above; PHP/script stripped and output escaped below
914 $css_code = isset($_POST['css_code']) ? sanitize_textarea_field( wp_unslash( $_POST['css_code'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above
915 $controls = isset($_POST['controls']) ? json_decode( sanitize_textarea_field( wp_unslash( $_POST['controls'] ) ), true ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above
916
917 // Preview NEVER executes user code: strip PHP tags and inline <script>.
918 $html_code = str_replace(chr(0), '', $html_code);
919 $html_code = preg_replace('/<\?php/i', '', $html_code);
920 $html_code = str_replace(array('<?=', '<?', '?>'), '', $html_code);
921 $html_code = preg_replace('#<script\b[^>]*>.*?</script>#is', '', $html_code);
922 $html_code = preg_replace('#</?script\b[^>]*>#i', '', $html_code);
923
924 // Strip </style> and PHP from preview CSS so it cannot break out of <style>.
925 $css_code = preg_replace('#</?style\b[^>]*>#i', '', $css_code);
926 $css_code = preg_replace('/<\?php/i', '', $css_code);
927 $css_code = str_replace(array('<?=', '<?', '?>'), '', $css_code);
928
929 // Build mock settings array from control defaults.
930 $settings = [];
931 if (!empty($controls) && is_array($controls)) {
932 foreach ($controls as $control) {
933 if (isset($control['name']) && isset($control['default'])) {
934 $settings[$control['name']] = $control['default'];
935 }
936 }
937 }
938
939 // Substitute {{field}} / {{field.prop}} placeholders with the mock default
940 // VALUE (escaped). This is a static render — no PHP is evaluated.
941 $html_code = preg_replace_callback('/\{\{([^}]+)\}\}/', function($matches) use ($settings) {
942 $path = array_filter(array_map('trim', explode('.', trim($matches[1]))), 'strlen');
943 $value = $settings;
944 foreach ($path as $key) {
945 if (is_array($value) && isset($value[$key])) {
946 $value = $value[$key];
947 } else {
948 return '';
949 }
950 }
951 if (is_array($value)) {
952 $value = isset($value['url']) ? $value['url'] : '';
953 }
954 return esc_html((string) $value);
955 }, $html_code);
956
957 $rendered_html = wp_kses_post($html_code);
958
959 // Build full HTML document with CSS
960 $output = '<!DOCTYPE html>
961 <html>
962 <head>
963 <meta charset="UTF-8">
964 <meta name="viewport" content="width=device-width, initial-scale=1.0">
965 <style>' . $css_code . '</style>
966 </head>
967 <body>' . $rendered_html . '</body>
968 </html>';
969
970 wp_send_json_success(['html' => $output]);
971 }
972 }
973