PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.86
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.86
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 51.1.45 All 40 releases
← All changes | includes/Core.php +829 -56 51.1.7851.1.86 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,19 @@
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();
314 362 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Form_Builder_Security.php');
315 363 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
316 364 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
317 365 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
@@ -361,8 +409,13 @@
361 409
362 410 add_action('elementor/init', [$this, 'initElementor']);
363 411
364 412 add_action('elementor/elements/categories_registered', [$this, 'addWidgetCategory']);
413 + // Elementor promotes Atomic / Custom Widget / Pro after that hook, so the
414 + // panel order is applied again once categories are fully initialised.
415 + add_action('elementor/init', [$this, 'reorderWidgetCategoriesLate'], 100);
416 + add_action('elementor/editor/before_enqueue_scripts', [$this, 'reorderWidgetCategoriesLate'], 1);
417 + add_filter('elementor/document/config', [$this, 'filterDocumentPanelCategories'], 1, 2);
365 418 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
366 419
367 420 self::enableFeatures();
368 421
@@ -401,9 +454,23 @@
401 454
402 455 // Notice - Upgrade Suggestion
403 456 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
404 457 add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
405 - add_action('admin_notices', [$this, 'showNoticeUpgrade']);
458 +
459 + /**
460 + * The previous notice is kept untouched in showNoticeUpgrade() as a fallback.
461 + * To go back to it, either define KING_ADDONS_UPGRADE_NOTICE_LEGACY as true
462 + * in wp-config.php, or return true from this filter.
463 + */
464 + $use_legacy_notice = defined('KING_ADDONS_UPGRADE_NOTICE_LEGACY')
465 + ? (bool) KING_ADDONS_UPGRADE_NOTICE_LEGACY
466 + : false;
467 + $use_legacy_notice = (bool) apply_filters('king_addons/upgrade_notice/use_legacy', $use_legacy_notice);
468 +
469 + add_action(
470 + 'admin_notices',
471 + [$this, $use_legacy_notice ? 'showNoticeUpgrade' : 'showNoticeUpgradeV2']
472 + );
406 473 }
407 474
408 475 // Dashboard UI settings AJAX handler
409 476 add_action('wp_ajax_king_addons_save_dashboard_ui', [$this, 'king_addons_save_dashboard_ui_callback']);
@@ -523,12 +590,12 @@
523 590 style="border-left: 4px solid #0071e3;padding: 10px 15px;">
524 591 <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
525 592 <span>
526 593 Get <strong style="font-weight: 700;">4,000+</strong> premium templates and sections,
527 - <strong style="font-weight: 700;">80+</strong> widgets,
594 + <strong style="font-weight: 700;">100+</strong> widgets,
528 595 <strong style="font-weight: 700;">200+</strong> advanced features,
529 596 and AI tools for Elementor.
530 - From $<strong style="font-weight: 700;">4</strong>/mo, billed annually.
597 + From $<strong style="font-weight: 700;">6.99</strong>/mo.
531 598 </span>
532 599 </p>
533 600 <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
534 601 <p style="display: flex;">
@@ -576,8 +643,241 @@
576 643 </script>
577 644 <?php
578 645 }
579 646
647 + /**
648 + * Upgrade notice, current design.
649 + *
650 + * Replaces showNoticeUpgrade(), which is kept above unchanged as a fallback —
651 + * see the KING_ADDONS_UPGRADE_NOTICE_LEGACY constant and the
652 + * king_addons/upgrade_notice/use_legacy filter.
653 + *
654 + * @return void
655 + */
656 + function showNoticeUpgradeV2()
657 + {
658 + if (!current_user_can('manage_options')) {
659 + return;
660 + }
661 +
662 + $user_id = get_current_user_id();
663 + $now = time();
664 + $last_dismissed = get_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', true);
665 +
666 + // Stay hidden for a week after the notice is dismissed.
667 + if ($last_dismissed && ($now - $last_dismissed) < WEEK_IN_SECONDS) {
668 + return;
669 + }
670 +
671 + $pricing_url = 'https://kingaddons.com/pricing/?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng';
672 + // Same page, different utm_source: the two links stay separately
673 + // measurable, which the previous notice could not do because both
674 + // carried kng-notice-offer.
675 + $explore_url = 'https://kingaddons.com/pricing/?utm_source=kng-notice-explore&utm_medium=plugin&utm_campaign=kng';
676 + ?>
677 + <style>
678 + .king-addons-upgrade-notice-v2 {
679 + border-left: 4px solid #5B03FF;
680 + padding: 18px 20px 18px 22px;
681 + }
682 +
683 + .king-addons-upgrade-notice-v2 .king-addons-un-main {
684 + /* Admin notices run the full width of the screen, so a second
685 + column always ends up marooned in empty space. Everything
686 + stays in one left-aligned stack instead, with the price sitting
687 + in the action row where the decision is made. */
688 + display: flex;
689 + flex-direction: column;
690 + gap: 10px;
691 + }
692 +
693 + .king-addons-upgrade-notice-v2 .king-addons-un-brand {
694 + display: flex;
695 + align-items: center;
696 + gap: 7px;
697 + font-size: 11px;
698 + font-weight: 700;
699 + letter-spacing: .09em;
700 + text-transform: uppercase;
701 + color: #5B03FF;
702 + }
703 +
704 + .king-addons-upgrade-notice-v2 .king-addons-un-brand svg {
705 + width: 14px;
706 + height: 14px;
707 + display: block;
708 + fill: currentColor;
709 + }
710 +
711 + .king-addons-upgrade-notice-v2 .king-addons-un-headline {
712 + margin: 0;
713 + font-size: 17px;
714 + font-weight: 700;
715 + line-height: 1.35;
716 + color: #1d2327;
717 + max-width: 46ch;
718 + }
719 +
720 + .king-addons-upgrade-notice-v2 .king-addons-un-sub {
721 + margin: 0;
722 + font-size: 14px;
723 + line-height: 1.5;
724 + color: #50575e;
725 + max-width: 58ch;
726 + }
727 +
728 + .king-addons-upgrade-notice-v2 .king-addons-un-actions {
729 + display: flex;
730 + align-items: center;
731 + gap: 20px;
732 + flex-wrap: wrap;
733 + margin-top: 2px;
734 + }
735 +
736 + .king-addons-upgrade-notice-v2 .king-addons-un-cta {
737 + background: #5B03FF;
738 + color: #fff;
739 + display: inline-flex;
740 + align-items: center;
741 + gap: 7px;
742 + padding: 9px 18px;
743 + font-size: 14px;
744 + font-weight: 600;
745 + text-decoration: none;
746 + border-radius: 8px;
747 + transition: background-color .15s ease;
748 + }
749 +
750 + .king-addons-upgrade-notice-v2 .king-addons-un-cta:hover, .king-addons-upgrade-notice-v2 .king-addons-un-cta:focus {
751 + background: #3D01B0;
752 + color: #fff;
753 + }
754 +
755 + .king-addons-upgrade-notice-v2 .king-addons-un-cta:focus-visible {
756 + outline: 2px solid #5B03FF;
757 + outline-offset: 2px;
758 + }
759 +
760 + .king-addons-upgrade-notice-v2 .king-addons-un-cta svg {
761 + width: 15px;
762 + height: 15px;
763 + display: block;
764 + }
765 +
766 + .king-addons-upgrade-notice-v2 .king-addons-un-secondary {
767 + font-size: 14px;
768 + font-weight: 500;
769 + color: #50575e;
770 + text-decoration: none;
771 + border-bottom: 1px solid #c9c4d8;
772 + padding-bottom: 1px;
773 + }
774 +
775 + .king-addons-upgrade-notice-v2 .king-addons-un-secondary:hover, .king-addons-upgrade-notice-v2 .king-addons-un-secondary:focus {
776 + color: #5B03FF;
777 + border-bottom-color: #5B03FF;
778 + }
779 +
780 + .king-addons-upgrade-notice-v2 .king-addons-un-trust {
781 + margin: 4px 0 0;
782 + font-size: 13px;
783 + color: #50575e;
784 + display: flex;
785 + align-items: center;
786 + gap: 8px 18px;
787 + flex-wrap: wrap;
788 + }
789 +
790 + .king-addons-upgrade-notice-v2 .king-addons-un-trust-item {
791 + display: inline-flex;
792 + align-items: baseline;
793 + gap: 6px;
794 + }
795 +
796 + .king-addons-upgrade-notice-v2 .king-addons-un-trust strong {
797 + color: #1d2327;
798 + font-weight: 700;
799 + }
800 +
801 + .king-addons-upgrade-notice-v2 .king-addons-un-check {
802 + color: #0a6b45;
803 + font-weight: 700;
804 + font-size: 12px;
805 + line-height: 1;
806 + }
807 +
808 + @media screen and (max-width: 782px) {
809 + .king-addons-upgrade-notice-v2 .king-addons-un-actions {
810 + align-items: flex-start;
811 + gap: 12px;
812 + }
813 + }
814 + </style>
815 + <div class="king-addons-upgrade-notice king-addons-upgrade-notice-v2 notice notice-info is-dismissible">
816 + <div class="king-addons-un-main">
817 + <div class="king-addons-un-brand">
818 + <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>
819 + <?php esc_html_e('King Addons Pro', 'king-addons'); ?>
820 + </div>
821 + <h2 class="king-addons-un-headline">
822 + <?php esc_html_e('Mega Menu, Popup Builder, WooCommerce Builder, Theme Builder &amp; AI Tools', 'king-addons'); ?>
823 + </h2>
824 + <p class="king-addons-un-sub">
825 + <?php esc_html_e('200+ Pro features, premium widgets and 4,000+ templates &amp; sections for Elementor.', 'king-addons'); ?>
826 + </p>
827 + <div class="king-addons-un-actions">
828 + <a class="king-addons-un-cta" href="<?php echo esc_url($pricing_url); ?>" target="_blank" rel="noopener noreferrer">
829 + <?php
830 + printf(
831 + /* translators: %s: monthly price, for example $6.99/mo */
832 + esc_html__('Upgrade to Pro for %s', 'king-addons'),
833 + esc_html__('$6.99/mo', 'king-addons')
834 + );
835 + ?>
836 + <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>
837 + </a>
838 + <a class="king-addons-un-secondary" href="<?php echo esc_url($explore_url); ?>" target="_blank" rel="noopener noreferrer">
839 + <?php esc_html_e('Explore Pro features', 'king-addons'); ?>
840 + </a>
841 + </div>
842 + <p class="king-addons-un-trust">
843 + <span class="king-addons-un-trust-item">
844 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
845 + <?php esc_html_e('30-day money-back guarantee', 'king-addons'); ?>
846 + </span>
847 + <span class="king-addons-un-trust-item">
848 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
849 + <?php esc_html_e('Cancel anytime', 'king-addons'); ?>
850 + </span>
851 + <span class="king-addons-un-trust-item">
852 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
853 + <?php
854 + printf(
855 + /* translators: %s: number of users, wrapped in bold */
856 + esc_html__('Trusted by %s users', 'king-addons'),
857 + '<strong>' . esc_html__('20,000+', 'king-addons') . '</strong>'
858 + );
859 + ?>
860 + </span>
861 + </p>
862 + </div>
863 + </div>
864 + <script>
865 + (function ($) {
866 + const kingAddonsPremiumNoticeNonce = '<?php echo esc_js(wp_create_nonce('king_addons_premium_notice_dismiss')); ?>';
867 + $(document).ready(function () {
868 + $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function () {
869 + $.post(ajaxurl, {
870 + action: 'king_addons_premium_notice_dismiss',
871 + nonce: kingAddonsPremiumNoticeNonce
872 + });
873 + });
874 + });
875 + })(jQuery);
876 + </script>
877 + <?php
878 + }
879 +
580 880 function enqueueFrontendStyles()
581 881 {
582 882 /**
583 883 * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
@@ -670,32 +970,116 @@
670 970 function addWidgetCategory(): void
671 971 {
672 972 $elements_manager = Plugin::instance()->elements_manager;
673 973
674 - // Add our categories
675 974 $elements_manager->add_category(
676 975 'king-addons',
677 976 [
678 977 'title' => esc_html__('King Addons', 'king-addons'),
679 - 'icon' => 'fa fa-plug'
978 + 'icon' => 'fa fa-plug',
680 979 ]
681 980 );
682 981
982 + if (class_exists('WooCommerce') && function_exists('WC')) {
983 + $elements_manager->add_category(
984 + 'king-addons-woo',
985 + [
986 + 'title' => esc_html__('King Addons WooCommerce', 'king-addons'),
987 + 'icon' => 'fa fa-shopping-cart',
988 + 'hideIfEmpty' => true,
989 + ]
990 + );
991 +
992 + $elements_manager->add_category(
993 + 'king-addons-woo-builder',
994 + [
995 + 'title' => esc_html__('King Addons WooCommerce Builder', 'king-addons'),
996 + 'icon' => 'fa fa-shopping-cart',
997 + 'hideIfEmpty' => true,
998 + ]
999 + );
1000 + }
1001 +
683 1002 $elements_manager->add_category(
684 - 'king-addons-woo-builder',
1003 + 'king-addons-theme-builder',
685 1004 [
686 - 'title' => esc_html__('King Addons Woo Builder', 'king-addons'),
687 - 'icon' => 'fa fa-shopping-cart'
1005 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1006 + 'icon' => 'fa fa-plug',
1007 + 'hideIfEmpty' => true,
688 1008 ]
689 1009 );
1010 + }
690 1011
691 - // Move our categories to the top of the panel
1012 + /**
1013 + * Apply the panel category order after Elementor has finished registering its own.
1014 + *
1015 + * @return void
1016 + */
1017 + public function reorderWidgetCategoriesLate(): void
1018 + {
1019 + if (!did_action('elementor/loaded')) {
1020 + return;
1021 + }
1022 +
1023 + $elements_manager = Plugin::instance()->elements_manager;
1024 + $elements_manager->get_categories();
692 1025 $this->reorderWidgetCategories($elements_manager);
1026 +
1027 + if (!$this->shouldApplyEditorPanelVisibility()) {
1028 + return;
1029 + }
1030 +
1031 + $post_id = $this->getCurrentEditorPostId();
1032 + if ($post_id > 0) {
1033 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1034 + }
693 1035 }
694 1036
695 1037 /**
696 - * Reorder widget categories so King Addons categories appear after Layout and Basic.
1038 + * Overwrite the editor panel category snapshot after Elementor has built document config.
697 1039 *
1040 + * @param array $additional_config
1041 + * @param int $post_id
1042 + * @return array
1043 + */
1044 + public function filterDocumentPanelCategories($additional_config, $post_id = 0)
1045 + {
1046 + $post_id = (int) $post_id;
1047 + if ($post_id <= 0) {
1048 + $post_id = $this->getCurrentEditorPostId();
1049 + }
1050 + if (!is_array($additional_config)) {
1051 + $additional_config = [];
1052 + }
1053 +
1054 + if (!did_action('elementor/loaded')) {
1055 + return $additional_config;
1056 + }
1057 +
1058 + $elements_manager = Plugin::instance()->elements_manager;
1059 + $elements_manager->get_categories();
1060 + $this->reorderWidgetCategories($elements_manager);
1061 + if ($post_id > 0) {
1062 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1063 + }
1064 +
1065 + $categories = \Elementor\Core\Base\Document::get_filtered_editor_panel_categories();
1066 + foreach ($this->getHiddenPanelCategoryKeys($post_id) as $hidden_key) {
1067 + unset($categories[$hidden_key]);
1068 + }
1069 +
1070 + $additional_config['panel']['elements_categories'] = $categories;
1071 +
1072 + foreach ($this->getHiddenPanelWidgetNames($post_id) as $widget_name) {
1073 + $additional_config['widgets'][$widget_name]['show_in_panel'] = false;
1074 + }
1075 +
1076 + return $additional_config;
1077 + }
1078 +
1079 + /**
1080 + * Reorder widget categories: Layout, then King Addons groups, then the rest of Elementor.
1081 + *
698 1082 * @param \Elementor\Elements_Manager $elements_manager
699 1083 * @return void
700 1084 */
701 1085 private function reorderWidgetCategories($elements_manager): void
@@ -702,56 +1086,77 @@
702 1086 {
703 1087 try {
704 1088 $reflection = new \ReflectionClass($elements_manager);
705 1089 $categories_property = $reflection->getProperty('categories');
706 - $categories_property->setAccessible(true);
1090 + if (PHP_VERSION_ID < 80100) {
1091 + $categories_property->setAccessible(true);
1092 + }
707 1093
708 1094 $categories = $categories_property->getValue($elements_manager);
709 - if (!is_array($categories)) {
1095 + if (!is_array($categories) || $categories === []) {
710 1096 return;
711 1097 }
712 1098
713 - // Extract our categories
714 - $our_categories = [];
715 - if (isset($categories['king-addons'])) {
716 - $our_categories['king-addons'] = $categories['king-addons'];
717 - unset($categories['king-addons']);
1099 + $layout = [];
1100 + $ours = [];
1101 + $rest = [];
1102 +
1103 + foreach ($categories as $key => $value) {
1104 + if ('layout' === $key) {
1105 + $layout[$key] = $value;
1106 + } elseif (str_starts_with((string) $key, 'king-addons')) {
1107 + $ours[$key] = $value;
1108 + } else {
1109 + $rest[$key] = $value;
1110 + }
718 1111 }
719 - if (isset($categories['king-addons-woo-builder'])) {
720 - $our_categories['king-addons-woo-builder'] = $categories['king-addons-woo-builder'];
721 - unset($categories['king-addons-woo-builder']);
1112 +
1113 + $ours_sorted = [];
1114 + foreach (['king-addons', 'king-addons-woo', 'king-addons-woo-builder', 'king-addons-theme-builder'] as $preferred) {
1115 + if (isset($ours[$preferred])) {
1116 + $ours_sorted[$preferred] = $ours[$preferred];
1117 + unset($ours[$preferred]);
1118 + }
722 1119 }
1120 + $ours_sorted += $ours;
723 1121
724 - // Insert our categories after Layout and Basic
725 - $reordered = [];
726 - $insert_after = ['layout', 'basic']; // Categories after which we insert ours
727 - $inserted = false;
1122 + $categories_property->setValue($elements_manager, $layout + $ours_sorted + $rest);
1123 + } catch (\ReflectionException $e) {
1124 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1125 + }
1126 + }
728 1127
729 - foreach ($categories as $key => $value) {
730 - $reordered[$key] = $value;
731 -
732 - // Insert our categories after the last target category
733 - if (!$inserted && in_array($key, $insert_after, true)) {
734 - // Check if next category is also in our target list
735 - $keys = array_keys($categories);
736 - $current_index = array_search($key, $keys, true);
737 - $next_key = $keys[$current_index + 1] ?? null;
738 -
739 - // Only insert if the next category is NOT in our target list
740 - if ($next_key === null || !in_array($next_key, $insert_after, true)) {
741 - $reordered = array_merge($reordered, $our_categories);
742 - $inserted = true;
743 - }
744 - }
1128 + /**
1129 + * Drop context-only categories from the manager for the current editor document.
1130 + *
1131 + * @param \Elementor\Elements_Manager $elements_manager
1132 + * @param int $post_id
1133 + * @return void
1134 + */
1135 + private function applyContextCategoryVisibility($elements_manager, int $post_id): void
1136 + {
1137 + $hidden = $this->getHiddenPanelCategoryKeys($post_id);
1138 + if ($hidden === []) {
1139 + return;
1140 + }
1141 +
1142 + try {
1143 + $reflection = new \ReflectionClass($elements_manager);
1144 + $categories_property = $reflection->getProperty('categories');
1145 + if (PHP_VERSION_ID < 80100) {
1146 + $categories_property->setAccessible(true);
745 1147 }
746 1148
747 - // If target categories weren't found, append at the end
748 - if (!$inserted) {
749 - $reordered = array_merge($reordered, $our_categories);
1149 + $categories = $categories_property->getValue($elements_manager);
1150 + if (!is_array($categories) || $categories === []) {
1151 + return;
750 1152 }
751 1153
752 - // Set back the reordered array
753 - $categories_property->setValue($elements_manager, $reordered);
1154 + foreach ($hidden as $key) {
1155 + unset($categories[$key]);
1156 + }
1157 +
1158 + $categories_property->setValue($elements_manager, $categories);
754 1159 } catch (\ReflectionException $e) {
755 1160 // Silently fail if reflection doesn't work (e.g., future Elementor changes)
756 1161 }
757 1162 }
@@ -756,8 +1161,163 @@
756 1161 }
757 1162 }
758 1163
759 1164 /**
1165 + * Category keys that should not appear in the current editor document.
1166 + *
1167 + * @param int $post_id
1168 + * @return array<int,string>
1169 + */
1170 + private function getHiddenPanelCategoryKeys(int $post_id): array
1171 + {
1172 + $hidden = [];
1173 + $is_woo_builder = $this->isWooBuilderDocument($post_id);
1174 + $is_theme_builder = $this->isThemeBuilderDocument($post_id);
1175 + $is_loop_item = $this->isLoopItemDocument($post_id);
1176 + $woocommerce_active = class_exists('WooCommerce') && function_exists('WC');
1177 +
1178 + if (!$woocommerce_active) {
1179 + $hidden[] = 'king-addons-woo';
1180 + $hidden[] = 'king-addons-woo-builder';
1181 + } elseif (!$is_woo_builder && !$is_loop_item) {
1182 + $hidden[] = 'king-addons-woo-builder';
1183 + }
1184 +
1185 + if (!$is_theme_builder && !$is_loop_item) {
1186 + $hidden[] = 'king-addons-theme-builder';
1187 + }
1188 +
1189 + return $hidden;
1190 + }
1191 +
1192 + /**
1193 + * Widget names that belong to hidden context categories.
1194 + *
1195 + * @param int $post_id
1196 + * @return array<int,string>
1197 + */
1198 + private function getHiddenPanelWidgetNames(int $post_id): array
1199 + {
1200 + $hidden_categories = $this->getHiddenPanelCategoryKeys($post_id);
1201 + if ($hidden_categories === [] || !did_action('elementor/loaded')) {
1202 + return [];
1203 + }
1204 +
1205 + $hidden_lookup = array_fill_keys($hidden_categories, true);
1206 + $names = [];
1207 +
1208 + foreach (Plugin::instance()->widgets_manager->get_widget_types() as $widget_name => $widget) {
1209 + if (!is_object($widget) || !method_exists($widget, 'get_categories')) {
1210 + continue;
1211 + }
1212 +
1213 + foreach ((array) $widget->get_categories() as $category) {
1214 + if (isset($hidden_lookup[$category])) {
1215 + $names[] = (string) $widget_name;
1216 + break;
1217 + }
1218 + }
1219 + }
1220 +
1221 + return $names;
1222 + }
1223 +
1224 + /**
1225 + * Whether the current request is the Elementor editor (or its AJAX).
1226 + *
1227 + * @return bool
1228 + */
1229 + private function shouldApplyEditorPanelVisibility(): bool
1230 + {
1231 + if (!empty($_GET['action']) && 'elementor' === $_GET['action']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1232 + return true;
1233 + }
1234 +
1235 + if (!empty($_REQUEST['editor_post_id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1236 + return true;
1237 + }
1238 +
1239 + return class_exists('\Elementor\Plugin')
1240 + && Plugin::instance()->editor
1241 + && Plugin::instance()->editor->is_edit_mode();
1242 + }
1243 +
1244 + /**
1245 + * Current Elementor editor post ID, if any.
1246 + *
1247 + * @return int
1248 + */
1249 + private function getCurrentEditorPostId(): int
1250 + {
1251 + foreach (['editor_post_id', 'post_id', 'post'] as $key) {
1252 + if (!empty($_REQUEST[$key])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1253 + $post_id = absint(wp_unslash($_REQUEST[$key])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1254 + if ($post_id > 0) {
1255 + return $post_id;
1256 + }
1257 + }
1258 + }
1259 +
1260 + $post = get_post();
1261 + return $post ? (int) $post->ID : 0;
1262 + }
1263 +
1264 + /**
1265 + * Whether the post is a King Addons Woo Builder template.
1266 + *
1267 + * @param int $post_id
1268 + * @return bool
1269 + */
1270 + private function isWooBuilderDocument(int $post_id): bool
1271 + {
1272 + if ($post_id <= 0) {
1273 + return false;
1274 + }
1275 +
1276 + $elementor_type = (string) get_post_meta($post_id, '_elementor_template_type', true);
1277 + if ('king-addons-woo-builder' === $elementor_type) {
1278 + return true;
1279 + }
1280 +
1281 + return (string) get_post_meta($post_id, 'ka_woo_template_type', true) !== '';
1282 + }
1283 +
1284 + /**
1285 + * Whether the post is a King Addons Theme Builder template.
1286 + *
1287 + * @param int $post_id
1288 + * @return bool
1289 + */
1290 + private function isThemeBuilderDocument(int $post_id): bool
1291 + {
1292 + if ($post_id <= 0) {
1293 + return false;
1294 + }
1295 +
1296 + $location_key = '_ka_theme_builder_location';
1297 + if (class_exists('\King_Addons\Theme_Builder\Meta_Keys')) {
1298 + $location_key = \King_Addons\Theme_Builder\Meta_Keys::LOCATION;
1299 + }
1300 +
1301 + return (string) get_post_meta($post_id, $location_key, true) !== '';
1302 + }
1303 +
1304 + /**
1305 + * Whether the post is a King Addons Loop Item template.
1306 + *
1307 + * @param int $post_id
1308 + * @return bool
1309 + */
1310 + private function isLoopItemDocument(int $post_id): bool
1311 + {
1312 + if ($post_id <= 0) {
1313 + return false;
1314 + }
1315 +
1316 + return 'king-addons-loop-item' === (string) get_post_meta($post_id, '_elementor_template_type', true);
1317 + }
1318 +
1319 + /**
760 1320 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
761 1321 * that caused a fatal error previously, and try them again if the plugin version is updated.
762 1322 *
763 1323 * @param Widgets_Manager $widgets_manager
@@ -1045,8 +1605,40 @@
1045 1605 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);
1046 1606 }
1047 1607 }
1048 1608
1609 + /**
1610 + * Widget settings with dynamic tags resolved, and nothing missing.
1611 + *
1612 + * get_settings() never parses dynamic tags, so a tag placed in such a
1613 + * widget printed its placeholder instead of the value. get_settings_for_display()
1614 + * does parse them, but it also drops every setting whose control condition
1615 + * is currently false - and widgets written against the raw array index into
1616 + * those keys without checking, which turns the switch into a batch of
1617 + * "Trying to access array offset on null" warnings.
1618 + *
1619 + * A hidden control comes back as null rather than missing, so the parsed
1620 + * value is preferred only when there is one, and the raw value fills in
1621 + * everywhere else.
1622 + *
1623 + * @param \Elementor\Controls_Stack $widget Widget being rendered.
1624 + *
1625 + * @return array<string,mixed>
1626 + */
1627 + public static function displaySettings($widget): array
1628 + {
1629 + $raw = (array) $widget->get_settings();
1630 + $display = (array) $widget->get_settings_for_display();
1631 +
1632 + foreach ($display as $key => $value) {
1633 + if (null !== $value) {
1634 + $raw[$key] = $value;
1635 + }
1636 + }
1637 +
1638 + return $raw;
1639 + }
1640 +
1049 1641 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
1050 1642 {
1051 1643 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1052 1644 return;
@@ -1073,8 +1665,34 @@
1073 1665
1074 1666 $module->end_controls_section();
1075 1667 }
1076 1668
1669 + /**
1670 + * Same upgrade notice as renderUpgradeProNotice(), but not tied to another
1671 + * control's value. Use it where a whole section is Pro-only, so the free
1672 + * build does not show a section header that opens onto nothing.
1673 + *
1674 + * @param mixed $module Widget or repeater the control belongs to.
1675 + * @param string $controls_manager Control type to render the notice with.
1676 + * @param string $widget_name Widget slug, used for the campaign link.
1677 + * @param string $control_id Unique control id for the notice.
1678 + */
1679 + public static function renderUpgradeProSection($module, $controls_manager, string $widget_name, string $control_id): void
1680 + {
1681 + if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1682 + return;
1683 + }
1684 +
1685 + $module->add_control(
1686 + $control_id,
1687 + [
1688 + '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!',
1689 + 'type' => $controls_manager,
1690 + 'content_classes' => 'king-addons-pro-notice',
1691 + ]
1692 + );
1693 + }
1694 +
1077 1695 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
1078 1696 {
1079 1697 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1080 1698 return;
@@ -1437,9 +2055,14 @@
1437 2055 if (!$api_key) {
1438 2056 return $mailchimp_list;
1439 2057 }
1440 2058
1441 - $url = 'https://' . explode('-', $api_key)[1] . '.api.mailchimp.com/3.0/lists/';
2059 + $parts = explode('-', (string) $api_key);
2060 + if (count($parts) < 2 || '' === $parts[1]) {
2061 + return $mailchimp_list;
2062 + }
2063 +
2064 + $url = 'https://' . $parts[1] . '.api.mailchimp.com/3.0/lists/';
1442 2065 $response = wp_remote_get($url, [
1443 2066 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
1444 2067 ]);
1445 2068
@@ -1454,12 +2077,22 @@
1454 2077 }
1455 2078
1456 2079 public static function getMailchimpGroups()
1457 2080 {
1458 - $apiKey = get_option('king_addons_mailchimp_api_key');
1459 - $domain = 'https://' . substr($apiKey, strpos($apiKey, '-') + 1) . '.api.mailchimp.com/3.0/';
2081 + $apiKey = (string) get_option('king_addons_mailchimp_api_key', '');
2082 + $groups = ['def' => 'Select Group'];
2083 +
2084 + if ('' === $apiKey || false === strpos($apiKey, '-')) {
2085 + return $groups;
2086 + }
2087 +
2088 + $dc = substr($apiKey, strpos($apiKey, '-') + 1);
2089 + if ('' === $dc) {
2090 + return $groups;
2091 + }
2092 +
2093 + $domain = 'https://' . $dc . '.api.mailchimp.com/3.0/';
1460 2094 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
1461 - $groups = ['def' => 'Select Group'];
1462 2095 $mailchimpIDs = Core::getMailchimpLists();
1463 2096
1464 2097 foreach ($mailchimpIDs as $audience => $ignore) {
1465 2098 if ($audience === 'def') {
@@ -1465,14 +2098,20 @@
1465 2098 if ($audience === 'def') {
1466 2099 continue;
1467 2100 }
1468 2101
1469 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
1470 - $cats = json_decode($cats['body'])->categories ?? [];
2102 + $cats_res = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
2103 + if (is_wp_error($cats_res)) {
2104 + continue;
2105 + }
2106 + $cats = json_decode((string) wp_remote_retrieve_body($cats_res))->categories ?? [];
1471 2107
1472 2108 foreach ($cats as $cat) {
1473 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
1474 - $interests = json_decode($interests['body'])->interests ?? [];
2109 + $interests_res = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
2110 + if (is_wp_error($interests_res)) {
2111 + continue;
2112 + }
2113 + $interests = json_decode((string) wp_remote_retrieve_body($interests_res))->interests ?? [];
1475 2114
1476 2115 foreach ($interests as $int) {
1477 2116 $groups[$int->id] = $int->name;
1478 2117 }
@@ -1608,8 +2247,10 @@
1608 2247 * @return void
1609 2248 */
1610 2249 public function enqueueAiFieldScript(): void
1611 2250 {
2251 + $ai_options = get_option('king_addons_ai_options', []);
2252 +
1612 2253 wp_enqueue_script(
1613 2254 'king-addons-ai-field',
1614 2255 KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
1615 2256 ['jquery', 'elementor-editor'],
@@ -1633,8 +2274,49 @@
1633 2274 'plugin_url' => KING_ADDONS_URL,
1634 2275 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1635 2276 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1636 2277 'translator_enabled' => isset($ai_options['enable_ai_page_translator']) ? (bool) $ai_options['enable_ai_page_translator'] : true,
2278 + // Editor prompts name the configured provider rather than always OpenAI.
2279 + 'provider' => \King_Addons\AI_Provider::getProvider(),
2280 + 'provider_label' => \King_Addons\AI_Provider::getLabel(),
2281 + 'api_keys_url' => \King_Addons\AI_Provider::getApiKeysUrl(),
2282 + 'api_keys_label' => \King_Addons\AI_Provider::isOpenRouter()
2283 + ? esc_html__('OpenRouter Keys', 'king-addons')
2284 + : esc_html__('OpenAI Platform', 'king-addons'),
2285 + 'setup_billing_note' => \King_Addons\AI_Provider::isOpenRouter()
2286 + ? esc_html__('Free models work without adding any credit.', 'king-addons')
2287 + : esc_html__('and top up your OpenAI account balance by at least $5', 'king-addons'),
2288 + 'setup_cost_note' => \King_Addons\AI_Provider::isOpenRouter()
2289 + ? esc_html__('Free models cost nothing. Paid models cost pennies (about $0.01 per full page).', 'king-addons')
2290 + : esc_html__('Processing a page costs pennies (about $0.01 per full page).', 'king-addons'),
2291 + /**
2292 + * Before either provider has a key there is no "configured
2293 + * provider" to describe - the setting is just its default. The
2294 + * setup dialog then has to present the choice instead of sending
2295 + * everyone to OpenAI.
2296 + */
2297 + 'has_any_key' => (
2298 + '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENAI)
2299 + || '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENROUTER)
2300 + ),
2301 + 'setup_providers' => [
2302 + [
2303 + 'name' => esc_html__('OpenRouter', 'king-addons'),
2304 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENROUTER),
2305 + 'note' => esc_html__('has free models, no credit needed to start', 'king-addons'),
2306 + ],
2307 + [
2308 + 'name' => esc_html__('OpenAI', 'king-addons'),
2309 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENAI),
2310 + 'note' => esc_html__('needs at least $5 on your account balance', 'king-addons'),
2311 + ],
2312 + ],
2313 + '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'),
2314 + 'missing_key_message' => sprintf(
2315 + /* translators: %s: provider name */
2316 + esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
2317 + \King_Addons\AI_Provider::getLabel()
2318 + ),
1637 2319 ]
1638 2320 );
1639 2321 }
1640 2322
@@ -1644,10 +2326,8 @@
1644 2326 * @return void
1645 2327 */
1646 2328 public function enqueueAiImageGenerationScript(): void
1647 2329 {
1648 - // Retrieve AI options and ensure it's an array to prevent warnings.
1649 - $ai_options = get_option('king_addons_ai_options', []);
1650 2330 wp_enqueue_script(
1651 2331 'king-addons-ai-image-field',
1652 2332 KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
1653 2333 ['jquery', 'elementor-editor'],
@@ -1662,13 +2342,27 @@
1662 2342 [
1663 2343 'ajax_url' => admin_url('admin-ajax.php'),
1664 2344 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
1665 2345 'generate_action' => 'king_addons_ai_generate_image',
1666 - 'image_model' => sanitize_text_field($ai_options['openai_image_model'] ?? ''),
2346 + 'image_model' => \King_Addons\AI_Provider::getImageModel(),
2347 + // The editor builds its model dropdown from this list, so the
2348 + // options follow whichever provider is configured.
2349 + 'image_models' => array_map(
2350 + static function ($model) {
2351 + return ['value' => $model['id'], 'label' => $model['label']];
2352 + },
2353 + \King_Addons\AI_Provider::getModelsFor('image')
2354 + ),
2355 + 'provider' => \King_Addons\AI_Provider::getProvider(),
1667 2356 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1668 2357 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1669 2358 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1670 2359 'plugin_url' => KING_ADDONS_URL,
2360 + 'missing_key_message' => sprintf(
2361 + /* translators: %s: provider name */
2362 + esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
2363 + \King_Addons\AI_Provider::getLabel()
2364 + ),
1671 2365 ]
1672 2366 );
1673 2367 }
1674 2368
@@ -1728,8 +2422,87 @@
1728 2422
1729 2423 // Note: Using existing KingAddonsAiField localization
1730 2424 // The translator script will use the same AJAX endpoints and settings
1731 2425 // No need for separate localization as it reuses existing AI infrastructure
2426 + }
2427 +
2428 + /**
2429 + * Render an attachment through an Elementor image-size group control.
2430 + *
2431 + * Group_Control_Image_Size::get_attachment_image_html() takes a *key into
2432 + * $settings* as its third argument, not an attachment ID. Handing it an ID
2433 + * makes Elementor read $settings[<id>], which raises two PHP warnings and
2434 + * returns nothing, so callers silently lose the image.
2435 + *
2436 + * @param array<string, mixed> $settings Widget settings; carries "<$size_key>_size" and friends.
2437 + * @param string $size_key Name of the image-size group control.
2438 + * @param int $attachment_id Attachment to render.
2439 + *
2440 + * @return string Image HTML, or an empty string when it cannot be rendered.
2441 + */
2442 + /**
2443 + * Show the person building the page why a widget rendered nothing.
2444 + *
2445 + * Only ever printed inside the Elementor editor, so the public page keeps
2446 + * rendering exactly what it rendered before.
2447 + *
2448 + * @param string $message Plain text explaining what the widget still needs.
2449 + */
2450 + public static function renderEditorHint(string $message): void
2451 + {
2452 + if (!Plugin::$instance->editor->is_edit_mode()) {
2453 + return;
2454 + }
2455 +
2456 + $style = 'display:block;padding:12px 14px;border:1px dashed #c3c4c7;border-radius:4px;'
2457 + . 'background:#f6f7f7;color:#50575e;font-size:13px;line-height:1.5;';
2458 +
2459 + echo '<div class="king-addons-editor-hint" style="' . esc_attr($style) . '">'
2460 + . esc_html($message) . '</div>';
2461 + }
2462 +
2463 + /**
2464 + * A setting written straight into a JavaScript object literal has to be a
2465 + * number. Elementor stores a number field the user cleared as an empty
2466 + * string, which would emit `slidesPerView: ,` - a syntax error that stops
2467 + * the whole carousel from initialising - or blow up in PHP when the value
2468 + * is used in arithmetic.
2469 + *
2470 + * @param array<string, mixed> $settings Widget settings.
2471 + * @param string $key Setting to read.
2472 + * @param int|float $fallback Value to use when the setting is empty or not a number.
2473 + *
2474 + * @return string Numeric string, safe to interpolate into JS.
2475 + */
2476 + public static function jsNumber(array $settings, string $key, $fallback = 0): string
2477 + {
2478 + $value = $settings[$key] ?? null;
2479 +
2480 + // Slider controls keep their number under 'size'.
2481 + if (is_array($value)) {
2482 + $value = $value['size'] ?? null;
2483 + }
2484 +
2485 + if (!is_numeric($value)) {
2486 + $value = $fallback;
2487 + }
2488 +
2489 + return (string) (0 + $value);
2490 + }
2491 +
2492 + public static function getAttachmentImageHTML(array $settings, string $size_key, int $attachment_id): string
2493 + {
2494 + if ($attachment_id < 1) {
2495 + return '';
2496 + }
2497 +
2498 + $image_key = '__king_addons_image';
2499 + $settings[$image_key] = [
2500 + 'id' => $attachment_id,
2501 + 'url' => wp_get_attachment_image_url($attachment_id, 'full') ?: '',
2502 + ];
2503 +
2504 + return (string) Group_Control_Image_Size::get_attachment_image_html($settings, $size_key, $image_key);
1732 2505 }
1733 2506 }
1734 2507
1735 2508 Core::instance();