PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.83
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 51.1.46 All 39 releases
← All changes | includes/Core.php +828 -54 51.1.7651.1.83 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');
@@ -360,8 +409,13 @@
360 409
361 410 add_action('elementor/init', [$this, 'initElementor']);
362 411
363 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);
364 418 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
365 419
366 420 self::enableFeatures();
367 421
@@ -400,9 +454,23 @@
400 454
401 455 // Notice - Upgrade Suggestion
402 456 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
403 457 add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
404 - 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 + );
405 473 }
406 474
407 475 // Dashboard UI settings AJAX handler
408 476 add_action('wp_ajax_king_addons_save_dashboard_ui', [$this, 'king_addons_save_dashboard_ui_callback']);
@@ -575,8 +643,241 @@
575 643 </script>
576 644 <?php
577 645 }
578 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 +
579 880 function enqueueFrontendStyles()
580 881 {
581 882 /**
582 883 * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
@@ -669,32 +970,116 @@
669 970 function addWidgetCategory(): void
670 971 {
671 972 $elements_manager = Plugin::instance()->elements_manager;
672 973
673 - // Add our categories
674 974 $elements_manager->add_category(
675 975 'king-addons',
676 976 [
677 977 'title' => esc_html__('King Addons', 'king-addons'),
678 - 'icon' => 'fa fa-plug'
978 + 'icon' => 'fa fa-plug',
679 979 ]
680 980 );
681 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 +
682 1002 $elements_manager->add_category(
683 - 'king-addons-woo-builder',
1003 + 'king-addons-theme-builder',
684 1004 [
685 - 'title' => esc_html__('King Addons Woo Builder', 'king-addons'),
686 - 'icon' => 'fa fa-shopping-cart'
1005 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1006 + 'icon' => 'fa fa-plug',
1007 + 'hideIfEmpty' => true,
687 1008 ]
688 1009 );
1010 + }
689 1011
690 - // 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();
691 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 + }
692 1035 }
693 1036
694 1037 /**
695 - * 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.
696 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 + *
697 1082 * @param \Elementor\Elements_Manager $elements_manager
698 1083 * @return void
699 1084 */
700 1085 private function reorderWidgetCategories($elements_manager): void
@@ -701,56 +1086,77 @@
701 1086 {
702 1087 try {
703 1088 $reflection = new \ReflectionClass($elements_manager);
704 1089 $categories_property = $reflection->getProperty('categories');
705 - $categories_property->setAccessible(true);
1090 + if (PHP_VERSION_ID < 80100) {
1091 + $categories_property->setAccessible(true);
1092 + }
706 1093
707 1094 $categories = $categories_property->getValue($elements_manager);
708 - if (!is_array($categories)) {
1095 + if (!is_array($categories) || $categories === []) {
709 1096 return;
710 1097 }
711 1098
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']);
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 + }
717 1111 }
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']);
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 + }
721 1119 }
1120 + $ours_sorted += $ours;
722 1121
723 - // Insert our categories after Layout and Basic
724 - $reordered = [];
725 - $insert_after = ['layout', 'basic']; // Categories after which we insert ours
726 - $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 + }
727 1127
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 - }
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);
744 1147 }
745 1148
746 - // If target categories weren't found, append at the end
747 - if (!$inserted) {
748 - $reordered = array_merge($reordered, $our_categories);
1149 + $categories = $categories_property->getValue($elements_manager);
1150 + if (!is_array($categories) || $categories === []) {
1151 + return;
749 1152 }
750 1153
751 - // Set back the reordered array
752 - $categories_property->setValue($elements_manager, $reordered);
1154 + foreach ($hidden as $key) {
1155 + unset($categories[$key]);
1156 + }
1157 +
1158 + $categories_property->setValue($elements_manager, $categories);
753 1159 } catch (\ReflectionException $e) {
754 1160 // Silently fail if reflection doesn't work (e.g., future Elementor changes)
755 1161 }
756 1162 }
@@ -755,8 +1161,163 @@
755 1161 }
756 1162 }
757 1163
758 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 + /**
759 1320 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
760 1321 * that caused a fatal error previously, and try them again if the plugin version is updated.
761 1322 *
762 1323 * @param Widgets_Manager $widgets_manager
@@ -1044,8 +1605,40 @@
1044 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);
1045 1606 }
1046 1607 }
1047 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 +
1048 1641 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
1049 1642 {
1050 1643 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1051 1644 return;
@@ -1072,8 +1665,34 @@
1072 1665
1073 1666 $module->end_controls_section();
1074 1667 }
1075 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 +
1076 1695 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
1077 1696 {
1078 1697 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1079 1698 return;
@@ -1436,9 +2055,14 @@
1436 2055 if (!$api_key) {
1437 2056 return $mailchimp_list;
1438 2057 }
1439 2058
1440 - $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/';
1441 2065 $response = wp_remote_get($url, [
1442 2066 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
1443 2067 ]);
1444 2068
@@ -1453,12 +2077,22 @@
1453 2077 }
1454 2078
1455 2079 public static function getMailchimpGroups()
1456 2080 {
1457 - $apiKey = get_option('king_addons_mailchimp_api_key');
1458 - $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/';
1459 2094 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
1460 - $groups = ['def' => 'Select Group'];
1461 2095 $mailchimpIDs = Core::getMailchimpLists();
1462 2096
1463 2097 foreach ($mailchimpIDs as $audience => $ignore) {
1464 2098 if ($audience === 'def') {
@@ -1464,14 +2098,20 @@
1464 2098 if ($audience === 'def') {
1465 2099 continue;
1466 2100 }
1467 2101
1468 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
1469 - $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 ?? [];
1470 2107
1471 2108 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 ?? [];
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 ?? [];
1474 2114
1475 2115 foreach ($interests as $int) {
1476 2116 $groups[$int->id] = $int->name;
1477 2117 }
@@ -1607,8 +2247,10 @@
1607 2247 * @return void
1608 2248 */
1609 2249 public function enqueueAiFieldScript(): void
1610 2250 {
2251 + $ai_options = get_option('king_addons_ai_options', []);
2252 +
1611 2253 wp_enqueue_script(
1612 2254 'king-addons-ai-field',
1613 2255 KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
1614 2256 ['jquery', 'elementor-editor'],
@@ -1632,8 +2274,49 @@
1632 2274 'plugin_url' => KING_ADDONS_URL,
1633 2275 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1634 2276 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1635 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 + ),
1636 2319 ]
1637 2320 );
1638 2321 }
1639 2322
@@ -1643,10 +2326,8 @@
1643 2326 * @return void
1644 2327 */
1645 2328 public function enqueueAiImageGenerationScript(): void
1646 2329 {
1647 - // Retrieve AI options and ensure it's an array to prevent warnings.
1648 - $ai_options = get_option('king_addons_ai_options', []);
1649 2330 wp_enqueue_script(
1650 2331 'king-addons-ai-image-field',
1651 2332 KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
1652 2333 ['jquery', 'elementor-editor'],
@@ -1661,13 +2342,27 @@
1661 2342 [
1662 2343 'ajax_url' => admin_url('admin-ajax.php'),
1663 2344 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
1664 2345 'generate_action' => 'king_addons_ai_generate_image',
1665 - '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(),
1666 2356 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1667 2357 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1668 2358 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1669 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 + ),
1670 2365 ]
1671 2366 );
1672 2367 }
1673 2368
@@ -1727,8 +2422,87 @@
1727 2422
1728 2423 // Note: Using existing KingAddonsAiField localization
1729 2424 // The translator script will use the same AJAX endpoints and settings
1730 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);
1731 2505 }
1732 2506 }
1733 2507
1734 2508 Core::instance();