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 +953 -64 51.1.65 → 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.
@@ -103,8 +104,10 @@
103 104 public function __construct()
104 105 {
105 106 require_once(KING_ADDONS_PATH . 'includes/ModulesMap.php');
106 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();
107 110
108 111 if ($this->hasElementorCompatibility()) {
109 112
110 113 // Initial requirements check
@@ -138,8 +141,26 @@
138 141 require_once(KING_ADDONS_PATH . 'includes/extensions/Cookie_Consent/Cookie_Consent.php');
139 142 Cookie_Consent::instance();
140 143 }
141 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 +
142 163 // WooCommerce Builder
143 164 if ($this->isExtensionEnabled('woo-builder', 'KING_ADDONS_EXT_WOO_BUILDER')) {
144 165 require_once(KING_ADDONS_PATH . 'includes/extensions/Woo_Builder/Woo_Builder.php');
145 166 if (class_exists('King_Addons\\Woo_Builder')) {
@@ -276,8 +297,24 @@
276 297 require_once(KING_ADDONS_PATH . 'includes/extensions/Site_Preloader/Site_Preloader.php');
277 298 Site_Preloader::instance();
278 299 }
279 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 +
280 317 // Image Optimizer
281 318 if ($this->isExtensionEnabled('image-optimizer', 'KING_ADDONS_EXT_IMAGE_OPTIMIZER')) {
282 319 require_once(KING_ADDONS_PATH . 'includes/extensions/Image_Optimizer/Image_Optimizer.php');
283 320 \King_Addons\Image_Optimizer\Image_Optimizer::instance();
@@ -303,8 +340,9 @@
303 340 require_once(KING_ADDONS_PATH . 'includes/widgets/Search/Search_Ajax.php');
304 341 require_once(KING_ADDONS_PATH . 'includes/widgets/MailChimp/MailChimp_Ajax.php');
305 342
306 343 // Additional - Grids, Magazine Grid
344 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Grid_Ajax_Security.php');
307 345 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_Posts_Ajax.php');
308 346 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_WooCommerce_Products_Ajax.php');
309 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
310 348
@@ -309,8 +347,20 @@
309 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
310 348
311 349 // Additional - Form Builder
312 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');
313 363 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
314 364 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
315 365 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
316 366 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Subscribe_Mailchimp.php');
@@ -340,8 +390,17 @@
340 390 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Frontend.php';
341 391 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_WooCommerce.php';
342 392 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Module.php';
343 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 + });
344 403 }
345 404
346 405 // Dynamic Posts Grid AJAX Helper - Initialize regardless of Elementor compatibility
347 406 // This is needed for AJAX functionality to work even when PRO version is disabled
@@ -359,8 +418,13 @@
359 418
360 419 add_action('elementor/init', [$this, 'initElementor']);
361 420
362 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);
363 427 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
364 428
365 429 self::enableFeatures();
366 430
@@ -399,9 +463,23 @@
399 463
400 464 // Notice - Upgrade Suggestion
401 465 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
402 466 add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
403 - 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 + );
404 482 }
405 483
406 484 // Dashboard UI settings AJAX handler
407 485 add_action('wp_ajax_king_addons_save_dashboard_ui', [$this, 'king_addons_save_dashboard_ui_callback']);
@@ -521,12 +599,12 @@
521 599 style="border-left: 4px solid #0071e3;padding: 10px 15px;">
522 600 <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
523 601 <span>
524 602 Get <strong style="font-weight: 700;">4,000+</strong> premium templates and sections,
525 - <strong style="font-weight: 700;">80+</strong> widgets,
603 + <strong style="font-weight: 700;">100+</strong> widgets,
526 604 <strong style="font-weight: 700;">200+</strong> advanced features,
527 605 and AI tools for Elementor.
528 - From $<strong style="font-weight: 700;">4</strong>/mo, billed annually.
606 + From $<strong style="font-weight: 700;">6.99</strong>/mo.
529 607 </span>
530 608 </p>
531 609 <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
532 610 <p style="display: flex;">
@@ -574,8 +652,241 @@
574 652 </script>
575 653 <?php
576 654 }
577 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 +
578 889 function enqueueFrontendStyles()
579 890 {
580 891 /**
581 892 * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
@@ -668,32 +979,116 @@
668 979 function addWidgetCategory(): void
669 980 {
670 981 $elements_manager = Plugin::instance()->elements_manager;
671 982
672 - // Add our categories
673 983 $elements_manager->add_category(
674 984 'king-addons',
675 985 [
676 986 'title' => esc_html__('King Addons', 'king-addons'),
677 - 'icon' => 'fa fa-plug'
987 + 'icon' => 'fa fa-plug',
678 988 ]
679 989 );
680 990
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 +
681 1011 $elements_manager->add_category(
682 - 'king-addons-woo-builder',
1012 + 'king-addons-theme-builder',
683 1013 [
684 - 'title' => esc_html__('King Addons Woo Builder', 'king-addons'),
685 - 'icon' => 'fa fa-shopping-cart'
1014 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1015 + 'icon' => 'fa fa-plug',
1016 + 'hideIfEmpty' => true,
686 1017 ]
687 1018 );
1019 + }
688 1020
689 - // Move our categories to the top of the panel
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();
690 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 + }
691 1044 }
692 1045
693 1046 /**
694 - * Reorder widget categories so King Addons categories appear after Layout and Basic.
1047 + * Overwrite the editor panel category snapshot after Elementor has built document config.
695 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 + *
696 1091 * @param \Elementor\Elements_Manager $elements_manager
697 1092 * @return void
698 1093 */
699 1094 private function reorderWidgetCategories($elements_manager): void
@@ -700,56 +1095,77 @@
700 1095 {
701 1096 try {
702 1097 $reflection = new \ReflectionClass($elements_manager);
703 1098 $categories_property = $reflection->getProperty('categories');
704 - $categories_property->setAccessible(true);
1099 + if (PHP_VERSION_ID < 80100) {
1100 + $categories_property->setAccessible(true);
1101 + }
705 1102
706 1103 $categories = $categories_property->getValue($elements_manager);
707 - if (!is_array($categories)) {
1104 + if (!is_array($categories) || $categories === []) {
708 1105 return;
709 1106 }
710 1107
711 - // Extract our categories
712 - $our_categories = [];
713 - if (isset($categories['king-addons'])) {
714 - $our_categories['king-addons'] = $categories['king-addons'];
715 - unset($categories['king-addons']);
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 + }
716 1120 }
717 - if (isset($categories['king-addons-woo-builder'])) {
718 - $our_categories['king-addons-woo-builder'] = $categories['king-addons-woo-builder'];
719 - unset($categories['king-addons-woo-builder']);
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 + }
720 1128 }
1129 + $ours_sorted += $ours;
721 1130
722 - // Insert our categories after Layout and Basic
723 - $reordered = [];
724 - $insert_after = ['layout', 'basic']; // Categories after which we insert ours
725 - $inserted = false;
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 + }
726 1136
727 - foreach ($categories as $key => $value) {
728 - $reordered[$key] = $value;
729 -
730 - // Insert our categories after the last target category
731 - if (!$inserted && in_array($key, $insert_after, true)) {
732 - // Check if next category is also in our target list
733 - $keys = array_keys($categories);
734 - $current_index = array_search($key, $keys, true);
735 - $next_key = $keys[$current_index + 1] ?? null;
736 -
737 - // Only insert if the next category is NOT in our target list
738 - if ($next_key === null || !in_array($next_key, $insert_after, true)) {
739 - $reordered = array_merge($reordered, $our_categories);
740 - $inserted = true;
741 - }
742 - }
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);
743 1156 }
744 1157
745 - // If target categories weren't found, append at the end
746 - if (!$inserted) {
747 - $reordered = array_merge($reordered, $our_categories);
1158 + $categories = $categories_property->getValue($elements_manager);
1159 + if (!is_array($categories) || $categories === []) {
1160 + return;
748 1161 }
749 1162
750 - // Set back the reordered array
751 - $categories_property->setValue($elements_manager, $reordered);
1163 + foreach ($hidden as $key) {
1164 + unset($categories[$key]);
1165 + }
1166 +
1167 + $categories_property->setValue($elements_manager, $categories);
752 1168 } catch (\ReflectionException $e) {
753 1169 // Silently fail if reflection doesn't work (e.g., future Elementor changes)
754 1170 }
755 1171 }
@@ -754,8 +1170,163 @@
754 1170 }
755 1171 }
756 1172
757 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 + /**
758 1329 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
759 1330 * that caused a fatal error previously, and try them again if the plugin version is updated.
760 1331 *
761 1332 * @param Widgets_Manager $widgets_manager
@@ -1043,8 +1614,40 @@
1043 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);
1044 1615 }
1045 1616 }
1046 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 +
1047 1650 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
1048 1651 {
1049 1652 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1050 1653 return;
@@ -1071,8 +1674,34 @@
1071 1674
1072 1675 $module->end_controls_section();
1073 1676 }
1074 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 +
1075 1704 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
1076 1705 {
1077 1706 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1078 1707 return;
@@ -1126,16 +1755,19 @@
1126 1755 $image = esc_url($args['image']);
1127 1756 $network = $args['network'];
1128 1757
1129 1758 $get_whatsapp_url = function ($a) {
1130 - if ('yes' === $a['show_whatsapp_title'] && 'yes' === $a['show_whatsapp_excerpt']) {
1131 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['text'] . '%0a' . $a['url'];
1132 - } elseif ('yes' === $a['show_whatsapp_title']) {
1133 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['url'];
1134 - } elseif ('yes' === $a['show_whatsapp_excerpt']) {
1135 - 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'] . '*';
1136 1762 }
1137 - 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));
1138 1770 };
1139 1771
1140 1772 $networks_map = [
1141 1773 'facebook-f' => [
@@ -1227,10 +1859,17 @@
1227 1859 $share_url = $networks_map[$network]['url'];
1228 1860 $network_title = $networks_map[$network]['title'];
1229 1861 $icon_category = $networks_map[$network]['icon'];
1230 1862
1231 - $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 + }
1232 1869
1870 + $output = '<a href="' . $href . '" class="king-addons-share-icon king-addons-share-' . esc_attr($network) . '" target="_blank">';
1871 +
1233 1872 if ('yes' === $args['tooltip']) {
1234 1873 $output .= '<span class="king-addons-share-tooltip king-addons-tooltip">' . esc_html($network_title) . '</span>';
1235 1874 }
1236 1875
@@ -1435,9 +2074,14 @@
1435 2074 if (!$api_key) {
1436 2075 return $mailchimp_list;
1437 2076 }
1438 2077
1439 - $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/';
1440 2084 $response = wp_remote_get($url, [
1441 2085 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
1442 2086 ]);
1443 2087
@@ -1452,12 +2096,22 @@
1452 2096 }
1453 2097
1454 2098 public static function getMailchimpGroups()
1455 2099 {
1456 - $apiKey = get_option('king_addons_mailchimp_api_key');
1457 - $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/';
1458 2113 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
1459 - $groups = ['def' => 'Select Group'];
1460 2114 $mailchimpIDs = Core::getMailchimpLists();
1461 2115
1462 2116 foreach ($mailchimpIDs as $audience => $ignore) {
1463 2117 if ($audience === 'def') {
@@ -1463,14 +2117,20 @@
1463 2117 if ($audience === 'def') {
1464 2118 continue;
1465 2119 }
1466 2120
1467 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
1468 - $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 ?? [];
1469 2126
1470 2127 foreach ($cats as $cat) {
1471 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
1472 - $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 ?? [];
1473 2133
1474 2134 foreach ($interests as $int) {
1475 2135 $groups[$int->id] = $int->name;
1476 2136 }
@@ -1606,8 +2266,10 @@
1606 2266 * @return void
1607 2267 */
1608 2268 public function enqueueAiFieldScript(): void
1609 2269 {
2270 + $ai_options = get_option('king_addons_ai_options', []);
2271 +
1610 2272 wp_enqueue_script(
1611 2273 'king-addons-ai-field',
1612 2274 KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
1613 2275 ['jquery', 'elementor-editor'],
@@ -1631,8 +2293,49 @@
1631 2293 'plugin_url' => KING_ADDONS_URL,
1632 2294 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1633 2295 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1634 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 + ),
1635 2338 ]
1636 2339 );
1637 2340 }
1638 2341
@@ -1642,10 +2345,8 @@
1642 2345 * @return void
1643 2346 */
1644 2347 public function enqueueAiImageGenerationScript(): void
1645 2348 {
1646 - // Retrieve AI options and ensure it's an array to prevent warnings.
1647 - $ai_options = get_option('king_addons_ai_options', []);
1648 2349 wp_enqueue_script(
1649 2350 'king-addons-ai-image-field',
1650 2351 KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
1651 2352 ['jquery', 'elementor-editor'],
@@ -1660,13 +2361,27 @@
1660 2361 [
1661 2362 'ajax_url' => admin_url('admin-ajax.php'),
1662 2363 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
1663 2364 'generate_action' => 'king_addons_ai_generate_image',
1664 - '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(),
1665 2375 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1666 2376 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1667 2377 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1668 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 + ),
1669 2384 ]
1670 2385 );
1671 2386 }
1672 2387
@@ -1726,8 +2441,182 @@
1726 2441
1727 2442 // Note: Using existing KingAddonsAiField localization
1728 2443 // The translator script will use the same AJAX endpoints and settings
1729 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);
1730 2619 }
1731 2620 }
1732 2621
1733 2622 Core::instance();