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 +669 -61 51.1.79 → 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,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');
@@ -342,8 +390,17 @@
342 390 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Frontend.php';
343 391 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_WooCommerce.php';
344 392 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Module.php';
345 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 + });
346 403 }
347 404
348 405 // Dynamic Posts Grid AJAX Helper - Initialize regardless of Elementor compatibility
349 406 // This is needed for AJAX functionality to work even when PRO version is disabled
@@ -361,8 +418,13 @@
361 418
362 419 add_action('elementor/init', [$this, 'initElementor']);
363 420
364 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);
365 427 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
366 428
367 429 self::enableFeatures();
368 430
@@ -537,12 +599,12 @@
537 599 style="border-left: 4px solid #0071e3;padding: 10px 15px;">
538 600 <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
539 601 <span>
540 602 Get <strong style="font-weight: 700;">4,000+</strong> premium templates and sections,
541 - <strong style="font-weight: 700;">80+</strong> widgets,
603 + <strong style="font-weight: 700;">100+</strong> widgets,
542 604 <strong style="font-weight: 700;">200+</strong> advanced features,
543 605 and AI tools for Elementor.
544 - From $<strong style="font-weight: 700;">4</strong>/mo, billed annually.
606 + From $<strong style="font-weight: 700;">6.99</strong>/mo.
545 607 </span>
546 608 </p>
547 609 <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
548 610 <p style="display: flex;">
@@ -765,9 +827,9 @@
765 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>
766 828 <?php esc_html_e('King Addons Pro', 'king-addons'); ?>
767 829 </div>
768 830 <h2 class="king-addons-un-headline">
769 - <?php esc_html_e('Mega Menu, Popup Builder, Theme Builder &amp; AI Tools', 'king-addons'); ?>
831 + <?php esc_html_e('Mega Menu, Popup Builder, WooCommerce Builder, Theme Builder &amp; AI Tools', 'king-addons'); ?>
770 832 </h2>
771 833 <p class="king-addons-un-sub">
772 834 <?php esc_html_e('200+ Pro features, premium widgets and 4,000+ templates &amp; sections for Elementor.', 'king-addons'); ?>
773 835 </p>
@@ -917,32 +979,116 @@
917 979 function addWidgetCategory(): void
918 980 {
919 981 $elements_manager = Plugin::instance()->elements_manager;
920 982
921 - // Add our categories
922 983 $elements_manager->add_category(
923 984 'king-addons',
924 985 [
925 986 'title' => esc_html__('King Addons', 'king-addons'),
926 - 'icon' => 'fa fa-plug'
987 + 'icon' => 'fa fa-plug',
927 988 ]
928 989 );
929 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 +
930 1011 $elements_manager->add_category(
931 - 'king-addons-woo-builder',
1012 + 'king-addons-theme-builder',
932 1013 [
933 - 'title' => esc_html__('King Addons Woo Builder', 'king-addons'),
934 - 'icon' => 'fa fa-shopping-cart'
1014 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1015 + 'icon' => 'fa fa-plug',
1016 + 'hideIfEmpty' => true,
935 1017 ]
936 1018 );
1019 + }
937 1020
938 - // 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();
939 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 + }
940 1044 }
941 1045
942 1046 /**
943 - * 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.
944 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 + *
945 1091 * @param \Elementor\Elements_Manager $elements_manager
946 1092 * @return void
947 1093 */
948 1094 private function reorderWidgetCategories($elements_manager): void
@@ -949,56 +1095,77 @@
949 1095 {
950 1096 try {
951 1097 $reflection = new \ReflectionClass($elements_manager);
952 1098 $categories_property = $reflection->getProperty('categories');
953 - $categories_property->setAccessible(true);
1099 + if (PHP_VERSION_ID < 80100) {
1100 + $categories_property->setAccessible(true);
1101 + }
954 1102
955 1103 $categories = $categories_property->getValue($elements_manager);
956 - if (!is_array($categories)) {
1104 + if (!is_array($categories) || $categories === []) {
957 1105 return;
958 1106 }
959 1107
960 - // Extract our categories
961 - $our_categories = [];
962 - if (isset($categories['king-addons'])) {
963 - $our_categories['king-addons'] = $categories['king-addons'];
964 - 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 + }
965 1120 }
966 - if (isset($categories['king-addons-woo-builder'])) {
967 - $our_categories['king-addons-woo-builder'] = $categories['king-addons-woo-builder'];
968 - 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 + }
969 1128 }
1129 + $ours_sorted += $ours;
970 1130
971 - // Insert our categories after Layout and Basic
972 - $reordered = [];
973 - $insert_after = ['layout', 'basic']; // Categories after which we insert ours
974 - $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 + }
975 1136
976 - foreach ($categories as $key => $value) {
977 - $reordered[$key] = $value;
978 -
979 - // Insert our categories after the last target category
980 - if (!$inserted && in_array($key, $insert_after, true)) {
981 - // Check if next category is also in our target list
982 - $keys = array_keys($categories);
983 - $current_index = array_search($key, $keys, true);
984 - $next_key = $keys[$current_index + 1] ?? null;
985 -
986 - // Only insert if the next category is NOT in our target list
987 - if ($next_key === null || !in_array($next_key, $insert_after, true)) {
988 - $reordered = array_merge($reordered, $our_categories);
989 - $inserted = true;
990 - }
991 - }
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);
992 1156 }
993 1157
994 - // If target categories weren't found, append at the end
995 - if (!$inserted) {
996 - $reordered = array_merge($reordered, $our_categories);
1158 + $categories = $categories_property->getValue($elements_manager);
1159 + if (!is_array($categories) || $categories === []) {
1160 + return;
997 1161 }
998 1162
999 - // Set back the reordered array
1000 - $categories_property->setValue($elements_manager, $reordered);
1163 + foreach ($hidden as $key) {
1164 + unset($categories[$key]);
1165 + }
1166 +
1167 + $categories_property->setValue($elements_manager, $categories);
1001 1168 } catch (\ReflectionException $e) {
1002 1169 // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1003 1170 }
1004 1171 }
@@ -1003,8 +1170,163 @@
1003 1170 }
1004 1171 }
1005 1172
1006 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 + /**
1007 1329 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
1008 1330 * that caused a fatal error previously, and try them again if the plugin version is updated.
1009 1331 *
1010 1332 * @param Widgets_Manager $widgets_manager
@@ -1292,8 +1614,40 @@
1292 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);
1293 1615 }
1294 1616 }
1295 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 +
1296 1650 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
1297 1651 {
1298 1652 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1299 1653 return;
@@ -1320,8 +1674,34 @@
1320 1674
1321 1675 $module->end_controls_section();
1322 1676 }
1323 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 +
1324 1704 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
1325 1705 {
1326 1706 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1327 1707 return;
@@ -1375,16 +1755,19 @@
1375 1755 $image = esc_url($args['image']);
1376 1756 $network = $args['network'];
1377 1757
1378 1758 $get_whatsapp_url = function ($a) {
1379 - if ('yes' === $a['show_whatsapp_title'] && 'yes' === $a['show_whatsapp_excerpt']) {
1380 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['text'] . '%0a' . $a['url'];
1381 - } elseif ('yes' === $a['show_whatsapp_title']) {
1382 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['url'];
1383 - } elseif ('yes' === $a['show_whatsapp_excerpt']) {
1384 - 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'] . '*';
1385 1762 }
1386 - 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));
1387 1770 };
1388 1771
1389 1772 $networks_map = [
1390 1773 'facebook-f' => [
@@ -1476,10 +1859,17 @@
1476 1859 $share_url = $networks_map[$network]['url'];
1477 1860 $network_title = $networks_map[$network]['title'];
1478 1861 $icon_category = $networks_map[$network]['icon'];
1479 1862
1480 - $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 + }
1481 1869
1870 + $output = '<a href="' . $href . '" class="king-addons-share-icon king-addons-share-' . esc_attr($network) . '" target="_blank">';
1871 +
1482 1872 if ('yes' === $args['tooltip']) {
1483 1873 $output .= '<span class="king-addons-share-tooltip king-addons-tooltip">' . esc_html($network_title) . '</span>';
1484 1874 }
1485 1875
@@ -1684,9 +2074,14 @@
1684 2074 if (!$api_key) {
1685 2075 return $mailchimp_list;
1686 2076 }
1687 2077
1688 - $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/';
1689 2084 $response = wp_remote_get($url, [
1690 2085 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
1691 2086 ]);
1692 2087
@@ -1701,12 +2096,22 @@
1701 2096 }
1702 2097
1703 2098 public static function getMailchimpGroups()
1704 2099 {
1705 - $apiKey = get_option('king_addons_mailchimp_api_key');
1706 - $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/';
1707 2113 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
1708 - $groups = ['def' => 'Select Group'];
1709 2114 $mailchimpIDs = Core::getMailchimpLists();
1710 2115
1711 2116 foreach ($mailchimpIDs as $audience => $ignore) {
1712 2117 if ($audience === 'def') {
@@ -1712,14 +2117,20 @@
1712 2117 if ($audience === 'def') {
1713 2118 continue;
1714 2119 }
1715 2120
1716 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
1717 - $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 ?? [];
1718 2126
1719 2127 foreach ($cats as $cat) {
1720 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
1721 - $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 ?? [];
1722 2133
1723 2134 foreach ($interests as $int) {
1724 2135 $groups[$int->id] = $int->name;
1725 2136 }
@@ -1895,8 +2306,31 @@
1895 2306 : esc_html__('and top up your OpenAI account balance by at least $5', 'king-addons'),
1896 2307 'setup_cost_note' => \King_Addons\AI_Provider::isOpenRouter()
1897 2308 ? esc_html__('Free models cost nothing. Paid models cost pennies (about $0.01 per full page).', 'king-addons')
1898 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'),
1899 2333 'missing_key_message' => sprintf(
1900 2334 /* translators: %s: provider name */
1901 2335 esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
1902 2336 \King_Addons\AI_Provider::getLabel()
@@ -2007,8 +2441,182 @@
2007 2441
2008 2442 // Note: Using existing KingAddonsAiField localization
2009 2443 // The translator script will use the same AJAX endpoints and settings
2010 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);
2011 2619 }
2012 2620 }
2013 2621
2014 2622 Core::instance();