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

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

1,278 lines 45.8 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 Custom Post Type
4 *
5 * @package MasterAddons
6 * @subpackage WidgetBuilder
7 */
8
9 namespace MasterAddons\Inc\Admin\WidgetBuilder;
10
11 use MasterAddons\Inc\Admin\Templates\Widgets\Widgets_Ajax;
12
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 class Widget_CPT {
18
19 private static $instance = null;
20 private $post_type = 'jltma_widget';
21
22 public static function get_instance() {
23 if (self::$instance === null) {
24 self::$instance = new self();
25 }
26 return self::$instance;
27 }
28
29 public function __construct() {
30 $this->init_hooks();
31 }
32
33 private function init_hooks() {
34 add_action('init', [$this, 'register_post_type']);
35 add_filter('manage_jltma_widget_posts_columns', [$this, 'set_custom_columns']);
36 add_action('manage_jltma_widget_posts_custom_column', [$this, 'custom_column_content'], 10, 2);
37 add_filter('manage_edit-jltma_widget_sortable_columns', [$this, 'set_sortable_columns']);
38 add_action('save_post', [$this, 'save_meta_data']);
39 add_filter('post_row_actions', [$this, 'modify_row_actions'], 10, 2);
40 add_action('admin_head', [$this, 'admin_head_css']);
41 add_action('admin_enqueue_scripts', [$this, 'enqueue_list_assets']);
42 add_action('admin_footer', [$this, 'admin_footer_js']);
43 add_action('wp_ajax_jltma_widget_get_shortcode', [$this, 'get_shortcode_ajax']);
44 add_action('wp_ajax_jltma_add_widget_category', [$this, 'add_widget_category_ajax']);
45 add_action('template_redirect', [$this, 'handle_widget_preview']);
46 }
47
48 public function register_post_type() {
49 $labels = [
50 'name' => _x('Master Addons Widgets', 'Post Type General Name', 'master-addons'),
51 'singular_name' => _x('Widget', 'Post Type Singular Name', 'master-addons'),
52 'menu_name' => __('Widgets', 'master-addons'),
53 'name_admin_bar' => __('Widget', 'master-addons'),
54 'archives' => __('Widget Archives', 'master-addons'),
55 'attributes' => __('Widget Attributes', 'master-addons'),
56 'parent_item_colon' => __('Parent Widget:', 'master-addons'),
57 'all_items' => __('All Widgets', 'master-addons'),
58 'add_new_item' => __('Add New Widget', 'master-addons'),
59 'add_new' => __('Add New', 'master-addons'),
60 'new_item' => __('New Widget', 'master-addons'),
61 'edit_item' => __('Edit Widget', 'master-addons'),
62 'update_item' => __('Update Widget', 'master-addons'),
63 'view_item' => __('View Widget', 'master-addons'),
64 'view_items' => __('View Widgets', 'master-addons'),
65 'search_items' => __('Search Widget', 'master-addons'),
66 'not_found' => __('Not found', 'master-addons'),
67 'not_found_in_trash' => __('Not found in Trash', 'master-addons'),
68 'featured_image' => __('Featured Image', 'master-addons'),
69 'set_featured_image' => __('Set featured image', 'master-addons'),
70 'remove_featured_image' => __('Remove featured image', 'master-addons'),
71 'use_featured_image' => __('Use as featured image', 'master-addons'),
72 'insert_into_item' => __('Insert into widget', 'master-addons'),
73 'uploaded_to_this_item' => __('Uploaded to this widget', 'master-addons'),
74 'items_list' => __('Widgets list', 'master-addons'),
75 'items_list_navigation' => __('Widgets list navigation', 'master-addons'),
76 'filter_items_list' => __('Filter widgets list', 'master-addons'),
77 ];
78
79 $args = [
80 'label' => __('Widget', 'master-addons'),
81 'description' => __('Master Addons Custom Widgets', 'master-addons'),
82 'labels' => $labels,
83 'supports' => ['title', 'editor', 'thumbnail'],
84 'public' => false,
85 'show_ui' => true,
86 'show_in_menu' => false, // We'll add it to our custom menu
87 'menu_position' => 20,
88 'show_in_admin_bar' => true,
89 'show_in_nav_menus' => false,
90 'can_export' => true,
91 'has_archive' => false,
92 'exclude_from_search' => true,
93 'publicly_queryable' => false,
94 'capability_type' => 'post',
95 'show_in_rest' => true,
96 'rewrite' => false,
97 ];
98
99 register_post_type($this->post_type, $args);
100 }
101
102 /**
103 * The preview column's icon fallback needs the icon-font stylesheets,
104 * which are not loaded on the list screen by default.
105 */
106 public function enqueue_list_assets($hook) {
107 if ('edit.php' !== $hook) {
108 return;
109 }
110
111 $screen = get_current_screen();
112 if (!$screen || $this->post_type !== $screen->post_type) {
113 return;
114 }
115
116 if (class_exists('MasterAddons\Inc\Admin\WidgetBuilder\Icon_Library_Helper')) {
117 Icon_Library_Helper::get_instance()->enqueue_icon_libraries();
118 }
119 }
120
121 public function set_custom_columns($columns) {
122 $new_columns = [];
123
124 $new_columns['cb'] = $columns['cb'];
125 $new_columns['jltma_widget_preview'] = __('Preview', 'master-addons');
126 $new_columns['title'] = __('Widget Name', 'master-addons');
127 $new_columns['jltma_widget_category'] = __('Widget Category', 'master-addons');
128 $new_columns['author'] = __('Author', 'master-addons');
129 $new_columns['jltma_widget_shortcode'] = __('Shortcode', 'master-addons');
130
131 return $new_columns;
132 }
133
134 public function custom_column_content($column, $post_id) {
135 switch ($column) {
136 case 'jltma_widget_preview':
137 $this->render_preview_column($post_id);
138 break;
139
140 case 'jltma_widget_category':
141 $this->render_category_column($post_id);
142 break;
143
144 case 'jltma_widget_shortcode':
145 $this->render_shortcode_column($post_id);
146 break;
147 }
148 }
149
150 /**
151 * Preview thumbnail.
152 *
153 * Always renders from the local media library — never hotlinks the remote
154 * catalog. Imports sideload the thumbnail themselves; this backfills the
155 * ones that did not, then falls back to the Elementor panel icon for
156 * locally built widgets, which have no catalog thumbnail anywhere.
157 */
158 private function render_preview_column($post_id) {
159 if (!has_post_thumbnail($post_id)) {
160 $this->backfill_library_thumbnail($post_id);
161 }
162
163 if (has_post_thumbnail($post_id)) {
164 echo '<span class="jltma-widget-preview-thumb">';
165 echo get_the_post_thumbnail($post_id, [120, 68], [
166 'style' => 'width:120px;height:68px;object-fit:cover;border-radius:4px;display:block',
167 ]);
168 echo '</span>';
169 return;
170 }
171
172 $data = $this->read_array_meta($post_id, '_jltma_widget_data');
173 $icon = !empty($data['icon']) ? $data['icon'] : 'eicon-code';
174
175 printf(
176 '<span class="jltma-widget-preview-icon" style="display:flex;align-items:center;justify-content:center;width:120px;height:68px;background:#f0f0f1;border-radius:4px"><i class="%s" style="font-size:24px;color:#50575e"></i></span>',
177 esc_attr($icon)
178 );
179 }
180
181 /**
182 * Sideload a Widgets Library thumbnail into the media library, once.
183 *
184 * Covers widgets imported before the import sideloaded thumbnails, and
185 * imports whose sideload failed. The attempt is recorded either way so a
186 * widget with no reachable thumbnail costs one request, not one per page
187 * load.
188 *
189 * @param int $post_id
190 * @return void
191 */
192 private function backfill_library_thumbnail($post_id) {
193 if (get_post_meta($post_id, '_jltma_library_thumbnail_synced', true)) {
194 return;
195 }
196
197 $url = $this->get_library_thumbnail_url($post_id);
198
199 if (!$url) {
200 return;
201 }
202
203 update_post_meta($post_id, '_jltma_library_thumbnail_synced', time());
204
205 if (!function_exists('media_sideload_image')) {
206 require_once ABSPATH . 'wp-admin/includes/media.php';
207 require_once ABSPATH . 'wp-admin/includes/file.php';
208 require_once ABSPATH . 'wp-admin/includes/image.php';
209 }
210
211 $attachment_id = media_sideload_image($url, $post_id, get_the_title($post_id), 'id');
212
213 if (!is_wp_error($attachment_id)) {
214 set_post_thumbnail($post_id, $attachment_id);
215 }
216 }
217
218 /**
219 * Source URL of a widget's catalog thumbnail.
220 *
221 * Prefers the URL recorded at import. Older imports predate that meta, so
222 * fall back to the catalog transient the library screen populates — read
223 * only, so the list table issues no catalog request of its own.
224 *
225 * @param int $post_id
226 * @return string Empty for locally built widgets.
227 */
228 private function get_library_thumbnail_url($post_id) {
229 $stored = get_post_meta($post_id, '_jltma_library_thumbnail', true);
230
231 if ($stored) {
232 return $stored;
233 }
234
235 $source_id = absint(get_post_meta($post_id, '_jltma_library_source_id', true));
236
237 if (!$source_id) {
238 return '';
239 }
240
241 $catalog = get_transient(Widgets_Ajax::CACHE_KEY);
242
243 if (!is_array($catalog)) {
244 return '';
245 }
246
247 foreach ($catalog as $widget) {
248 if (!is_array($widget) || absint($widget['widget_id'] ?? 0) !== $source_id) {
249 continue;
250 }
251
252 return !empty($widget['thumbnail']) ? $widget['thumbnail'] : '';
253 }
254
255 return '';
256 }
257
258 /**
259 * Read widget meta as an array.
260 *
261 * The REST/admin save paths store arrays, while the demo importer stores the
262 * same keys JSON-encoded — accept either so imported widgets still preview.
263 *
264 * @param int $post_id
265 * @param string $key
266 * @return array
267 */
268 private function read_array_meta($post_id, $key) {
269 $value = get_post_meta($post_id, $key, true);
270
271 if (is_array($value)) {
272 return $value;
273 }
274
275 if (is_string($value) && '' !== $value) {
276 $decoded = json_decode($value, true);
277
278 if (is_array($decoded)) {
279 return $decoded;
280 }
281 }
282
283 return [];
284 }
285
286 private function render_category_column($post_id) {
287 $category = get_post_meta($post_id, '_jltma_widget_category', true);
288
289 if (empty($category)) {
290 $category = 'general';
291 }
292
293 $category_name = ucfirst(str_replace('_', ' ', $category));
294
295 echo '<span class="jltma-widget-category">' . esc_html($category_name) . '</span>';
296 echo '<br><a href="#" class="jltma-widget-edit-category" data-post-id="' . esc_attr( $post_id ) . '" data-category="' . esc_attr($category) . '">';
297 echo '<span class="dashicons dashicons-edit"></span> ' . esc_html__('Edit Category', 'master-addons');
298 echo '</a>';
299 }
300
301 private function render_shortcode_column($post_id) {
302 // Use the new shortcode format with widget ID
303 $shortcode = '[jltma_widget_' . $post_id . ']';
304
305 echo '<div style="position: relative; display: inline-block; max-width: 280px;">';
306 echo '<input type="text" readonly value="' . esc_attr($shortcode) . '" onclick="this.select()" style="width: 100%; font-family: monospace; font-size: 12px; padding: 4px 32px 4px 8px; border: 1px solid #ddd; border-radius: 3px; background: #f9f9f9; box-sizing: border-box;" title="' . esc_attr__('Click to select', 'master-addons') . '">';
307 echo '<button type="button" class="jltma-copy-shortcode-widget" data-shortcode="' . esc_attr($shortcode) . '" style="position: absolute; right: 2px; top: 2px; padding: 2px 4px; border: 1px solid #2271b1; border-radius: 3px; background: #fff; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: #2271b1;" title="' . esc_attr__('Copy to clipboard', 'master-addons') . '">';
308 echo '<span class="dashicons dashicons-clipboard" style="font-size: 14px; width: 14px; height: 14px;"></span>';
309 echo '</button>';
310 echo '</div>';
311 }
312
313 public function set_sortable_columns($columns) {
314 $columns['jltma_widget_category'] = 'jltma_widget_category';
315 return $columns;
316 }
317
318 public function modify_row_actions($actions, $post) {
319 if ($post->post_type === $this->post_type) {
320 // Remove default actions
321 unset($actions['inline hide-if-no-js']);
322 unset($actions['view']);
323
324 // Replace default edit with link to widget editor page
325 $editor_url = admin_url('admin.php?page=jltma-widget-editor&widget_id=' . $post->ID);
326 $actions['edit'] = sprintf(
327 '<a href="%s">%s</a>',
328 esc_url($editor_url),
329 __('Edit', 'master-addons')
330 );
331 }
332 return $actions;
333 }
334
335 public function save_meta_data($post_id) {
336 if (!isset($_POST['jltma_widget_meta_nonce_field']) ||
337 !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['jltma_widget_meta_nonce_field'] ) ), 'jltma_widget_meta_nonce')) {
338 return;
339 }
340
341 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
342 return;
343 }
344
345 if (!current_user_can('edit_post', $post_id)) {
346 return;
347 }
348
349 // Save widget name
350 if (isset($_POST['jltma_widget_name'])) {
351 $widget_name = sanitize_title( wp_unslash( $_POST['jltma_widget_name'] ) );
352 update_post_meta($post_id, '_jltma_widget_name', $widget_name);
353 }
354
355 // Save category
356 if (isset($_POST['jltma_widget_category'])) {
357 update_post_meta($post_id, '_jltma_widget_category', sanitize_text_field( wp_unslash( $_POST['jltma_widget_category'] ) ));
358 }
359 }
360
361 public function admin_head_css() {
362 global $current_screen;
363
364 if (!$current_screen || ($current_screen->post_type !== $this->post_type && $current_screen->id !== 'admin_page_jltma-widget-editor')) {
365 return;
366 }
367
368 ?>
369 <style>
370 .column-jltma_widget_preview {
371 width: 140px;
372 }
373 .jltma-widget-category {
374 padding: 4px 8px;
375 border-radius: 3px;
376 font-size: 11px;
377 font-weight: 600;
378 background: #dcdcde;
379 color: #50575e;
380 }
381 .jltma-widget-edit-category {
382 color: #2271b1;
383 text-decoration: none;
384 font-size: 12px;
385 }
386 .jltma-widget-edit-category:hover {
387 color: #135e96;
388 }
389 /* Shortcode Column Styling */
390 .jltma-shortcode-box {
391 display: inline-flex;
392 align-items: center;
393 border: 1px solid #dcdcde;
394 border-radius: 4px;
395 background: #fff;
396 overflow: hidden;
397 max-width: 280px;
398 transition: border-color 0.2s ease;
399 }
400 .jltma-shortcode-box:hover {
401 border-color: #8c8f94;
402 }
403 .jltma-shortcode-input {
404 flex: 1;
405 padding: 6px 10px;
406 border: none;
407 background: transparent;
408 font-family: 'Courier New', Courier, monospace;
409 font-size: 13px;
410 color: #2c3338;
411 outline: none;
412 min-width: 0;
413 }
414 .jltma-copy-icon-btn {
415 display: inline-flex;
416 align-items: center;
417 justify-content: center;
418 padding: 6px 10px;
419 background: transparent;
420 border: none;
421 border-left: 1px solid #dcdcde;
422 cursor: pointer;
423 transition: all 0.2s ease;
424 color: #50575e;
425 }
426 .jltma-copy-icon-btn:hover {
427 background: #f0f0f1;
428 color: #2271b1;
429 }
430 .jltma-copy-icon-btn:active {
431 background: #dcdcde;
432 }
433 .jltma-copy-icon-btn svg {
434 width: 18px;
435 height: 18px;
436 display: block;
437 }
438 .jltma-copy-feedback {
439 display: inline-flex;
440 align-items: center;
441 gap: 4px;
442 margin-top: 6px;
443 color: #00a32a;
444 font-size: 12px;
445 font-weight: 500;
446 animation: fadeInOut 2.5s ease-in-out;
447 }
448 .jltma-copy-feedback .dashicons {
449 font-size: 16px;
450 width: 16px;
451 height: 16px;
452 }
453 @keyframes fadeInOut {
454 0%, 100% { opacity: 0; }
455 10%, 90% { opacity: 1; }
456 }
457
458 /* Widget Preview Styles */
459 .jltma-preview-notice {
460 padding: 20px;
461 background: #f0f6fc;
462 border: 1px solid #c3dafe;
463 border-radius: 4px;
464 color: #0c5460;
465 text-align: center;
466 }
467 .jltma-widget-preview-wrapper {
468 background: #f9f9f9;
469 border: 1px solid #ddd;
470 border-radius: 4px;
471 overflow: hidden;
472 }
473 .jltma-preview-toolbar {
474 background: #fff;
475 border-bottom: 1px solid #ddd;
476 padding: 12px 16px;
477 display: flex;
478 justify-content: space-between;
479 align-items: center;
480 }
481 .jltma-preview-actions {
482 display: flex;
483 align-items: center;
484 gap: 12px;
485 }
486 .jltma-refresh-preview {
487 display: inline-flex;
488 align-items: center;
489 gap: 6px;
490 padding: 6px 12px;
491 background: #2271b1;
492 color: #fff;
493 border: none;
494 border-radius: 3px;
495 cursor: pointer;
496 transition: all 0.2s ease;
497 }
498 .jltma-refresh-preview:hover {
499 background: #135e96;
500 }
501 .jltma-refresh-preview .dashicons {
502 font-size: 16px;
503 width: 16px;
504 height: 16px;
505 }
506 .jltma-preview-size-selector {
507 display: flex;
508 gap: 4px;
509 border: 1px solid #ddd;
510 border-radius: 3px;
511 overflow: hidden;
512 }
513 .jltma-preview-size {
514 padding: 6px 10px;
515 background: #fff;
516 border: none;
517 border-right: 1px solid #ddd;
518 cursor: pointer;
519 transition: all 0.2s ease;
520 }
521 .jltma-preview-size:last-child {
522 border-right: none;
523 }
524 .jltma-preview-size:hover {
525 background: #f0f0f1;
526 }
527 .jltma-preview-size.active {
528 background: #2271b1;
529 color: #fff;
530 }
531 .jltma-preview-size .dashicons {
532 font-size: 18px;
533 width: 18px;
534 height: 18px;
535 }
536 .jltma-preview-container {
537 position: relative;
538 background: #fff;
539 min-height: 400px;
540 display: flex;
541 justify-content: center;
542 align-items: center;
543 }
544 .jltma-preview-loader {
545 position: absolute;
546 top: 50%;
547 left: 50%;
548 transform: translate(-50%, -50%);
549 text-align: center;
550 z-index: 10;
551 }
552 .jltma-preview-loader.hidden {
553 display: none;
554 }
555 .jltma-spinner {
556 border: 3px solid #f3f3f3;
557 border-top: 3px solid #2271b1;
558 border-radius: 50%;
559 width: 40px;
560 height: 40px;
561 animation: spin 1s linear infinite;
562 margin: 0 auto 12px;
563 }
564 @keyframes spin {
565 0% { transform: rotate(0deg); }
566 100% { transform: rotate(360deg); }
567 }
568 .jltma-preview-loader p {
569 color: #666;
570 font-size: 14px;
571 margin: 0;
572 }
573 .jltma-preview-frame {
574 width: 100%;
575 min-height: 400px;
576 border: none;
577 background: #fff;
578 transition: all 0.3s ease;
579 }
580 .jltma-preview-frame.loading {
581 opacity: 0.3;
582 }
583 .jltma-preview-frame.desktop {
584 width: 100%;
585 }
586 .jltma-preview-frame.tablet {
587 width: 768px;
588 max-width: 100%;
589 margin: 20px auto;
590 box-shadow: 0 0 20px rgba(0,0,0,0.1);
591 }
592 .jltma-preview-frame.mobile {
593 width: 375px;
594 max-width: 100%;
595 margin: 20px auto;
596 box-shadow: 0 0 20px rgba(0,0,0,0.1);
597 }
598
599 /* Shortcode Copy Button */
600 .jltma-copy-shortcode-widget {
601 transition: all 0.2s ease;
602 }
603 .jltma-copy-shortcode-widget:hover {
604 background: #f0f6fc !important;
605 border-color: #0073aa !important;
606 }
607 .jltma-copy-shortcode-widget:hover .dashicons {
608 color: #0073aa !important;
609 }
610
611 /* Toast Notification */
612 .jltma-toast-notification {
613 position: fixed;
614 bottom: 30px;
615 left: 50%;
616 transform: translateX(-50%);
617 background: linear-gradient(135deg, #00b09b 0%, #96c93d 100%);
618 color: #fff;
619 padding: 16px 24px;
620 border-radius: 8px;
621 box-shadow: 0 10px 40px rgba(0, 176, 155, 0.4);
622 display: flex;
623 align-items: center;
624 gap: 10px;
625 font-size: 14px;
626 font-weight: 500;
627 min-width: 300px;
628 max-width: 500px;
629 text-align: center;
630 z-index: 999999;
631 animation: jltmaSlideUp 0.3s ease-out;
632 overflow: hidden;
633 }
634 .jltma-toast-notification::before {
635 content: "";
636 position: absolute;
637 top: 0;
638 left: 0;
639 right: 0;
640 bottom: 0;
641 background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, transparent 100%);
642 pointer-events: none;
643 }
644 .jltma-toast-notification .dashicons {
645 font-size: 20px;
646 width: 20px;
647 height: 20px;
648 flex-shrink: 0;
649 position: relative;
650 z-index: 1;
651 }
652 .jltma-toast-notification span:not(.dashicons) {
653 position: relative;
654 z-index: 1;
655 }
656 @keyframes jltmaSlideUp {
657 from {
658 transform: translateX(-50%) translateY(100px);
659 opacity: 0;
660 }
661 to {
662 transform: translateX(-50%) translateY(0);
663 opacity: 1;
664 }
665 }
666
667 /* Add New Category Modal */
668 .jltma-category-modal-overlay {
669 display: none;
670 position: fixed;
671 top: 0;
672 left: 0;
673 right: 0;
674 bottom: 0;
675 background: rgba(0, 0, 0, 0.7);
676 z-index: 999999;
677 animation: fadeIn 0.2s ease-out;
678 }
679 .jltma-category-modal {
680 position: fixed;
681 top: 50%;
682 left: 50%;
683 transform: translate(-50%, -50%);
684 background: #2c2f36;
685 border-radius: 8px;
686 padding: 30px;
687 min-width: 500px;
688 max-width: 90%;
689 z-index: 1000000;
690 animation: slideDown 0.3s ease-out;
691 }
692 .jltma-category-modal h2 {
693 color: #ffffff;
694 font-size: 20px;
695 margin: 0 0 24px 0;
696 font-weight: 500;
697 }
698 .jltma-category-modal-input-wrapper {
699 margin-bottom: 20px;
700 }
701 .jltma-category-modal input[type="text"] {
702 width: 100%;
703 padding: 12px 16px;
704 background: #1e2126;
705 border: 1px solid #3e434a;
706 border-radius: 6px;
707 color: #ffffff;
708 font-size: 14px;
709 outline: none;
710 transition: border-color 0.2s ease;
711 }
712 .jltma-category-modal input[type="text"]:focus {
713 border-color: #5b6aff;
714 }
715 .jltma-category-modal input[type="text"]::placeholder {
716 color: #8a909a;
717 }
718 .jltma-category-modal-buttons {
719 display: flex;
720 justify-content: flex-end;
721 gap: 12px;
722 margin-top: 24px;
723 }
724 .jltma-category-modal-btn {
725 padding: 10px 24px;
726 border-radius: 6px;
727 font-size: 14px;
728 font-weight: 500;
729 cursor: pointer;
730 border: none;
731 transition: all 0.2s ease;
732 display: inline-flex;
733 align-items: center;
734 gap: 8px;
735 }
736 .jltma-category-modal-btn.primary {
737 background: #5b6aff;
738 color: #ffffff;
739 }
740 .jltma-category-modal-btn.primary:hover {
741 background: #4a59ee;
742 }
743 .jltma-category-modal-btn.primary:disabled {
744 background: #3e4a8a;
745 cursor: not-allowed;
746 opacity: 0.6;
747 }
748 .jltma-category-modal-btn.secondary {
749 background: transparent;
750 color: #ff4757;
751 border: 1px solid #ff4757;
752 }
753 .jltma-category-modal-btn.secondary:hover {
754 background: rgba(255, 71, 87, 0.1);
755 }
756 .jltma-category-modal-description {
757 color: #8a909a;
758 font-size: 13px;
759 font-style: italic;
760 margin-top: 12px;
761 }
762 @keyframes fadeIn {
763 from { opacity: 0; }
764 to { opacity: 1; }
765 }
766 @keyframes slideDown {
767 from {
768 transform: translate(-50%, -60%);
769 opacity: 0;
770 }
771 to {
772 transform: translate(-50%, -50%);
773 opacity: 1;
774 }
775 }
776 </style>
777 <?php
778 }
779
780 public function admin_footer_js() {
781 $screen = get_current_screen();
782
783 // Check if we're on widget listing or edit page
784 if (!$screen || !in_array($screen->id, ['edit-jltma_widget', 'jltma_widget', 'admin_page_jltma-widget-editor'])) {
785 return;
786 }
787
788 ?>
789 <!-- Add New Category Modal -->
790 <div class="jltma-category-modal-overlay">
791 <div class="jltma-category-modal">
792 <h2><?php esc_html_e('Add New Widget Category', 'master-addons'); ?></h2>
793 <div class="jltma-category-modal-input-wrapper">
794 <input type="text" id="jltma-new-category-name" placeholder="<?php esc_attr_e('Enter category name', 'master-addons'); ?>" />
795 <p class="jltma-category-modal-description"><?php esc_html_e('Category slug will be auto-generated from the name', 'master-addons'); ?></p>
796 </div>
797 <div class="jltma-category-modal-buttons">
798 <button type="button" class="jltma-category-modal-btn secondary" id="jltma-cancel-category">
799 <?php esc_html_e('Cancel', 'master-addons'); ?>
800 </button>
801 <button type="button" class="jltma-category-modal-btn primary" id="jltma-add-category" disabled>
802 <?php esc_html_e('Add Category', 'master-addons'); ?>
803 </button>
804 </div>
805 </div>
806 </div>
807
808 <script>
809
810 jQuery(document).ready(function($) {
811
812 // ===========================
813 // Add New Category Modal
814 // ===========================
815 var $categorySelect = $('#jltma_widget_category');
816 var $modalOverlay = $('.jltma-category-modal-overlay');
817 var $categoryInput = $('#jltma-new-category-name');
818 var $addButton = $('#jltma-add-category');
819 var $cancelButton = $('#jltma-cancel-category');
820 var previousCategoryValue = '';
821
822 // Handle "+ Add New Category" selection
823 $categorySelect.on('change', function() {
824 if ($(this).val() === 'add_new_category') {
825 previousCategoryValue = $(this).data('previous-value') || 'general';
826 openCategoryModal();
827 } else {
828 $(this).data('previous-value', $(this).val());
829 }
830 });
831
832 // Open modal
833 function openCategoryModal() {
834 $modalOverlay.fadeIn(200);
835 $categoryInput.val('').focus();
836 $addButton.prop('disabled', true);
837 }
838
839 // Close modal
840 function closeCategoryModal() {
841 $modalOverlay.fadeOut(200);
842 $categoryInput.val('');
843 $categorySelect.val(previousCategoryValue);
844 $addButton.prop('disabled', true);
845 }
846
847 // Close modal on overlay click
848 $modalOverlay.on('click', function(e) {
849 if (e.target === this) {
850 closeCategoryModal();
851 }
852 });
853
854 // Close modal on Cancel button
855 $cancelButton.on('click', function() {
856 closeCategoryModal();
857 });
858
859 // Enable/disable Add button based on input
860 $categoryInput.on('input', function() {
861 var value = $(this).val().trim();
862 $addButton.prop('disabled', value.length === 0);
863 });
864
865 // Handle Enter key in input
866 $categoryInput.on('keypress', function(e) {
867 if (e.which === 13) { // Enter key
868 e.preventDefault();
869 if (!$addButton.prop('disabled')) {
870 $addButton.click();
871 }
872 }
873 });
874
875 // Handle Escape key
876 $(document).on('keydown', function(e) {
877 if (e.key === 'Escape' && $modalOverlay.is(':visible')) {
878 closeCategoryModal();
879 }
880 });
881
882 // Handle Add Category button
883 $addButton.on('click', function() {
884 var categoryName = $categoryInput.val().trim();
885
886 if (categoryName.length === 0) {
887 return;
888 }
889
890 // Disable button and show loading state
891 $addButton.prop('disabled', true).text('<?php echo esc_js(__('Adding...', 'master-addons')); ?>');
892
893 // Send AJAX request
894 $.ajax({
895 url: ajaxurl,
896 type: 'POST',
897 data: {
898 action: 'jltma_add_widget_category',
899 category_name: categoryName,
900 _nonce: '<?php echo esc_js( wp_create_nonce('jltma_add_category_nonce') ); ?>'
901 },
902 success: function(response) {
903 if (response.success) {
904 // Add new option to select (before "+ Add New Category")
905 var newOption = '<option value="' + response.data.slug + '">' +
906 response.data.name + '</option>';
907 $categorySelect.find('option[value="add_new_category"]').before(newOption);
908
909 // Select the new category
910 $categorySelect.val(response.data.slug).data('previous-value', response.data.slug);
911
912 // Close modal
913 closeCategoryModal();
914
915 // Show success message
916 showToast(response.data.message || '<?php echo esc_js(__('Category added successfully!', 'master-addons')); ?>');
917 } else {
918 alert(response.data.message || '<?php echo esc_js(__('Failed to add category. Please try again.', 'master-addons')); ?>');
919 $addButton.prop('disabled', false).text('<?php echo esc_js(__('Add Category', 'master-addons')); ?>');
920 }
921 },
922 error: function() {
923 alert('<?php echo esc_js(__('An error occurred. Please try again.', 'master-addons')); ?>');
924 $addButton.prop('disabled', false).text('<?php echo esc_js(__('Add Category', 'master-addons')); ?>');
925 }
926 });
927 });
928
929 // Copy shortcode to clipboard (listing page - new style)
930 $(document).on('click', '.jltma-copy-shortcode-widget', function(e) {
931 e.preventDefault();
932 var button = $(this);
933 var shortcode = button.data('shortcode');
934 var originalIcon = button.find('.dashicons').attr('class');
935
936 if (!shortcode) {
937 return;
938 }
939
940 // Use modern clipboard API if available
941 if (navigator.clipboard && navigator.clipboard.writeText) {
942 navigator.clipboard.writeText(shortcode).then(function() {
943 showCopySuccess(button, originalIcon);
944 }).catch(function(err) {
945 fallbackCopyButton(shortcode, button, originalIcon);
946 });
947 } else {
948 fallbackCopyButton(shortcode, button, originalIcon);
949 }
950 });
951
952 function fallbackCopyButton(text, button, originalIcon) {
953 var temp = $('<textarea>');
954 $('body').append(temp);
955 temp.val(text).select();
956 document.execCommand('copy');
957 temp.remove();
958 showCopySuccess(button, originalIcon);
959 }
960
961 function showCopySuccess(button, originalIcon) {
962 // Change icon to checkmark
963 button.find('.dashicons').removeClass('dashicons-clipboard').addClass('dashicons-yes-alt');
964
965 // Use JLTMA Toaster if available, otherwise create custom toast
966 if (typeof JLTMA_Toaster !== 'undefined' && JLTMA_Toaster.success) {
967 JLTMA_Toaster.success('<?php echo esc_js(__('Shortcode copied to clipboard!', 'master-addons')); ?>', 2000);
968 } else {
969 showToast('<?php echo esc_js(__('Shortcode copied to clipboard!', 'master-addons')); ?>');
970 }
971
972 // Reset icon after delay
973 setTimeout(function() {
974 button.find('.dashicons').attr('class', originalIcon);
975 }, 2000);
976 }
977
978 function showToast(message) {
979 // Remove any existing toasts
980 $('.jltma-toast-notification').remove();
981
982 // Create toast
983 var toast = $('<div class="jltma-toast-notification">' +
984 '<span class="dashicons dashicons-yes"></span>' +
985 '<span>' + message + '</span>' +
986 '</div>');
987
988 $('body').append(toast);
989
990 // Auto-remove after 3 seconds
991 setTimeout(function() {
992 toast.fadeOut(300, function() {
993 $(this).remove();
994 });
995 }, 3000);
996 }
997
998 // Copy shortcode to clipboard (old style - if any remain)
999 $(document).on('click', '.jltma-copy-icon-btn', function(e) {
1000 e.preventDefault();
1001 var button = $(this);
1002 var shortcode = button.data('shortcode');
1003 var box = button.closest('.jltma-shortcode-box');
1004 var feedback = box.next('.jltma-copy-feedback');
1005
1006 // Use modern clipboard API if available
1007 if (navigator.clipboard && navigator.clipboard.writeText) {
1008 navigator.clipboard.writeText(shortcode).then(function() {
1009 showCopyFeedback(feedback);
1010 }).catch(function() {
1011 fallbackCopy(shortcode, feedback);
1012 });
1013 } else {
1014 fallbackCopy(shortcode, feedback);
1015 }
1016 });
1017
1018 // Fallback copy method for older browsers
1019 function fallbackCopy(text, feedback) {
1020 var temp = $('<textarea>');
1021 $('body').append(temp);
1022 temp.val(text).select();
1023 document.execCommand('copy');
1024 temp.remove();
1025 showCopyFeedback(feedback);
1026 }
1027
1028 // Show copy feedback
1029 function showCopyFeedback(feedback) {
1030 feedback.show();
1031 setTimeout(function() {
1032 feedback.fadeOut(400);
1033 }, 2000);
1034 }
1035
1036 // Copy shortcode button (meta box)
1037 $('.jltma-copy-shortcode').on('click', function(e) {
1038 e.preventDefault();
1039 var button = $(this);
1040 var shortcode = button.data('shortcode');
1041
1042 if (navigator.clipboard && navigator.clipboard.writeText) {
1043 navigator.clipboard.writeText(shortcode).then(function() {
1044 showButtonFeedback(button);
1045 }).catch(function() {
1046 fallbackCopyButton(shortcode, button);
1047 });
1048 } else {
1049 fallbackCopyButton(shortcode, button);
1050 }
1051 });
1052
1053 function fallbackCopyButton(text, button) {
1054 var temp = $('<textarea>');
1055 $('body').append(temp);
1056 temp.val(text).select();
1057 document.execCommand('copy');
1058 temp.remove();
1059 showButtonFeedback(button);
1060 }
1061
1062 function showButtonFeedback(button) {
1063 var originalText = button.text();
1064 button.text('<?php echo esc_js(__('Copied!', 'master-addons')); ?>').css({
1065 'background': '#00a32a',
1066 'border-color': '#00a32a'
1067 });
1068 setTimeout(function() {
1069 button.text(originalText).css({
1070 'background': '',
1071 'border-color': ''
1072 });
1073 }, 2000);
1074 }
1075
1076 // Widget Preview Functionality
1077 var $previewFrame = $('#jltma-widget-preview-frame');
1078 var $previewLoader = $('.jltma-preview-loader');
1079
1080 if ($previewFrame.length) {
1081 // Load preview on page load
1082 function loadPreview() {
1083 var widgetId = $previewFrame.data('widget-id');
1084 var shortcode = $previewFrame.data('shortcode');
1085
1086 if (!widgetId) return;
1087
1088 // Show loader
1089 $previewLoader.removeClass('hidden');
1090 $previewFrame.addClass('loading');
1091
1092 // Create preview URL
1093 var previewUrl = '<?php echo esc_js(add_query_arg(['jltma_widget_preview' => '1'], home_url('/'))); ?>';
1094 previewUrl += '&widget_id=' + widgetId;
1095 previewUrl += '&shortcode=' + encodeURIComponent(shortcode);
1096 previewUrl += '&t=' + new Date().getTime(); // Cache buster
1097
1098 // Load iframe
1099 $previewFrame.attr('src', previewUrl);
1100 }
1101
1102 // Hide loader when iframe loads
1103 $previewFrame.on('load', function() {
1104 setTimeout(function() {
1105 $previewLoader.addClass('hidden');
1106 $previewFrame.removeClass('loading');
1107 }, 300);
1108 });
1109
1110 // Refresh button
1111 $('.jltma-refresh-preview').on('click', function(e) {
1112 e.preventDefault();
1113 loadPreview();
1114 });
1115
1116 // Preview size selector
1117 $('.jltma-preview-size').on('click', function(e) {
1118 e.preventDefault();
1119 var size = $(this).data('size');
1120
1121 // Update active state
1122 $('.jltma-preview-size').removeClass('active');
1123 $(this).addClass('active');
1124
1125 // Update frame class
1126 $previewFrame.removeClass('desktop tablet mobile').addClass(size);
1127 });
1128
1129 // Initial load
1130 loadPreview();
1131 }
1132 });
1133 </script>
1134 <?php
1135 }
1136
1137 public function get_shortcode_ajax() {
1138 if (!current_user_can('manage_options')) {
1139 wp_send_json_error(['message' => 'You do not have permission to perform this action.'], 403);
1140 }
1141
1142 check_ajax_referer('jltma_widget_nonce', '_nonce');
1143
1144 $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
1145
1146 // Use the new shortcode format with widget ID
1147 $shortcode = '[jltma_widget_' . $post_id . ']';
1148
1149 wp_send_json_success(['shortcode' => $shortcode]);
1150 }
1151
1152 public function add_widget_category_ajax() {
1153 // Verify nonce
1154 check_ajax_referer('jltma_add_category_nonce', '_nonce');
1155
1156 // Check user capabilities
1157 if (!current_user_can('manage_options')) {
1158 wp_send_json_error([
1159 'message' => __('You do not have permission to add categories.', 'master-addons')
1160 ]);
1161 }
1162
1163 // Get category name
1164 $category_name = isset($_POST['category_name']) ? sanitize_text_field( wp_unslash( $_POST['category_name'] ) ) : '';
1165
1166 if (empty($category_name)) {
1167 wp_send_json_error([
1168 'message' => __('Category name is required.', 'master-addons')
1169 ]);
1170 }
1171
1172 // Generate slug from name
1173 $category_slug = sanitize_title($category_name);
1174
1175 // Get existing categories
1176 $categories = get_option('jltma_widget_categories', []);
1177
1178 // Check if category already exists
1179 if (isset($categories[$category_slug])) {
1180 wp_send_json_error([
1181 'message' => __('A category with this name already exists.', 'master-addons')
1182 ]);
1183 }
1184
1185 // Add new category
1186 $categories[$category_slug] = $category_name;
1187
1188 // Save categories
1189 update_option('jltma_widget_categories', $categories);
1190
1191 // Return success
1192 wp_send_json_success([
1193 /* translators: %s is the category name. */
1194 'message' => sprintf(__('Category "%s" added successfully!', 'master-addons'), $category_name),
1195 'slug' => $category_slug,
1196 'name' => $category_name
1197 ]);
1198 }
1199
1200 public function handle_widget_preview() {
1201 // Check if this is a preview request
1202 if (!isset($_GET['jltma_widget_preview']) || $_GET['jltma_widget_preview'] != '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only preview check, no nonce available
1203 return;
1204 }
1205
1206 // Verify user permissions
1207 if (!current_user_can('edit_posts')) {
1208 wp_die(esc_html__('You do not have permission to preview widgets.', 'master-addons'));
1209 }
1210
1211 $widget_id = isset($_GET['widget_id']) ? intval($_GET['widget_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce not available for preview URL
1212 $shortcode = isset($_GET['shortcode']) ? sanitize_text_field( wp_unslash( $_GET['shortcode'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce not available for preview URL
1213
1214 if (!$widget_id || !$shortcode) {
1215 wp_die(esc_html__('Invalid widget preview request.', 'master-addons'));
1216 }
1217
1218 // Check if widget exists
1219 $widget_post = get_post($widget_id);
1220 if (!$widget_post || $widget_post->post_type !== $this->post_type) {
1221 wp_die(esc_html__('Widget not found.', 'master-addons'));
1222 }
1223
1224 // Render preview template
1225 $this->render_preview_template($widget_id, $shortcode);
1226 exit;
1227 }
1228
1229 private function render_preview_template($widget_id, $shortcode) {
1230 ?>
1231 <!DOCTYPE html>
1232 <html <?php language_attributes(); ?>>
1233 <head>
1234 <meta charset="<?php bloginfo('charset'); ?>">
1235 <meta name="viewport" content="width=device-width, initial-scale=1">
1236 <title><?php esc_html_e('Widget Preview', 'master-addons'); ?></title>
1237 <?php wp_head(); ?>
1238 <style>
1239 body {
1240 margin: 0;
1241 padding: 20px;
1242 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
1243 background: #fff;
1244 }
1245 .jltma-preview-content {
1246 max-width: 100%;
1247 margin: 0 auto;
1248 }
1249 .jltma-preview-notice {
1250 background: #f0f6fc;
1251 border-left: 4px solid #2271b1;
1252 padding: 12px 16px;
1253 margin-bottom: 20px;
1254 font-size: 13px;
1255 color: #1d2327;
1256 }
1257 </style>
1258 </head>
1259 <body>
1260 <div class="jltma-preview-content">
1261 <div class="jltma-preview-notice">
1262 <?php esc_html_e('This is a live preview of your widget. Changes made to the widget will be reflected here after saving and refreshing.', 'master-addons'); ?>
1263 </div>
1264 <div class="jltma-widget-preview-output">
1265 <?php echo do_shortcode($shortcode); ?>
1266 </div>
1267 </div>
1268 <?php wp_footer(); ?>
1269 </body>
1270 </html>
1271 <?php
1272 }
1273
1274 public function get_post_type() {
1275 return $this->post_type;
1276 }
1277 }
1278