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 / 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.2.3, at inc/admin/widget-builder/class-widget-admin.php

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