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