PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 All 41 releases
← All changes | includes/Core.php +1056 -39 51.1.44 → 51.1.87 View file →
@@ -8,8 +8,9 @@
8 8
9 9 use Elementor\Plugin;
10 10 use Elementor\Widgets_Manager;
11 11 use Elementor\Controls_Manager;
12 +use Elementor\Group_Control_Image_Size;
12 13 use King_Addons\Wishlist\Wishlist_Module;
13 14
14 15 if (!defined('ABSPATH')) {
15 16 exit; // Exit if accessed directly.
@@ -59,8 +60,13 @@
59 60 * @since 1.0.0
60 61 */
61 62 private function isExtensionEnabled(string $extension_id, string $constant_name): bool
62 63 {
64 + // Dependency checks (extensions that require other plugins).
65 + if ($extension_id === 'woo-builder' && (!class_exists('WooCommerce') || !function_exists('WC'))) {
66 + return false;
67 + }
68 +
63 69 // Get options from database
64 70 $options = get_option('king_addons_options', []);
65 71
66 72 // If a constant is defined and explicitly false, treat it as a hard disable.
@@ -98,8 +104,10 @@
98 104 public function __construct()
99 105 {
100 106 require_once(KING_ADDONS_PATH . 'includes/ModulesMap.php');
101 107 require_once(KING_ADDONS_PATH . 'includes/LibrariesMap.php');
108 + require_once(KING_ADDONS_PATH . 'includes/helpers/Global/Text_Entities_Migration.php');
109 + Text_Entities_Migration::boot();
102 110
103 111 if ($this->hasElementorCompatibility()) {
104 112
105 113 // Initial requirements check
@@ -133,8 +141,26 @@
133 141 require_once(KING_ADDONS_PATH . 'includes/extensions/Cookie_Consent/Cookie_Consent.php');
134 142 Cookie_Consent::instance();
135 143 }
136 144
145 + // Dynamic Tags
146 + // Loaded before the builders so their widgets can offer dynamic
147 + // values as soon as the editor asks Elementor for the tag list.
148 + if ($this->isExtensionEnabled('dynamic-tags', 'KING_ADDONS_EXT_DYNAMIC_TAGS')) {
149 + require_once(KING_ADDONS_PATH . 'includes/extensions/Dynamic_Tags/Dynamic_Tags.php');
150 + if (class_exists('King_Addons\\Dynamic_Tags')) {
151 + new Dynamic_Tags();
152 + }
153 + }
154 +
155 + // Loop Builder
156 + if ($this->isExtensionEnabled('loop-builder', 'KING_ADDONS_EXT_LOOP_BUILDER')) {
157 + require_once(KING_ADDONS_PATH . 'includes/extensions/Loop_Builder/Loop_Builder.php');
158 + if (class_exists('King_Addons\\Loop_Builder')) {
159 + new Loop_Builder();
160 + }
161 + }
162 +
137 163 // WooCommerce Builder
138 164 if ($this->isExtensionEnabled('woo-builder', 'KING_ADDONS_EXT_WOO_BUILDER')) {
139 165 require_once(KING_ADDONS_PATH . 'includes/extensions/Woo_Builder/Woo_Builder.php');
140 166 if (class_exists('King_Addons\\Woo_Builder')) {
@@ -271,11 +297,40 @@
271 297 require_once(KING_ADDONS_PATH . 'includes/extensions/Site_Preloader/Site_Preloader.php');
272 298 Site_Preloader::instance();
273 299 }
274 300
301 + // Free Shipping Bar
302 + if ($this->isExtensionEnabled('free-shipping-bar', 'KING_ADDONS_EXT_FREE_SHIPPING_BAR')) {
303 + require_once(KING_ADDONS_PATH . 'includes/extensions/Free_Shipping_Bar/Free_Shipping_Bar.php');
304 + if (class_exists('King_Addons\\Free_Shipping_Bar')) {
305 + Free_Shipping_Bar::instance();
306 + }
307 + }
308 +
309 + // Sticky Add To Cart
310 + if ($this->isExtensionEnabled('sticky-add-to-cart', 'KING_ADDONS_EXT_STICKY_ADD_TO_CART')) {
311 + require_once(KING_ADDONS_PATH . 'includes/extensions/Sticky_Add_To_Cart/Sticky_Add_To_Cart.php');
312 + if (class_exists('King_Addons\\Sticky_Add_To_Cart')) {
313 + Sticky_Add_To_Cart::instance();
314 + }
315 + }
316 +
317 + // Image Optimizer
318 + if ($this->isExtensionEnabled('image-optimizer', 'KING_ADDONS_EXT_IMAGE_OPTIMIZER')) {
319 + require_once(KING_ADDONS_PATH . 'includes/extensions/Image_Optimizer/Image_Optimizer.php');
320 + \King_Addons\Image_Optimizer\Image_Optimizer::instance();
321 + }
322 +
275 323 // Admin
276 324 require_once(KING_ADDONS_PATH . 'includes/Admin.php');
277 325
326 + // Rating Notice (admin only)
327 + // Temporarily disabled
328 + // if (is_admin()) {
329 + // require_once(KING_ADDONS_PATH . 'includes/admin/notices/RatingNotice.php');
330 + // \King_Addons\Admin\Notices\RatingNotice::instance();
331 + // }
332 +
278 333 // Additional - Controls
279 334 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2.php');
280 335 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2_API.php');
281 336 require_once(KING_ADDONS_PATH . 'includes/controls/Animations/Animations.php');
@@ -285,8 +340,9 @@
285 340 require_once(KING_ADDONS_PATH . 'includes/widgets/Search/Search_Ajax.php');
286 341 require_once(KING_ADDONS_PATH . 'includes/widgets/MailChimp/MailChimp_Ajax.php');
287 342
288 343 // Additional - Grids, Magazine Grid
344 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Grid_Ajax_Security.php');
289 345 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_Posts_Ajax.php');
290 346 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_WooCommerce_Products_Ajax.php');
291 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
292 348
@@ -291,8 +347,20 @@
291 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
292 348
293 349 // Additional - Form Builder
294 350 if (KING_ADDONS_WGT_FORM_BUILDER) {
351 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Extra_Fields.php');
352 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Conditional_Logic.php');
353 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Spam_Protection.php');
354 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Integrations.php');
355 + new Form_Integrations();
356 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Partial_Entries.php');
357 + new Form_Partial_Entries();
358 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Formula.php');
359 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Payments.php');
360 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Payment_Confirm.php');
361 + new Form_Payments();
362 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Form_Builder_Security.php');
295 363 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
296 364 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
297 365 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
298 366 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Subscribe_Mailchimp.php');
@@ -303,11 +371,15 @@
303 371 }
304 372
305 373 // ADDITIONAL CLASSES
306 374
307 - // Alt Text Generator for Media Library
308 - require_once(KING_ADDONS_PATH . 'includes/extensions/alt-text-generator/Alt_Text_Generator.php');
309 - new Alt_Text_Generator();
375 + // AI SEO Tools (Alt Text Generator + Auto Tagging)
376 + if ($this->isExtensionEnabled('ai-seo-tools', 'KING_ADDONS_EXT_AI_SEO_TOOLS')) {
377 + require_once(KING_ADDONS_PATH . 'includes/extensions/AI_SEO_Tools/AI_SEO_Tools.php');
378 + if (class_exists('King_Addons\\AI_SEO_Tools\\AI_SEO_Tools')) {
379 + \King_Addons\AI_SEO_Tools\AI_SEO_Tools::instance();
380 + }
381 + }
310 382
311 383 // Wishlist module - check extension toggle
312 384 if ($this->isExtensionEnabled('wishlist', 'KING_ADDONS_EXT_WISHLIST')) {
313 385 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_DB.php';
@@ -318,8 +390,17 @@
318 390 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Frontend.php';
319 391 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_WooCommerce.php';
320 392 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Module.php';
321 393 $this->wishlist_module = new Wishlist_Module();
394 + } else {
395 + // Extension off: still expire leftover guest cookies so public caches work.
396 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Session.php';
397 + add_action('send_headers', static function () {
398 + if (is_admin()) {
399 + return;
400 + }
401 + \King_Addons\Wishlist\Wishlist_Session::expire_if_present();
402 + });
322 403 }
323 404
324 405 // Dynamic Posts Grid AJAX Helper - Initialize regardless of Elementor compatibility
325 406 // This is needed for AJAX functionality to work even when PRO version is disabled
@@ -337,8 +418,13 @@
337 418
338 419 add_action('elementor/init', [$this, 'initElementor']);
339 420
340 421 add_action('elementor/elements/categories_registered', [$this, 'addWidgetCategory']);
422 + // Elementor promotes Atomic / Custom Widget / Pro after that hook, so the
423 + // panel order is applied again once categories are fully initialised.
424 + add_action('elementor/init', [$this, 'reorderWidgetCategoriesLate'], 100);
425 + add_action('elementor/editor/before_enqueue_scripts', [$this, 'reorderWidgetCategoriesLate'], 1);
426 + add_filter('elementor/document/config', [$this, 'filterDocumentPanelCategories'], 1, 2);
341 427 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
342 428
343 429 self::enableFeatures();
344 430
@@ -377,9 +463,23 @@
377 463
378 464 // Notice - Upgrade Suggestion
379 465 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
380 466 add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
381 - add_action('admin_notices', [$this, 'showNoticeUpgrade']);
467 +
468 + /**
469 + * The previous notice is kept untouched in showNoticeUpgrade() as a fallback.
470 + * To go back to it, either define KING_ADDONS_UPGRADE_NOTICE_LEGACY as true
471 + * in wp-config.php, or return true from this filter.
472 + */
473 + $use_legacy_notice = defined('KING_ADDONS_UPGRADE_NOTICE_LEGACY')
474 + ? (bool) KING_ADDONS_UPGRADE_NOTICE_LEGACY
475 + : false;
476 + $use_legacy_notice = (bool) apply_filters('king_addons/upgrade_notice/use_legacy', $use_legacy_notice);
477 +
478 + add_action(
479 + 'admin_notices',
480 + [$this, $use_legacy_notice ? 'showNoticeUpgrade' : 'showNoticeUpgradeV2']
481 + );
382 482 }
383 483
384 484 // Dashboard UI settings AJAX handler
385 485 add_action('wp_ajax_king_addons_save_dashboard_ui', [$this, 'king_addons_save_dashboard_ui_callback']);
@@ -452,8 +552,12 @@
452 552 wp_send_json_error(['message' => 'Invalid theme mode'], 400);
453 553 }
454 554
455 555 update_user_meta($user_id, 'king_addons_theme_mode', $mode);
556 +
557 + // Also store as a global option so pages can fall back when user meta isn't set.
558 + update_option('king_addons_theme_mode', $mode);
559 +
456 560 wp_send_json_success(['key' => $key, 'value' => $mode]);
457 561 }
458 562
459 563 // Backward compatibility: old boolean dark_theme maps to theme_mode.
@@ -491,23 +595,44 @@
491 595 return;
492 596 }
493 597 ?>
494 598 <div class="king-addons-upgrade-notice notice notice-info is-dismissible"
495 - style="border-left: 4px solid #FF4040;padding: 10px 15px;">
599 + style="border-left: 4px solid #0071e3;padding: 10px 15px;">
496 600 <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
497 - <span>Unlock <strong style="font-weight: 700;">650+</strong> premium templates and <strong
498 - style="font-weight: 700;">200+</strong> advanced features for only $<strong
499 - style="font-weight: 700;">4.99</strong>/month. Upgrade now and boost your website's performance!</span>
601 + <span>
602 + Get <strong style="font-weight: 700;">4,000+</strong> premium templates and sections,
603 + <strong style="font-weight: 700;">100+</strong> widgets,
604 + <strong style="font-weight: 700;">200+</strong> advanced features,
605 + and AI tools for Elementor.
606 + From $<strong style="font-weight: 700;">6.99</strong>/mo.
607 +</span>
500 608 </p>
501 609 <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
502 610 <p style="display: flex;">
503 - <a style="font-size: 14px;padding: 4px 22px;display: flex;align-items: center;background-image: linear-gradient(120deg, #A20BD8 0%, #FF4040 100%);border: none;"
611 + <a href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&amp;utm_medium=plugin&amp;utm_campaign=kng" target="_blank" class="ka-wb-btn ka-wb-btn-primary" style="
612 + background: #0071e3;
613 + color: #fff;
614 + display: inline-flex;
615 + align-items: center;
616 + justify-content: center;
617 + gap: 6px;
618 + padding: 10px 18px;
619 + font-size: 14px;
620 + font-weight: 500;
621 + text-decoration: none;
622 + border-radius: 980px;
623 + border: none;
624 + cursor: pointer;
625 + transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
626 + white-space: nowrap;
627 + font-family: inherit;
628 +">Upgrade to Pro<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="
629 + width: 16px;
630 + height: 16px;
631 +"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
632 + </a>
633 + <a style="margin-left: 20px;display: flex;align-items: center;font-size: 14px;color: #0071e3;"
504 634 href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
505 - class="button button-primary"><img style="margin-right: 7px;width: 15px;height: 15px;"
506 - src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/icon-for-admin.svg'; ?>"
507 - alt="<?php echo esc_html__('Upgrade Now', 'king-addons'); ?>">Upgrade Now</a>
508 - <a style="margin-left: 20px;display: flex;align-items: center;"
509 - href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
510 635 class="link">Learn More</a>
511 636 </p>
512 637 </div>
513 638 <script>
@@ -527,8 +652,241 @@
527 652 </script>
528 653 <?php
529 654 }
530 655
656 + /**
657 + * Upgrade notice, current design.
658 + *
659 + * Replaces showNoticeUpgrade(), which is kept above unchanged as a fallback —
660 + * see the KING_ADDONS_UPGRADE_NOTICE_LEGACY constant and the
661 + * king_addons/upgrade_notice/use_legacy filter.
662 + *
663 + * @return void
664 + */
665 + function showNoticeUpgradeV2()
666 + {
667 + if (!current_user_can('manage_options')) {
668 + return;
669 + }
670 +
671 + $user_id = get_current_user_id();
672 + $now = time();
673 + $last_dismissed = get_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', true);
674 +
675 + // Stay hidden for a week after the notice is dismissed.
676 + if ($last_dismissed && ($now - $last_dismissed) < WEEK_IN_SECONDS) {
677 + return;
678 + }
679 +
680 + $pricing_url = 'https://kingaddons.com/pricing/?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng';
681 + // Same page, different utm_source: the two links stay separately
682 + // measurable, which the previous notice could not do because both
683 + // carried kng-notice-offer.
684 + $explore_url = 'https://kingaddons.com/pricing/?utm_source=kng-notice-explore&utm_medium=plugin&utm_campaign=kng';
685 + ?>
686 + <style>
687 + .king-addons-upgrade-notice-v2 {
688 + border-left: 4px solid #5B03FF;
689 + padding: 18px 20px 18px 22px;
690 + }
691 +
692 + .king-addons-upgrade-notice-v2 .king-addons-un-main {
693 + /* Admin notices run the full width of the screen, so a second
694 + column always ends up marooned in empty space. Everything
695 + stays in one left-aligned stack instead, with the price sitting
696 + in the action row where the decision is made. */
697 + display: flex;
698 + flex-direction: column;
699 + gap: 10px;
700 + }
701 +
702 + .king-addons-upgrade-notice-v2 .king-addons-un-brand {
703 + display: flex;
704 + align-items: center;
705 + gap: 7px;
706 + font-size: 11px;
707 + font-weight: 700;
708 + letter-spacing: .09em;
709 + text-transform: uppercase;
710 + color: #5B03FF;
711 + }
712 +
713 + .king-addons-upgrade-notice-v2 .king-addons-un-brand svg {
714 + width: 14px;
715 + height: 14px;
716 + display: block;
717 + fill: currentColor;
718 + }
719 +
720 + .king-addons-upgrade-notice-v2 .king-addons-un-headline {
721 + margin: 0;
722 + font-size: 17px;
723 + font-weight: 700;
724 + line-height: 1.35;
725 + color: #1d2327;
726 + max-width: 46ch;
727 + }
728 +
729 + .king-addons-upgrade-notice-v2 .king-addons-un-sub {
730 + margin: 0;
731 + font-size: 14px;
732 + line-height: 1.5;
733 + color: #50575e;
734 + max-width: 58ch;
735 + }
736 +
737 + .king-addons-upgrade-notice-v2 .king-addons-un-actions {
738 + display: flex;
739 + align-items: center;
740 + gap: 20px;
741 + flex-wrap: wrap;
742 + margin-top: 2px;
743 + }
744 +
745 + .king-addons-upgrade-notice-v2 .king-addons-un-cta {
746 + background: #5B03FF;
747 + color: #fff;
748 + display: inline-flex;
749 + align-items: center;
750 + gap: 7px;
751 + padding: 9px 18px;
752 + font-size: 14px;
753 + font-weight: 600;
754 + text-decoration: none;
755 + border-radius: 8px;
756 + transition: background-color .15s ease;
757 + }
758 +
759 + .king-addons-upgrade-notice-v2 .king-addons-un-cta:hover, .king-addons-upgrade-notice-v2 .king-addons-un-cta:focus {
760 + background: #3D01B0;
761 + color: #fff;
762 + }
763 +
764 + .king-addons-upgrade-notice-v2 .king-addons-un-cta:focus-visible {
765 + outline: 2px solid #5B03FF;
766 + outline-offset: 2px;
767 + }
768 +
769 + .king-addons-upgrade-notice-v2 .king-addons-un-cta svg {
770 + width: 15px;
771 + height: 15px;
772 + display: block;
773 + }
774 +
775 + .king-addons-upgrade-notice-v2 .king-addons-un-secondary {
776 + font-size: 14px;
777 + font-weight: 500;
778 + color: #50575e;
779 + text-decoration: none;
780 + border-bottom: 1px solid #c9c4d8;
781 + padding-bottom: 1px;
782 + }
783 +
784 + .king-addons-upgrade-notice-v2 .king-addons-un-secondary:hover, .king-addons-upgrade-notice-v2 .king-addons-un-secondary:focus {
785 + color: #5B03FF;
786 + border-bottom-color: #5B03FF;
787 + }
788 +
789 + .king-addons-upgrade-notice-v2 .king-addons-un-trust {
790 + margin: 4px 0 0;
791 + font-size: 13px;
792 + color: #50575e;
793 + display: flex;
794 + align-items: center;
795 + gap: 8px 18px;
796 + flex-wrap: wrap;
797 + }
798 +
799 + .king-addons-upgrade-notice-v2 .king-addons-un-trust-item {
800 + display: inline-flex;
801 + align-items: baseline;
802 + gap: 6px;
803 + }
804 +
805 + .king-addons-upgrade-notice-v2 .king-addons-un-trust strong {
806 + color: #1d2327;
807 + font-weight: 700;
808 + }
809 +
810 + .king-addons-upgrade-notice-v2 .king-addons-un-check {
811 + color: #0a6b45;
812 + font-weight: 700;
813 + font-size: 12px;
814 + line-height: 1;
815 + }
816 +
817 + @media screen and (max-width: 782px) {
818 + .king-addons-upgrade-notice-v2 .king-addons-un-actions {
819 + align-items: flex-start;
820 + gap: 12px;
821 + }
822 + }
823 + </style>
824 + <div class="king-addons-upgrade-notice king-addons-upgrade-notice-v2 notice notice-info is-dismissible">
825 + <div class="king-addons-un-main">
826 + <div class="king-addons-un-brand">
827 + <svg viewBox="0 0 512 512" aria-hidden="true" focusable="false"><path d="M504.981,150.787c-6.048-5.163-14.583-6.251-21.736-2.769l-109.444,53.28L271.109,82.896C267.311,78.516,261.798,76,256,76c-5.798,0-11.31,2.516-15.109,6.896L138.199,201.297l-109.444-53.28c-7.153-3.481-15.687-2.394-21.737,2.769c-6.05,5.163-8.466,13.421-6.153,21.031l76,250C79.426,430.242,87.195,436,96,436h320c8.804,0,16.574-5.758,19.134-14.182l76-250C513.448,164.208,511.032,155.95,504.981,150.787z M401.175,396H110.823L52.472,204.052l82.022,39.931c8.144,3.964,17.93,1.962,23.863-4.878L256,126.525l97.644,112.58c5.932,6.841,15.721,8.841,23.862,4.878l82.022-39.931L401.175,396z"></path></svg>
828 + <?php esc_html_e('King Addons Pro', 'king-addons'); ?>
829 + </div>
830 + <h2 class="king-addons-un-headline">
831 + <?php esc_html_e('Mega Menu, Popup Builder, WooCommerce Builder, Theme Builder &amp; AI Tools', 'king-addons'); ?>
832 + </h2>
833 + <p class="king-addons-un-sub">
834 + <?php esc_html_e('200+ Pro features, premium widgets and 4,000+ templates &amp; sections for Elementor.', 'king-addons'); ?>
835 + </p>
836 + <div class="king-addons-un-actions">
837 + <a class="king-addons-un-cta" href="<?php echo esc_url($pricing_url); ?>" target="_blank" rel="noopener noreferrer">
838 + <?php
839 + printf(
840 + /* translators: %s: monthly price, for example $6.99/mo */
841 + esc_html__('Upgrade to Pro for %s', 'king-addons'),
842 + esc_html__('$6.99/mo', 'king-addons')
843 + );
844 + ?>
845 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true" focusable="false"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
846 + </a>
847 + <a class="king-addons-un-secondary" href="<?php echo esc_url($explore_url); ?>" target="_blank" rel="noopener noreferrer">
848 + <?php esc_html_e('Explore Pro features', 'king-addons'); ?>
849 + </a>
850 + </div>
851 + <p class="king-addons-un-trust">
852 + <span class="king-addons-un-trust-item">
853 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
854 + <?php esc_html_e('30-day money-back guarantee', 'king-addons'); ?>
855 + </span>
856 + <span class="king-addons-un-trust-item">
857 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
858 + <?php esc_html_e('Cancel anytime', 'king-addons'); ?>
859 + </span>
860 + <span class="king-addons-un-trust-item">
861 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
862 + <?php
863 + printf(
864 + /* translators: %s: number of users, wrapped in bold */
865 + esc_html__('Trusted by %s users', 'king-addons'),
866 + '<strong>' . esc_html__('20,000+', 'king-addons') . '</strong>'
867 + );
868 + ?>
869 + </span>
870 + </p>
871 + </div>
872 + </div>
873 + <script>
874 + (function ($) {
875 + const kingAddonsPremiumNoticeNonce = '<?php echo esc_js(wp_create_nonce('king_addons_premium_notice_dismiss')); ?>';
876 + $(document).ready(function () {
877 + $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function () {
878 + $.post(ajaxurl, {
879 + action: 'king_addons_premium_notice_dismiss',
880 + nonce: kingAddonsPremiumNoticeNonce
881 + });
882 + });
883 + });
884 + })(jQuery);
885 + </script>
886 + <?php
887 + }
888 +
531 889 function enqueueFrontendStyles()
532 890 {
533 891 /**
534 892 * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
@@ -604,30 +962,371 @@
604 962 {
605 963 add_action('elementor/widgets/register', [$this, 'registerWidgets']);
606 964 add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueEditorStyles']);
607 965 add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueEditorScripts']);
966 + add_action('elementor/preview/enqueue_styles', [$this, 'enqueueEditorPreviewStyles']);
608 967 }
609 968
969 + function enqueueEditorPreviewStyles(): void
970 + {
971 + wp_enqueue_style(
972 + KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-preview',
973 + KING_ADDONS_URL . 'includes/admin/css/elementor-preview.css',
974 + [],
975 + KING_ADDONS_VERSION
976 + );
977 + }
978 +
610 979 function addWidgetCategory(): void
611 980 {
612 - Plugin::instance()->elements_manager->add_category(
981 + $elements_manager = Plugin::instance()->elements_manager;
982 +
983 + $elements_manager->add_category(
613 984 'king-addons',
614 985 [
615 986 'title' => esc_html__('King Addons', 'king-addons'),
616 - 'icon' => 'fa fa-plug'
987 + 'icon' => 'fa fa-plug',
617 988 ]
618 989 );
619 990
620 - Plugin::instance()->elements_manager->add_category(
621 - 'king-addons-woo-builder',
991 + if (class_exists('WooCommerce') && function_exists('WC')) {
992 + $elements_manager->add_category(
993 + 'king-addons-woo',
994 + [
995 + 'title' => esc_html__('King Addons WooCommerce', 'king-addons'),
996 + 'icon' => 'fa fa-shopping-cart',
997 + 'hideIfEmpty' => true,
998 + ]
999 + );
1000 +
1001 + $elements_manager->add_category(
1002 + 'king-addons-woo-builder',
1003 + [
1004 + 'title' => esc_html__('King Addons WooCommerce Builder', 'king-addons'),
1005 + 'icon' => 'fa fa-shopping-cart',
1006 + 'hideIfEmpty' => true,
1007 + ]
1008 + );
1009 + }
1010 +
1011 + $elements_manager->add_category(
1012 + 'king-addons-theme-builder',
622 1013 [
623 - 'title' => esc_html__('King Addons Woo Builder', 'king-addons'),
624 - 'icon' => 'fa fa-shopping-cart'
1014 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1015 + 'icon' => 'fa fa-plug',
1016 + 'hideIfEmpty' => true,
625 1017 ]
626 1018 );
627 1019 }
628 1020
629 1021 /**
1022 + * Apply the panel category order after Elementor has finished registering its own.
1023 + *
1024 + * @return void
1025 + */
1026 + public function reorderWidgetCategoriesLate(): void
1027 + {
1028 + if (!did_action('elementor/loaded')) {
1029 + return;
1030 + }
1031 +
1032 + $elements_manager = Plugin::instance()->elements_manager;
1033 + $elements_manager->get_categories();
1034 + $this->reorderWidgetCategories($elements_manager);
1035 +
1036 + if (!$this->shouldApplyEditorPanelVisibility()) {
1037 + return;
1038 + }
1039 +
1040 + $post_id = $this->getCurrentEditorPostId();
1041 + if ($post_id > 0) {
1042 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1043 + }
1044 + }
1045 +
1046 + /**
1047 + * Overwrite the editor panel category snapshot after Elementor has built document config.
1048 + *
1049 + * @param array $additional_config
1050 + * @param int $post_id
1051 + * @return array
1052 + */
1053 + public function filterDocumentPanelCategories($additional_config, $post_id = 0)
1054 + {
1055 + $post_id = (int) $post_id;
1056 + if ($post_id <= 0) {
1057 + $post_id = $this->getCurrentEditorPostId();
1058 + }
1059 + if (!is_array($additional_config)) {
1060 + $additional_config = [];
1061 + }
1062 +
1063 + if (!did_action('elementor/loaded')) {
1064 + return $additional_config;
1065 + }
1066 +
1067 + $elements_manager = Plugin::instance()->elements_manager;
1068 + $elements_manager->get_categories();
1069 + $this->reorderWidgetCategories($elements_manager);
1070 + if ($post_id > 0) {
1071 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1072 + }
1073 +
1074 + $categories = \Elementor\Core\Base\Document::get_filtered_editor_panel_categories();
1075 + foreach ($this->getHiddenPanelCategoryKeys($post_id) as $hidden_key) {
1076 + unset($categories[$hidden_key]);
1077 + }
1078 +
1079 + $additional_config['panel']['elements_categories'] = $categories;
1080 +
1081 + foreach ($this->getHiddenPanelWidgetNames($post_id) as $widget_name) {
1082 + $additional_config['widgets'][$widget_name]['show_in_panel'] = false;
1083 + }
1084 +
1085 + return $additional_config;
1086 + }
1087 +
1088 + /**
1089 + * Reorder widget categories: Layout, then King Addons groups, then the rest of Elementor.
1090 + *
1091 + * @param \Elementor\Elements_Manager $elements_manager
1092 + * @return void
1093 + */
1094 + private function reorderWidgetCategories($elements_manager): void
1095 + {
1096 + try {
1097 + $reflection = new \ReflectionClass($elements_manager);
1098 + $categories_property = $reflection->getProperty('categories');
1099 + if (PHP_VERSION_ID < 80100) {
1100 + $categories_property->setAccessible(true);
1101 + }
1102 +
1103 + $categories = $categories_property->getValue($elements_manager);
1104 + if (!is_array($categories) || $categories === []) {
1105 + return;
1106 + }
1107 +
1108 + $layout = [];
1109 + $ours = [];
1110 + $rest = [];
1111 +
1112 + foreach ($categories as $key => $value) {
1113 + if ('layout' === $key) {
1114 + $layout[$key] = $value;
1115 + } elseif (str_starts_with((string) $key, 'king-addons')) {
1116 + $ours[$key] = $value;
1117 + } else {
1118 + $rest[$key] = $value;
1119 + }
1120 + }
1121 +
1122 + $ours_sorted = [];
1123 + foreach (['king-addons', 'king-addons-woo', 'king-addons-woo-builder', 'king-addons-theme-builder'] as $preferred) {
1124 + if (isset($ours[$preferred])) {
1125 + $ours_sorted[$preferred] = $ours[$preferred];
1126 + unset($ours[$preferred]);
1127 + }
1128 + }
1129 + $ours_sorted += $ours;
1130 +
1131 + $categories_property->setValue($elements_manager, $layout + $ours_sorted + $rest);
1132 + } catch (\ReflectionException $e) {
1133 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1134 + }
1135 + }
1136 +
1137 + /**
1138 + * Drop context-only categories from the manager for the current editor document.
1139 + *
1140 + * @param \Elementor\Elements_Manager $elements_manager
1141 + * @param int $post_id
1142 + * @return void
1143 + */
1144 + private function applyContextCategoryVisibility($elements_manager, int $post_id): void
1145 + {
1146 + $hidden = $this->getHiddenPanelCategoryKeys($post_id);
1147 + if ($hidden === []) {
1148 + return;
1149 + }
1150 +
1151 + try {
1152 + $reflection = new \ReflectionClass($elements_manager);
1153 + $categories_property = $reflection->getProperty('categories');
1154 + if (PHP_VERSION_ID < 80100) {
1155 + $categories_property->setAccessible(true);
1156 + }
1157 +
1158 + $categories = $categories_property->getValue($elements_manager);
1159 + if (!is_array($categories) || $categories === []) {
1160 + return;
1161 + }
1162 +
1163 + foreach ($hidden as $key) {
1164 + unset($categories[$key]);
1165 + }
1166 +
1167 + $categories_property->setValue($elements_manager, $categories);
1168 + } catch (\ReflectionException $e) {
1169 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1170 + }
1171 + }
1172 +
1173 + /**
1174 + * Category keys that should not appear in the current editor document.
1175 + *
1176 + * @param int $post_id
1177 + * @return array<int,string>
1178 + */
1179 + private function getHiddenPanelCategoryKeys(int $post_id): array
1180 + {
1181 + $hidden = [];
1182 + $is_woo_builder = $this->isWooBuilderDocument($post_id);
1183 + $is_theme_builder = $this->isThemeBuilderDocument($post_id);
1184 + $is_loop_item = $this->isLoopItemDocument($post_id);
1185 + $woocommerce_active = class_exists('WooCommerce') && function_exists('WC');
1186 +
1187 + if (!$woocommerce_active) {
1188 + $hidden[] = 'king-addons-woo';
1189 + $hidden[] = 'king-addons-woo-builder';
1190 + } elseif (!$is_woo_builder && !$is_loop_item) {
1191 + $hidden[] = 'king-addons-woo-builder';
1192 + }
1193 +
1194 + if (!$is_theme_builder && !$is_loop_item) {
1195 + $hidden[] = 'king-addons-theme-builder';
1196 + }
1197 +
1198 + return $hidden;
1199 + }
1200 +
1201 + /**
1202 + * Widget names that belong to hidden context categories.
1203 + *
1204 + * @param int $post_id
1205 + * @return array<int,string>
1206 + */
1207 + private function getHiddenPanelWidgetNames(int $post_id): array
1208 + {
1209 + $hidden_categories = $this->getHiddenPanelCategoryKeys($post_id);
1210 + if ($hidden_categories === [] || !did_action('elementor/loaded')) {
1211 + return [];
1212 + }
1213 +
1214 + $hidden_lookup = array_fill_keys($hidden_categories, true);
1215 + $names = [];
1216 +
1217 + foreach (Plugin::instance()->widgets_manager->get_widget_types() as $widget_name => $widget) {
1218 + if (!is_object($widget) || !method_exists($widget, 'get_categories')) {
1219 + continue;
1220 + }
1221 +
1222 + foreach ((array) $widget->get_categories() as $category) {
1223 + if (isset($hidden_lookup[$category])) {
1224 + $names[] = (string) $widget_name;
1225 + break;
1226 + }
1227 + }
1228 + }
1229 +
1230 + return $names;
1231 + }
1232 +
1233 + /**
1234 + * Whether the current request is the Elementor editor (or its AJAX).
1235 + *
1236 + * @return bool
1237 + */
1238 + private function shouldApplyEditorPanelVisibility(): bool
1239 + {
1240 + if (!empty($_GET['action']) && 'elementor' === $_GET['action']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1241 + return true;
1242 + }
1243 +
1244 + if (!empty($_REQUEST['editor_post_id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1245 + return true;
1246 + }
1247 +
1248 + return class_exists('\Elementor\Plugin')
1249 + && Plugin::instance()->editor
1250 + && Plugin::instance()->editor->is_edit_mode();
1251 + }
1252 +
1253 + /**
1254 + * Current Elementor editor post ID, if any.
1255 + *
1256 + * @return int
1257 + */
1258 + private function getCurrentEditorPostId(): int
1259 + {
1260 + foreach (['editor_post_id', 'post_id', 'post'] as $key) {
1261 + if (!empty($_REQUEST[$key])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1262 + $post_id = absint(wp_unslash($_REQUEST[$key])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1263 + if ($post_id > 0) {
1264 + return $post_id;
1265 + }
1266 + }
1267 + }
1268 +
1269 + $post = get_post();
1270 + return $post ? (int) $post->ID : 0;
1271 + }
1272 +
1273 + /**
1274 + * Whether the post is a King Addons Woo Builder template.
1275 + *
1276 + * @param int $post_id
1277 + * @return bool
1278 + */
1279 + private function isWooBuilderDocument(int $post_id): bool
1280 + {
1281 + if ($post_id <= 0) {
1282 + return false;
1283 + }
1284 +
1285 + $elementor_type = (string) get_post_meta($post_id, '_elementor_template_type', true);
1286 + if ('king-addons-woo-builder' === $elementor_type) {
1287 + return true;
1288 + }
1289 +
1290 + return (string) get_post_meta($post_id, 'ka_woo_template_type', true) !== '';
1291 + }
1292 +
1293 + /**
1294 + * Whether the post is a King Addons Theme Builder template.
1295 + *
1296 + * @param int $post_id
1297 + * @return bool
1298 + */
1299 + private function isThemeBuilderDocument(int $post_id): bool
1300 + {
1301 + if ($post_id <= 0) {
1302 + return false;
1303 + }
1304 +
1305 + $location_key = '_ka_theme_builder_location';
1306 + if (class_exists('\King_Addons\Theme_Builder\Meta_Keys')) {
1307 + $location_key = \King_Addons\Theme_Builder\Meta_Keys::LOCATION;
1308 + }
1309 +
1310 + return (string) get_post_meta($post_id, $location_key, true) !== '';
1311 + }
1312 +
1313 + /**
1314 + * Whether the post is a King Addons Loop Item template.
1315 + *
1316 + * @param int $post_id
1317 + * @return bool
1318 + */
1319 + private function isLoopItemDocument(int $post_id): bool
1320 + {
1321 + if ($post_id <= 0) {
1322 + return false;
1323 + }
1324 +
1325 + return 'king-addons-loop-item' === (string) get_post_meta($post_id, '_elementor_template_type', true);
1326 + }
1327 +
1328 + /**
630 1329 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
631 1330 * that caused a fatal error previously, and try them again if the plugin version is updated.
632 1331 *
633 1332 * @param Widgets_Manager $widgets_manager
@@ -915,8 +1614,40 @@
915 1614 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-form-builder-editor-handler', KING_ADDONS_URL . 'includes/widgets/Form_Builder/editor-handler.js', '', KING_ADDONS_VERSION);
916 1615 }
917 1616 }
918 1617
1618 + /**
1619 + * Widget settings with dynamic tags resolved, and nothing missing.
1620 + *
1621 + * get_settings() never parses dynamic tags, so a tag placed in such a
1622 + * widget printed its placeholder instead of the value. get_settings_for_display()
1623 + * does parse them, but it also drops every setting whose control condition
1624 + * is currently false - and widgets written against the raw array index into
1625 + * those keys without checking, which turns the switch into a batch of
1626 + * "Trying to access array offset on null" warnings.
1627 + *
1628 + * A hidden control comes back as null rather than missing, so the parsed
1629 + * value is preferred only when there is one, and the raw value fills in
1630 + * everywhere else.
1631 + *
1632 + * @param \Elementor\Controls_Stack $widget Widget being rendered.
1633 + *
1634 + * @return array<string,mixed>
1635 + */
1636 + public static function displaySettings($widget): array
1637 + {
1638 + $raw = (array) $widget->get_settings();
1639 + $display = (array) $widget->get_settings_for_display();
1640 +
1641 + foreach ($display as $key => $value) {
1642 + if (null !== $value) {
1643 + $raw[$key] = $value;
1644 + }
1645 + }
1646 +
1647 + return $raw;
1648 + }
1649 +
919 1650 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
920 1651 {
921 1652 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
922 1653 return;
@@ -943,8 +1674,34 @@
943 1674
944 1675 $module->end_controls_section();
945 1676 }
946 1677
1678 + /**
1679 + * Same upgrade notice as renderUpgradeProNotice(), but not tied to another
1680 + * control's value. Use it where a whole section is Pro-only, so the free
1681 + * build does not show a section header that opens onto nothing.
1682 + *
1683 + * @param mixed $module Widget or repeater the control belongs to.
1684 + * @param string $controls_manager Control type to render the notice with.
1685 + * @param string $widget_name Widget slug, used for the campaign link.
1686 + * @param string $control_id Unique control id for the notice.
1687 + */
1688 + public static function renderUpgradeProSection($module, $controls_manager, string $widget_name, string $control_id): void
1689 + {
1690 + if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1691 + return;
1692 + }
1693 +
1694 + $module->add_control(
1695 + $control_id,
1696 + [
1697 + 'raw' => 'Upgrade to the <strong><a href="https://kingaddons.com/pricing/?utm_source=kng-module-' . $widget_name . '-settings-upgrade-pro&utm_medium=plugin&utm_campaign=kng" target="_blank">Pro version</a></strong> now<br> and unlock this feature!',
1698 + 'type' => $controls_manager,
1699 + 'content_classes' => 'king-addons-pro-notice',
1700 + ]
1701 + );
1702 + }
1703 +
947 1704 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
948 1705 {
949 1706 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
950 1707 return;
@@ -998,16 +1755,19 @@
998 1755 $image = esc_url($args['image']);
999 1756 $network = $args['network'];
1000 1757
1001 1758 $get_whatsapp_url = function ($a) {
1002 - if ('yes' === $a['show_whatsapp_title'] && 'yes' === $a['show_whatsapp_excerpt']) {
1003 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['text'] . '%0a' . $a['url'];
1004 - } elseif ('yes' === $a['show_whatsapp_title']) {
1005 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['url'];
1006 - } elseif ('yes' === $a['show_whatsapp_excerpt']) {
1007 - return 'https://api.whatsapp.com/send?text=*' . $a['text'] . '%0a' . $a['url'];
1759 + $parts = [];
1760 + if ('yes' === $a['show_whatsapp_title'] && $a['title'] !== '') {
1761 + $parts[] = '*' . $a['title'] . '*';
1008 1762 }
1009 - return 'https://api.whatsapp.com/send?text=' . $a['url'];
1763 + if ('yes' === $a['show_whatsapp_excerpt'] && $a['text'] !== '') {
1764 + $parts[] = $a['text'];
1765 + }
1766 + if ($a['url'] !== '') {
1767 + $parts[] = $a['url'];
1768 + }
1769 + return 'https://api.whatsapp.com/send?text=' . rawurlencode(implode("\n", $parts));
1010 1770 };
1011 1771
1012 1772 $networks_map = [
1013 1773 'facebook-f' => [
@@ -1099,10 +1859,17 @@
1099 1859 $share_url = $networks_map[$network]['url'];
1100 1860 $network_title = $networks_map[$network]['title'];
1101 1861 $icon_category = $networks_map[$network]['icon'];
1102 1862
1103 - $output = '<a href="' . esc_url($share_url) . '" class="king-addons-share-icon king-addons-share-' . esc_attr($network) . '" target="_blank">';
1863 + // esc_url() strips encoded newlines (%0A), which concatenates WhatsApp title and URL.
1864 + if ('whatsapp' === $network && str_starts_with($share_url, 'https://api.whatsapp.com/send?text=')) {
1865 + $href = esc_attr($share_url);
1866 + } else {
1867 + $href = esc_url($share_url);
1868 + }
1104 1869
1870 + $output = '<a href="' . $href . '" class="king-addons-share-icon king-addons-share-' . esc_attr($network) . '" target="_blank">';
1871 +
1105 1872 if ('yes' === $args['tooltip']) {
1106 1873 $output .= '<span class="king-addons-share-tooltip king-addons-tooltip">' . esc_html($network_title) . '</span>';
1107 1874 }
1108 1875
@@ -1307,9 +2074,14 @@
1307 2074 if (!$api_key) {
1308 2075 return $mailchimp_list;
1309 2076 }
1310 2077
1311 - $url = 'https://' . explode('-', $api_key)[1] . '.api.mailchimp.com/3.0/lists/';
2078 + $parts = explode('-', (string) $api_key);
2079 + if (count($parts) < 2 || '' === $parts[1]) {
2080 + return $mailchimp_list;
2081 + }
2082 +
2083 + $url = 'https://' . $parts[1] . '.api.mailchimp.com/3.0/lists/';
1312 2084 $response = wp_remote_get($url, [
1313 2085 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
1314 2086 ]);
1315 2087
@@ -1324,12 +2096,22 @@
1324 2096 }
1325 2097
1326 2098 public static function getMailchimpGroups()
1327 2099 {
1328 - $apiKey = get_option('king_addons_mailchimp_api_key');
1329 - $domain = 'https://' . substr($apiKey, strpos($apiKey, '-') + 1) . '.api.mailchimp.com/3.0/';
2100 + $apiKey = (string) get_option('king_addons_mailchimp_api_key', '');
2101 + $groups = ['def' => 'Select Group'];
2102 +
2103 + if ('' === $apiKey || false === strpos($apiKey, '-')) {
2104 + return $groups;
2105 + }
2106 +
2107 + $dc = substr($apiKey, strpos($apiKey, '-') + 1);
2108 + if ('' === $dc) {
2109 + return $groups;
2110 + }
2111 +
2112 + $domain = 'https://' . $dc . '.api.mailchimp.com/3.0/';
1330 2113 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
1331 - $groups = ['def' => 'Select Group'];
1332 2114 $mailchimpIDs = Core::getMailchimpLists();
1333 2115
1334 2116 foreach ($mailchimpIDs as $audience => $ignore) {
1335 2117 if ($audience === 'def') {
@@ -1335,14 +2117,20 @@
1335 2117 if ($audience === 'def') {
1336 2118 continue;
1337 2119 }
1338 2120
1339 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
1340 - $cats = json_decode($cats['body'])->categories ?? [];
2121 + $cats_res = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
2122 + if (is_wp_error($cats_res)) {
2123 + continue;
2124 + }
2125 + $cats = json_decode((string) wp_remote_retrieve_body($cats_res))->categories ?? [];
1341 2126
1342 2127 foreach ($cats as $cat) {
1343 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
1344 - $interests = json_decode($interests['body'])->interests ?? [];
2128 + $interests_res = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
2129 + if (is_wp_error($interests_res)) {
2130 + continue;
2131 + }
2132 + $interests = json_decode((string) wp_remote_retrieve_body($interests_res))->interests ?? [];
1345 2133
1346 2134 foreach ($interests as $int) {
1347 2135 $groups[$int->id] = $int->name;
1348 2136 }
@@ -1478,8 +2266,10 @@
1478 2266 * @return void
1479 2267 */
1480 2268 public function enqueueAiFieldScript(): void
1481 2269 {
2270 + $ai_options = get_option('king_addons_ai_options', []);
2271 +
1482 2272 wp_enqueue_script(
1483 2273 'king-addons-ai-field',
1484 2274 KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
1485 2275 ['jquery', 'elementor-editor'],
@@ -1503,8 +2293,49 @@
1503 2293 'plugin_url' => KING_ADDONS_URL,
1504 2294 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1505 2295 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1506 2296 'translator_enabled' => isset($ai_options['enable_ai_page_translator']) ? (bool) $ai_options['enable_ai_page_translator'] : true,
2297 + // Editor prompts name the configured provider rather than always OpenAI.
2298 + 'provider' => \King_Addons\AI_Provider::getProvider(),
2299 + 'provider_label' => \King_Addons\AI_Provider::getLabel(),
2300 + 'api_keys_url' => \King_Addons\AI_Provider::getApiKeysUrl(),
2301 + 'api_keys_label' => \King_Addons\AI_Provider::isOpenRouter()
2302 + ? esc_html__('OpenRouter Keys', 'king-addons')
2303 + : esc_html__('OpenAI Platform', 'king-addons'),
2304 + 'setup_billing_note' => \King_Addons\AI_Provider::isOpenRouter()
2305 + ? esc_html__('Free models work without adding any credit.', 'king-addons')
2306 + : esc_html__('and top up your OpenAI account balance by at least $5', 'king-addons'),
2307 + 'setup_cost_note' => \King_Addons\AI_Provider::isOpenRouter()
2308 + ? esc_html__('Free models cost nothing. Paid models cost pennies (about $0.01 per full page).', 'king-addons')
2309 + : esc_html__('Processing a page costs pennies (about $0.01 per full page).', 'king-addons'),
2310 + /**
2311 + * Before either provider has a key there is no "configured
2312 + * provider" to describe - the setting is just its default. The
2313 + * setup dialog then has to present the choice instead of sending
2314 + * everyone to OpenAI.
2315 + */
2316 + 'has_any_key' => (
2317 + '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENAI)
2318 + || '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENROUTER)
2319 + ),
2320 + 'setup_providers' => [
2321 + [
2322 + 'name' => esc_html__('OpenRouter', 'king-addons'),
2323 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENROUTER),
2324 + 'note' => esc_html__('has free models, no credit needed to start', 'king-addons'),
2325 + ],
2326 + [
2327 + 'name' => esc_html__('OpenAI', 'king-addons'),
2328 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENAI),
2329 + 'note' => esc_html__('needs at least $5 on your account balance', 'king-addons'),
2330 + ],
2331 + ],
2332 + 'setup_cost_note_neutral' => esc_html__('With a free OpenRouter model, nothing. On a paid model it is pennies (about $0.01 per full page).', 'king-addons'),
2333 + 'missing_key_message' => sprintf(
2334 + /* translators: %s: provider name */
2335 + esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
2336 + \King_Addons\AI_Provider::getLabel()
2337 + ),
1507 2338 ]
1508 2339 );
1509 2340 }
1510 2341
@@ -1514,10 +2345,8 @@
1514 2345 * @return void
1515 2346 */
1516 2347 public function enqueueAiImageGenerationScript(): void
1517 2348 {
1518 - // Retrieve AI options and ensure it's an array to prevent warnings.
1519 - $ai_options = get_option('king_addons_ai_options', []);
1520 2349 wp_enqueue_script(
1521 2350 'king-addons-ai-image-field',
1522 2351 KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
1523 2352 ['jquery', 'elementor-editor'],
@@ -1532,13 +2361,27 @@
1532 2361 [
1533 2362 'ajax_url' => admin_url('admin-ajax.php'),
1534 2363 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
1535 2364 'generate_action' => 'king_addons_ai_generate_image',
1536 - 'image_model' => sanitize_text_field($ai_options['openai_image_model'] ?? ''),
2365 + 'image_model' => \King_Addons\AI_Provider::getImageModel(),
2366 + // The editor builds its model dropdown from this list, so the
2367 + // options follow whichever provider is configured.
2368 + 'image_models' => array_map(
2369 + static function ($model) {
2370 + return ['value' => $model['id'], 'label' => $model['label']];
2371 + },
2372 + \King_Addons\AI_Provider::getModelsFor('image')
2373 + ),
2374 + 'provider' => \King_Addons\AI_Provider::getProvider(),
1537 2375 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1538 2376 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1539 2377 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1540 2378 'plugin_url' => KING_ADDONS_URL,
2379 + 'missing_key_message' => sprintf(
2380 + /* translators: %s: provider name */
2381 + esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
2382 + \King_Addons\AI_Provider::getLabel()
2383 + ),
1541 2384 ]
1542 2385 );
1543 2386 }
1544 2387
@@ -1598,8 +2441,182 @@
1598 2441
1599 2442 // Note: Using existing KingAddonsAiField localization
1600 2443 // The translator script will use the same AJAX endpoints and settings
1601 2444 // No need for separate localization as it reuses existing AI infrastructure
2445 + }
2446 +
2447 + /**
2448 + * Render an attachment through an Elementor image-size group control.
2449 + *
2450 + * Group_Control_Image_Size::get_attachment_image_html() takes a *key into
2451 + * $settings* as its third argument, not an attachment ID. Handing it an ID
2452 + * makes Elementor read $settings[<id>], which raises two PHP warnings and
2453 + * returns nothing, so callers silently lose the image.
2454 + *
2455 + * @param array<string, mixed> $settings Widget settings; carries "<$size_key>_size" and friends.
2456 + * @param string $size_key Name of the image-size group control.
2457 + * @param int $attachment_id Attachment to render.
2458 + *
2459 + * @return string Image HTML, or an empty string when it cannot be rendered.
2460 + */
2461 + /**
2462 + * Show the person building the page why a widget rendered nothing.
2463 + *
2464 + * Only ever printed inside the Elementor editor, so the public page keeps
2465 + * rendering exactly what it rendered before.
2466 + *
2467 + * @param string $message Plain text explaining what the widget still needs.
2468 + */
2469 + public static function renderEditorHint(string $message): void
2470 + {
2471 + if (!Plugin::$instance->editor->is_edit_mode()) {
2472 + return;
2473 + }
2474 +
2475 + $style = 'display:block;padding:12px 14px;border:1px dashed #c3c4c7;border-radius:4px;'
2476 + . 'background:#f6f7f7;color:#50575e;font-size:13px;line-height:1.5;';
2477 +
2478 + echo '<div class="king-addons-editor-hint" style="' . esc_attr($style) . '">'
2479 + . esc_html($message) . '</div>';
2480 + }
2481 +
2482 + /**
2483 + * A setting written straight into a JavaScript object literal has to be a
2484 + * number. Elementor stores a number field the user cleared as an empty
2485 + * string, which would emit `slidesPerView: ,` - a syntax error that stops
2486 + * the whole carousel from initialising - or blow up in PHP when the value
2487 + * is used in arithmetic.
2488 + *
2489 + * @param array<string, mixed> $settings Widget settings.
2490 + * @param string $key Setting to read.
2491 + * @param int|float $fallback Value to use when the setting is empty or not a number.
2492 + *
2493 + * @return string Numeric string, safe to interpolate into JS.
2494 + */
2495 + public static function jsNumber(array $settings, string $key, $fallback = 0): string
2496 + {
2497 + $value = $settings[$key] ?? null;
2498 +
2499 + // Slider controls keep their number under 'size'.
2500 + if (is_array($value)) {
2501 + $value = $value['size'] ?? null;
2502 + }
2503 +
2504 + if (!is_numeric($value)) {
2505 + $value = $fallback;
2506 + }
2507 +
2508 + return (string) (0 + $value);
2509 + }
2510 +
2511 + /**
2512 + * Swiper breakpoints use viewport width, so a desktop page with a ~600px
2513 + * content column still shows 3–5 cards and crushes the text. Print a helper
2514 + * that clamps slidesPerView to the carousel's own width.
2515 + */
2516 + public static function printFitSlidesHelper(): void
2517 + {
2518 + static $printed = false;
2519 + if ($printed) {
2520 + return;
2521 + }
2522 + $printed = true;
2523 + // Avoid && in this snippet: the_content encodes ampersands and would
2524 + // turn the helper into invalid JavaScript.
2525 + wp_print_inline_script_tag(<<<'JS'
2526 +window.kingAddonsFitSlidesPerView = function (el, desired, minWidth, space) {
2527 + desired = parseFloat(desired) || 1;
2528 + minWidth = parseFloat(minWidth) || 240;
2529 + space = parseFloat(space) || 0;
2530 + if (!el) {
2531 + return Math.max(1, desired);
2532 + }
2533 + var width = el.clientWidth || 0;
2534 + if (width <= 0) {
2535 + return Math.max(1, desired);
2536 + }
2537 + var maxFit = Math.max(1, Math.floor((width + space) / (minWidth + space)));
2538 + return Math.min(desired, maxFit);
2539 +};
2540 +window.kingAddonsBindFitSlides = function (swiper, el, minWidth) {
2541 + if (!swiper) {
2542 + return;
2543 + }
2544 + if (!el) {
2545 + return;
2546 + }
2547 + if (typeof swiper.params === 'undefined') {
2548 + return;
2549 + }
2550 + minWidth = parseFloat(minWidth) || 240;
2551 + var apply = function () {
2552 + var passed = swiper.passedParams || {};
2553 + var bpKey = swiper.currentBreakpoint;
2554 + var fromBp = null;
2555 + if (bpKey) {
2556 + if (passed.breakpoints) {
2557 + fromBp = passed.breakpoints[bpKey] || null;
2558 + }
2559 + }
2560 + var desiredRaw = passed.slidesPerView;
2561 + if (fromBp) {
2562 + if (typeof fromBp.slidesPerView !== 'undefined') {
2563 + desiredRaw = fromBp.slidesPerView;
2564 + }
2565 + }
2566 + var desired = parseFloat(desiredRaw) || 1;
2567 + var spaceRaw = swiper.params.spaceBetween;
2568 + if (fromBp) {
2569 + if (typeof fromBp.spaceBetween !== 'undefined') {
2570 + spaceRaw = fromBp.spaceBetween;
2571 + }
2572 + }
2573 + var space = parseFloat(spaceRaw) || 0;
2574 + var next = window.kingAddonsFitSlidesPerView(el, desired, minWidth, space);
2575 + if (swiper.params.slidesPerView !== next) {
2576 + swiper.params.slidesPerView = next;
2577 + if (typeof swiper.update === 'function') {
2578 + swiper.update();
2579 + }
2580 + }
2581 + };
2582 + apply();
2583 + if (typeof swiper.on === 'function') {
2584 + swiper.on('breakpoint', apply);
2585 + }
2586 + if (window.ResizeObserver) {
2587 + new ResizeObserver(apply).observe(el);
2588 + } else {
2589 + window.addEventListener('resize', apply);
2590 + }
2591 +};
2592 +JS
2593 + );
2594 + }
2595 +
2596 + /**
2597 + * Wrap an inline `new Swiper(...)` so slidesPerView is clamped to the container.
2598 + */
2599 + public static function wrapFitSwiper(string $selector, string $js_swiper, int $min_slide = 240): string
2600 + {
2601 + $sel = wp_json_encode($selector);
2602 + $min = (string) max(1, $min_slide);
2603 + return '(function(){var el=document.querySelector(' . $sel . ');if(!el){return;}if(typeof Swiper==="undefined"){return;}var sw=' . $js_swiper . ';if(typeof kingAddonsBindFitSlides==="function"){kingAddonsBindFitSlides(sw,el,' . $min . ');}})()';
2604 + }
2605 +
2606 + public static function getAttachmentImageHTML(array $settings, string $size_key, int $attachment_id): string
2607 + {
2608 + if ($attachment_id < 1) {
2609 + return '';
2610 + }
2611 +
2612 + $image_key = '__king_addons_image';
2613 + $settings[$image_key] = [
2614 + 'id' => $attachment_id,
2615 + 'url' => wp_get_attachment_image_url($attachment_id, 'full') ?: '',
2616 + ];
2617 +
2618 + return (string) Group_Control_Image_Size::get_attachment_image_html($settings, $size_key, $image_key);
1602 2619 }
1603 2620 }
1604 2621
1605 2622 Core::instance();