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 +1346 -68 51.1.3551.1.83 View file →
@@ -8,8 +8,10 @@
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;
13 +use King_Addons\Wishlist\Wishlist_Module;
12 14
13 15 if (!defined('ABSPATH')) {
14 16 exit; // Exit if accessed directly.
15 17 }
@@ -23,8 +25,15 @@
23 25 */
24 26 private static ?Core $_instance = null;
25 27
26 28 /**
29 + * Wishlist module instance.
30 + *
31 + * @var Wishlist_Module|null
32 + */
33 + private ?Wishlist_Module $wishlist_module = null;
34 +
35 + /**
27 36 * Instance
28 37 *
29 38 * Ensures only one instance of the class is loaded or can be loaded.
30 39 *
@@ -39,8 +48,53 @@
39 48 return self::$_instance;
40 49 }
41 50
42 51 /**
52 + * Check if an extension is enabled.
53 + *
54 + * Checks database options first, defaults to enabled if not set (new installations).
55 + * Falls back to constant check for backward compatibility.
56 + *
57 + * @param string $extension_id The extension ID (e.g., 'templates-catalog', 'popup-builder').
58 + * @param string $constant_name The constant name to check as fallback (e.g., 'KING_ADDONS_EXT_POPUP_BUILDER').
59 + * @return bool True if extension is enabled, false otherwise.
60 + * @since 1.0.0
61 + */
62 + private function isExtensionEnabled(string $extension_id, string $constant_name): bool
63 + {
64 + // Dependency checks (extensions that require other plugins).
65 + if ($extension_id === 'woo-builder' && (!class_exists('WooCommerce') || !function_exists('WC'))) {
66 + return false;
67 + }
68 +
69 + // Get options from database
70 + $options = get_option('king_addons_options', []);
71 +
72 + // If a constant is defined and explicitly false, treat it as a hard disable.
73 + // This is useful for extensions that are in development and should not be
74 + // available/visible even if the database option is enabled.
75 + if ($constant_name !== '' && defined($constant_name) && constant($constant_name) === false) {
76 + return false;
77 + }
78 +
79 + // Check if option exists in database
80 + $option_key = 'ext_' . $extension_id;
81 + if (isset($options[$option_key])) {
82 + // Option exists, use its value
83 + return $options[$option_key] === 'enabled';
84 + }
85 +
86 + // Option doesn't exist (new installation), default to enabled
87 + // But also check constant as fallback for backward compatibility
88 + if ($constant_name !== '' && defined($constant_name)) {
89 + return constant($constant_name);
90 + }
91 +
92 + // Default to enabled for new installations
93 + return true;
94 + }
95 +
96 + /**
43 97 * Constructor
44 98 *
45 99 * Perform some compatibility checks to make sure basic requirements are meet.
46 100 * If all compatibility checks pass, initialize the functionality.
@@ -50,9 +104,10 @@
50 104 public function __construct()
51 105 {
52 106 require_once(KING_ADDONS_PATH . 'includes/ModulesMap.php');
53 107 require_once(KING_ADDONS_PATH . 'includes/LibrariesMap.php');
54 - require_once(KING_ADDONS_PATH . 'includes/SectionsMap.php');
108 + require_once(KING_ADDONS_PATH . 'includes/helpers/Global/Text_Entities_Migration.php');
109 + Text_Entities_Migration::boot();
55 110
56 111 if ($this->hasElementorCompatibility()) {
57 112
58 113 // Initial requirements check
@@ -58,9 +113,9 @@
58 113 // Initial requirements check
59 114 require_once(KING_ADDONS_PATH . 'includes/helpers/Check_Requirements/Check_Requirements.php');
60 115
61 116 // Templates Catalog
62 - if (KING_ADDONS_EXT_TEMPLATES_CATALOG) {
117 + if ($this->isExtensionEnabled('templates-catalog', 'KING_ADDONS_EXT_TEMPLATES_CATALOG')) {
63 118 require_once(KING_ADDONS_PATH . 'includes/TemplatesMap.php');
64 119 require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/CollectionsMap.php');
65 120 require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/Templates.php');
66 121
@@ -68,29 +123,214 @@
68 123 require_once(KING_ADDONS_PATH . 'includes/extensions/Template_Catalog_Button/Template_Catalog_Button.php');
69 124 Template_Catalog_Button::instance();
70 125 }
71 126
72 - // Sections Catalog
73 - // if (KING_ADDONS_EXT_SECTIONS_CATALOG) {
74 - // require_once(KING_ADDONS_PATH . 'includes/extensions/Sections_Catalog/Sections_Catalog.php');
75 - // Sections_Catalog::instance();
76 - // }
77 -
78 127 // Header & Footer Builder
79 - if (KING_ADDONS_EXT_HEADER_FOOTER_BUILDER) {
128 + if ($this->isExtensionEnabled('header-footer-builder', 'KING_ADDONS_EXT_HEADER_FOOTER_BUILDER')) {
80 129 require_once(KING_ADDONS_PATH . 'includes/extensions/Header_Footer_Builder/Header_Footer_Builder.php');
81 130 Header_Footer_Builder::instance();
82 131 }
83 132
84 133 // Popup Builder
85 - if (KING_ADDONS_EXT_POPUP_BUILDER) {
134 + if ($this->isExtensionEnabled('popup-builder', 'KING_ADDONS_EXT_POPUP_BUILDER')) {
86 135 require_once(KING_ADDONS_PATH . 'includes/extensions/Popup_Builder/Popup_Builder.php');
87 136 Popup_Builder::instance();
88 137 }
89 138
139 + // Cookie / Consent Bar
140 + if ($this->isExtensionEnabled('cookie-consent', 'KING_ADDONS_EXT_COOKIE_CONSENT')) {
141 + require_once(KING_ADDONS_PATH . 'includes/extensions/Cookie_Consent/Cookie_Consent.php');
142 + Cookie_Consent::instance();
143 + }
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 +
163 + // WooCommerce Builder
164 + if ($this->isExtensionEnabled('woo-builder', 'KING_ADDONS_EXT_WOO_BUILDER')) {
165 + require_once(KING_ADDONS_PATH . 'includes/extensions/Woo_Builder/Woo_Builder.php');
166 + if (class_exists('King_Addons\\Woo_Builder')) {
167 + new Woo_Builder();
168 + }
169 + }
170 +
171 + // Sticky Contact Bar
172 + if ($this->isExtensionEnabled('sticky-contact-bar', 'KING_ADDONS_EXT_STICKY_CONTACT_BAR')) {
173 + require_once(KING_ADDONS_PATH . 'includes/extensions/Sticky_Contact_Bar/Sticky_Contact_Bar.php');
174 +
175 + $pro_loaded = false;
176 + if (
177 + function_exists('king_addons_freemius')
178 + && king_addons_freemius()->can_use_premium_code__premium_only()
179 + && defined('KING_ADDONS_PRO_PATH')
180 + ) {
181 + $pro_file_path = KING_ADDONS_PRO_PATH . 'includes/extensions/Sticky_Contact_Bar_Pro/Sticky_Contact_Bar_Pro.php';
182 + if (file_exists($pro_file_path)) {
183 + require_once $pro_file_path;
184 + if (class_exists('King_Addons\\Sticky_Contact_Bar_Pro')) {
185 + new Sticky_Contact_Bar_Pro();
186 + $pro_loaded = true;
187 + }
188 + }
189 + }
190 +
191 + if (!$pro_loaded && class_exists('King_Addons\\Sticky_Contact_Bar')) {
192 + new Sticky_Contact_Bar();
193 + }
194 + }
195 +
196 + // Theme Builder
197 + if ($this->isExtensionEnabled('theme-builder', 'KING_ADDONS_EXT_THEME_BUILDER')) {
198 + require_once(KING_ADDONS_PATH . 'includes/extensions/Theme_Builder/Theme_Builder.php');
199 +
200 + $pro_loaded = false;
201 + if (
202 + function_exists('king_addons_freemius')
203 + && king_addons_freemius()->can_use_premium_code__premium_only()
204 + && defined('KING_ADDONS_PRO_PATH')
205 + ) {
206 + $pro_file_path = KING_ADDONS_PRO_PATH . 'includes/extensions/Theme_Builder_Pro/Theme_Builder_Pro.php';
207 + if (file_exists($pro_file_path)) {
208 + require_once $pro_file_path;
209 + if (class_exists('King_Addons\\Theme_Builder_Pro')) {
210 + new Theme_Builder_Pro();
211 + $pro_loaded = true;
212 + }
213 + }
214 + }
215 +
216 + if (!$pro_loaded && class_exists('King_Addons\\Theme_Builder')) {
217 + new Theme_Builder();
218 + }
219 + }
220 +
221 + // Custom Cursor
222 + if ($this->isExtensionEnabled('custom-cursor', 'KING_ADDONS_EXT_CUSTOM_CURSOR')) {
223 + require_once(KING_ADDONS_PATH . 'includes/extensions/Custom_Cursor/Custom_Cursor.php');
224 + if (class_exists('King_Addons\\Custom_Cursor')) {
225 + new Custom_Cursor();
226 + }
227 + }
228 +
229 + // Age Gate
230 + if ($this->isExtensionEnabled('age-gate', 'KING_ADDONS_EXT_AGE_GATE')) {
231 + require_once(KING_ADDONS_PATH . 'includes/extensions/Age_Gate/Age_Gate.php');
232 + if (defined('KING_ADDONS_PRO_PATH')) {
233 + $age_gate_pro = KING_ADDONS_PRO_PATH . 'includes/extensions/Age_Gate_Pro/Age_Gate_Pro.php';
234 + if (file_exists($age_gate_pro)) {
235 + require_once $age_gate_pro;
236 + }
237 + }
238 + Age_Gate::instance();
239 + }
240 +
241 + // Live Chat & Support Builder
242 + if ($this->isExtensionEnabled('live-chat', 'KING_ADDONS_EXT_LIVE_CHAT')) {
243 + require_once(KING_ADDONS_PATH . 'includes/extensions/Live_Chat/Live_Chat.php');
244 + Live_Chat::instance();
245 + }
246 +
247 + // Docs & Knowledge Base
248 + if ($this->isExtensionEnabled('docs-kb', 'KING_ADDONS_EXT_DOCS_KB')) {
249 + require_once(KING_ADDONS_PATH . 'includes/extensions/Docs_KB/Docs_KB.php');
250 + Docs_KB::instance();
251 + }
252 +
253 + // Pricing Table Builder
254 + if ($this->isExtensionEnabled('pricing-table-builder', 'KING_ADDONS_EXT_PRICING_TABLE_BUILDER')) {
255 + require_once(KING_ADDONS_PATH . 'includes/extensions/Pricing_Table_Builder/Pricing_Table_Builder.php');
256 + Pricing_Table_Builder::instance();
257 + }
258 +
259 + // Custom Code Manager
260 + if ($this->isExtensionEnabled('custom-code-manager', 'KING_ADDONS_EXT_CUSTOM_CODE_MANAGER')) {
261 + require_once(KING_ADDONS_PATH . 'includes/extensions/Custom_Code_Manager/Custom_Code_Manager.php');
262 + Custom_Code_Manager::getInstance();
263 + }
264 +
265 + // Fomo Notifications
266 + if ($this->isExtensionEnabled('fomo-notifications', 'KING_ADDONS_EXT_FOMO_NOTIFICATIONS')) {
267 + require_once(KING_ADDONS_PATH . 'includes/extensions/Fomo_Notifications/Fomo_Notifications.php');
268 + Fomo_Notifications::instance();
269 + }
270 +
271 + // Smart Links
272 + if ($this->isExtensionEnabled('smart-links', 'KING_ADDONS_EXT_SMART_LINKS')) {
273 + require_once(KING_ADDONS_PATH . 'includes/extensions/Smart_Links/Smart_Links.php');
274 + \King_Addons\Smart_Links\Smart_Links::instance();
275 + }
276 +
277 + // Activity Log
278 + if ($this->isExtensionEnabled('activity-log', 'KING_ADDONS_EXT_ACTIVITY_LOG')) {
279 + require_once(KING_ADDONS_PATH . 'includes/extensions/Activity_Log/Activity_Log.php');
280 + \King_Addons\Activity_Log\Activity_Log::instance();
281 + }
282 +
283 + // Maintenance Mode
284 + if ($this->isExtensionEnabled('maintenance-mode', 'KING_ADDONS_EXT_MAINTENANCE_MODE')) {
285 + require_once(KING_ADDONS_PATH . 'includes/extensions/Maintenance_Mode/Maintenance_Mode.php');
286 + \King_Addons\Maintenance_Mode\Maintenance_Mode::instance();
287 + }
288 +
289 + // Data Table Builder
290 + if ($this->isExtensionEnabled('table-builder', 'KING_ADDONS_EXT_TABLE_BUILDER')) {
291 + require_once(KING_ADDONS_PATH . 'includes/extensions/Data_Table_Builder/Data_Table_Builder.php');
292 + Data_Table_Builder::instance();
293 + }
294 +
295 + // Site Preloader Animation
296 + if ($this->isExtensionEnabled('site-preloader', 'KING_ADDONS_EXT_SITE_PRELOADER')) {
297 + require_once(KING_ADDONS_PATH . 'includes/extensions/Site_Preloader/Site_Preloader.php');
298 + Site_Preloader::instance();
299 + }
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 +
317 + // Image Optimizer
318 + if ($this->isExtensionEnabled('image-optimizer', 'KING_ADDONS_EXT_IMAGE_OPTIMIZER')) {
319 + require_once(KING_ADDONS_PATH . 'includes/extensions/Image_Optimizer/Image_Optimizer.php');
320 + \King_Addons\Image_Optimizer\Image_Optimizer::instance();
321 + }
322 +
90 323 // Admin
91 324 require_once(KING_ADDONS_PATH . 'includes/Admin.php');
92 325
326 + // Rating Notice (admin only)
327 + // Temporarily disabled
328 + // if (is_admin()) {
329 + // require_once(KING_ADDONS_PATH . 'includes/admin/notices/RatingNotice.php');
330 + // \King_Addons\Admin\Notices\RatingNotice::instance();
331 + // }
332 +
93 333 // Additional - Controls
94 334 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2.php');
95 335 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2_API.php');
96 336 require_once(KING_ADDONS_PATH . 'includes/controls/Animations/Animations.php');
@@ -100,8 +340,9 @@
100 340 require_once(KING_ADDONS_PATH . 'includes/widgets/Search/Search_Ajax.php');
101 341 require_once(KING_ADDONS_PATH . 'includes/widgets/MailChimp/MailChimp_Ajax.php');
102 342
103 343 // Additional - Grids, Magazine Grid
344 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Grid_Ajax_Security.php');
104 345 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_Posts_Ajax.php');
105 346 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_WooCommerce_Products_Ajax.php');
106 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
107 348
@@ -106,8 +347,20 @@
106 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
107 348
108 349 // Additional - Form Builder
109 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');
110 363 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
111 364 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
112 365 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
113 366 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Subscribe_Mailchimp.php');
@@ -118,12 +371,29 @@
118 371 }
119 372
120 373 // ADDITIONAL CLASSES
121 374
122 - // Alt Text Generator for Media Library
123 - require_once(KING_ADDONS_PATH . 'includes/extensions/alt-text-generator/Alt_Text_Generator.php');
124 - new Alt_Text_Generator();
375 + // AI SEO Tools (Alt Text Generator + Auto Tagging)
376 + if ($this->isExtensionEnabled('ai-seo-tools', 'KING_ADDONS_EXT_AI_SEO_TOOLS')) {
377 + require_once(KING_ADDONS_PATH . 'includes/extensions/AI_SEO_Tools/AI_SEO_Tools.php');
378 + if (class_exists('King_Addons\\AI_SEO_Tools\\AI_SEO_Tools')) {
379 + \King_Addons\AI_SEO_Tools\AI_SEO_Tools::instance();
380 + }
381 + }
125 382
383 + // Wishlist module - check extension toggle
384 + if ($this->isExtensionEnabled('wishlist', 'KING_ADDONS_EXT_WISHLIST')) {
385 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_DB.php';
386 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Session.php';
387 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Settings.php';
388 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Service.php';
389 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Renderer.php';
390 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Frontend.php';
391 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_WooCommerce.php';
392 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Module.php';
393 + $this->wishlist_module = new Wishlist_Module();
394 + }
395 +
126 396 // Dynamic Posts Grid AJAX Helper - Initialize regardless of Elementor compatibility
127 397 // This is needed for AJAX functionality to work even when PRO version is disabled
128 398 require_once(KING_ADDONS_PATH . 'includes/helpers/Dynamic_Posts_Grid_Ajax.php');
129 399 \King_Addons\Dynamic_Posts_Grid_Ajax::get_instance();
@@ -139,8 +409,13 @@
139 409
140 410 add_action('elementor/init', [$this, 'initElementor']);
141 411
142 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);
143 418 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
144 419
145 420 self::enableFeatures();
146 421
@@ -161,12 +436,16 @@
161 436
162 437 // Initialize social login handler
163 438 \King_Addons\Widgets\Login_Register_Form\Social_Login_Handler::init();
164 439
165 - // Initialize Security Dashboard for admins
440 + // Initialize Security Dashboard for admins (only if Login Register Form widget is enabled)
166 441 if (is_admin()) {
167 - require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Security_Dashboard.php');
168 - \King_Addons\Widgets\Login_Register_Form\Security_Dashboard::init();
442 + $widget_options = get_option('king_addons_options', []);
443 + $login_form_enabled = !isset($widget_options['login-register-form']) || $widget_options['login-register-form'] === 'enabled';
444 + if ($login_form_enabled) {
445 + require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Security_Dashboard.php');
446 + \King_Addons\Widgets\Login_Register_Form\Security_Dashboard::init();
447 + }
169 448 }
170 449
171 450 new Admin();
172 451
@@ -175,11 +454,28 @@
175 454
176 455 // Notice - Upgrade Suggestion
177 456 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
178 457 add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
179 - 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 + );
180 473 }
181 474
475 + // Dashboard UI settings AJAX handler
476 + add_action('wp_ajax_king_addons_save_dashboard_ui', [$this, 'king_addons_save_dashboard_ui_callback']);
477 +
182 478 // Conditionally enqueue AI text-field enhancement script and styles in Elementor editor
183 479 $ai_options = get_option('king_addons_ai_options', []);
184 480 $enable_ai_text_buttons = isset($ai_options['enable_ai_buttons']) ? (bool) $ai_options['enable_ai_buttons'] : true;
185 481 if ($enable_ai_text_buttons) {
@@ -205,8 +501,11 @@
205 501 // Check user capabilities
206 502 if (!current_user_can('manage_options')) {
207 503 wp_die();
208 504 }
505 +
506 + check_ajax_referer('king_addons_premium_notice_dismiss', 'nonce');
507 +
209 508 $user_id = get_current_user_id();
210 509 // Save the current time as the last dismissal time for the premium notice
211 510 update_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', time());
212 511 wp_die(); // End AJAX request
@@ -211,8 +510,65 @@
211 510 update_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', time());
212 511 wp_die(); // End AJAX request
213 512 }
214 513
514 + /**
515 + * AJAX callback for saving dashboard UI settings (theme, view toggle)
516 + *
517 + * @since 1.0.0
518 + */
519 + function king_addons_save_dashboard_ui_callback()
520 + {
521 + // Check user capabilities
522 + if (!current_user_can('manage_options')) {
523 + wp_send_json_error(['message' => 'Unauthorized'], 403);
524 + }
525 +
526 + check_ajax_referer('king_addons_dashboard_ui', 'nonce');
527 +
528 + $key = isset($_POST['key']) ? sanitize_text_field($_POST['key']) : '';
529 +
530 + $user_id = get_current_user_id();
531 +
532 + // Validate key
533 + $allowed_keys = ['dark_theme', 'theme_mode', 'show_descriptions'];
534 + if (!in_array($key, $allowed_keys, true)) {
535 + wp_send_json_error(['message' => 'Invalid key'], 400);
536 + }
537 +
538 + // Theme preference is per-user.
539 + if ($key === 'theme_mode') {
540 + $mode = isset($_POST['value']) ? sanitize_key($_POST['value']) : '';
541 + $allowed_modes = ['dark', 'light', 'auto'];
542 + if (!in_array($mode, $allowed_modes, true)) {
543 + wp_send_json_error(['message' => 'Invalid theme mode'], 400);
544 + }
545 +
546 + update_user_meta($user_id, 'king_addons_theme_mode', $mode);
547 +
548 + // Also store as a global option so pages can fall back when user meta isn't set.
549 + update_option('king_addons_theme_mode', $mode);
550 +
551 + wp_send_json_success(['key' => $key, 'value' => $mode]);
552 + }
553 +
554 + // Backward compatibility: old boolean dark_theme maps to theme_mode.
555 + if ($key === 'dark_theme') {
556 + $is_dark = isset($_POST['value']) && $_POST['value'] === '1';
557 + $mode = $is_dark ? 'dark' : 'light';
558 + update_user_meta($user_id, 'king_addons_theme_mode', $mode);
559 + wp_send_json_success(['key' => 'theme_mode', 'value' => $mode]);
560 + }
561 +
562 + // Remaining UI settings are still stored as site option (shared).
563 + $value = isset($_POST['value']) && $_POST['value'] === '1';
564 + $settings = get_option('king_addons_dashboard_ui', []);
565 + $settings[$key] = $value;
566 + update_option('king_addons_dashboard_ui', $settings);
567 +
568 + wp_send_json_success(['key' => $key, 'value' => $value]);
569 + }
570 +
215 571 function showNoticeUpgrade()
216 572 {
217 573 // Check user capabilities; show notice only to administrators as an example
218 574 if (!current_user_can('manage_options')) {
@@ -228,44 +584,300 @@
228 584 if ($last_dismissed && ($now - $last_dismissed) < 604800) {
229 585 // if ($last_dismissed && ($now - $last_dismissed) < 60) {
230 586 return;
231 587 }
232 -?>
588 + ?>
233 589 <div class="king-addons-upgrade-notice notice notice-info is-dismissible"
234 - style="border-left: 4px solid #FF4040;padding: 10px 15px;">
590 + style="border-left: 4px solid #0071e3;padding: 10px 15px;">
235 591 <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
236 - <span>Unlock <strong style="font-weight: 700;">650+</strong> premium templates and <strong
237 - style="font-weight: 700;">200+</strong> advanced features for only $<strong
238 - style="font-weight: 700;">4.99</strong>/month. Upgrade now and boost your website's performance!</span>
592 + <span>
593 + Get <strong style="font-weight: 700;">4,000+</strong> premium templates and sections,
594 + <strong style="font-weight: 700;">80+</strong> widgets,
595 + <strong style="font-weight: 700;">200+</strong> advanced features,
596 + and AI tools for Elementor.
597 + From $<strong style="font-weight: 700;">4</strong>/mo, billed annually.
598 +</span>
239 599 </p>
240 600 <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
241 601 <p style="display: flex;">
242 - <a style="font-size: 14px;padding: 4px 22px;display: flex;align-items: center;background-image: linear-gradient(120deg, #A20BD8 0%, #FF4040 100%);border: none;"
602 + <a href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&amp;utm_medium=plugin&amp;utm_campaign=kng" target="_blank" class="ka-wb-btn ka-wb-btn-primary" style="
603 + background: #0071e3;
604 + color: #fff;
605 + display: inline-flex;
606 + align-items: center;
607 + justify-content: center;
608 + gap: 6px;
609 + padding: 10px 18px;
610 + font-size: 14px;
611 + font-weight: 500;
612 + text-decoration: none;
613 + border-radius: 980px;
614 + border: none;
615 + cursor: pointer;
616 + transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
617 + white-space: nowrap;
618 + font-family: inherit;
619 +">Upgrade to Pro<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="
620 + width: 16px;
621 + height: 16px;
622 +"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
623 + </a>
624 + <a style="margin-left: 20px;display: flex;align-items: center;font-size: 14px;color: #0071e3;"
243 625 href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
244 - class="button button-primary"><img style="margin-right: 7px;width: 15px;height: 15px;"
245 - src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/icon-for-admin.svg'; ?>"
246 - alt="<?php echo esc_html__('Upgrade Now', 'king-addons'); ?>">Upgrade Now</a>
247 - <a style="margin-left: 20px;display: flex;align-items: center;"
248 - href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
249 626 class="link">Learn More</a>
250 627 </p>
251 628 </div>
252 629 <script>
253 - (function($) {
630 + (function ($) {
254 631 // Wait for the document to be ready
255 - $(document).ready(function() {
632 + const kingAddonsPremiumNoticeNonce = '<?php echo esc_js(wp_create_nonce('king_addons_premium_notice_dismiss')); ?>';
633 + $(document).ready(function () {
256 634 // Attach click handler to the dismiss button of the premium notice
257 - $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function() {
635 + $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function () {
258 636 $.post(ajaxurl, {
259 - action: 'king_addons_premium_notice_dismiss'
637 + action: 'king_addons_premium_notice_dismiss',
638 + nonce: kingAddonsPremiumNoticeNonce
260 639 });
261 640 });
262 641 });
263 642 })(jQuery);
264 643 </script>
265 -<?php
644 + <?php
266 645 }
267 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 +
268 880 function enqueueFrontendStyles()
269 881 {
270 882 /**
271 883 * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
@@ -341,22 +953,371 @@
341 953 {
342 954 add_action('elementor/widgets/register', [$this, 'registerWidgets']);
343 955 add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueEditorStyles']);
344 956 add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueEditorScripts']);
957 + add_action('elementor/preview/enqueue_styles', [$this, 'enqueueEditorPreviewStyles']);
345 958 }
346 959
960 + function enqueueEditorPreviewStyles(): void
961 + {
962 + wp_enqueue_style(
963 + KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-preview',
964 + KING_ADDONS_URL . 'includes/admin/css/elementor-preview.css',
965 + [],
966 + KING_ADDONS_VERSION
967 + );
968 + }
969 +
347 970 function addWidgetCategory(): void
348 971 {
349 - Plugin::instance()->elements_manager->add_category(
972 + $elements_manager = Plugin::instance()->elements_manager;
973 +
974 + $elements_manager->add_category(
350 975 'king-addons',
351 976 [
352 977 'title' => esc_html__('King Addons', 'king-addons'),
353 - 'icon' => 'fa fa-plug'
978 + 'icon' => 'fa fa-plug',
354 979 ]
355 980 );
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 +
1002 + $elements_manager->add_category(
1003 + 'king-addons-theme-builder',
1004 + [
1005 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1006 + 'icon' => 'fa fa-plug',
1007 + 'hideIfEmpty' => true,
1008 + ]
1009 + );
356 1010 }
357 1011
358 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();
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 + }
1035 + }
1036 +
1037 + /**
1038 + * Overwrite the editor panel category snapshot after Elementor has built document config.
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 + *
1082 + * @param \Elementor\Elements_Manager $elements_manager
1083 + * @return void
1084 + */
1085 + private function reorderWidgetCategories($elements_manager): void
1086 + {
1087 + try {
1088 + $reflection = new \ReflectionClass($elements_manager);
1089 + $categories_property = $reflection->getProperty('categories');
1090 + if (PHP_VERSION_ID < 80100) {
1091 + $categories_property->setAccessible(true);
1092 + }
1093 +
1094 + $categories = $categories_property->getValue($elements_manager);
1095 + if (!is_array($categories) || $categories === []) {
1096 + return;
1097 + }
1098 +
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 + }
1111 + }
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 + }
1119 + }
1120 + $ours_sorted += $ours;
1121 +
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 + }
1127 +
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);
1147 + }
1148 +
1149 + $categories = $categories_property->getValue($elements_manager);
1150 + if (!is_array($categories) || $categories === []) {
1151 + return;
1152 + }
1153 +
1154 + foreach ($hidden as $key) {
1155 + unset($categories[$key]);
1156 + }
1157 +
1158 + $categories_property->setValue($elements_manager, $categories);
1159 + } catch (\ReflectionException $e) {
1160 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1161 + }
1162 + }
1163 +
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 + /**
359 1320 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
360 1321 * that caused a fatal error previously, and try them again if the plugin version is updated.
361 1322 *
362 1323 * @param Widgets_Manager $widgets_manager
@@ -370,9 +1331,46 @@
370 1331 $currentPluginVersion = KING_ADDONS_VERSION;
371 1332
372 1333 // Get plugin options to check if a widget is enabled
373 1334 $options = get_option('king_addons_options');
1335 + $options = is_array($options) ? $options : [];
374 1336
1337 + // Extension toggles (used to prevent loading dependent widgets when extension is disabled).
1338 + $wishlist_extension_enabled = !isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled';
1339 + if (defined('KING_ADDONS_EXT_WISHLIST') && KING_ADDONS_EXT_WISHLIST === false) {
1340 + $wishlist_extension_enabled = false;
1341 + }
1342 +
1343 + // Ensure Woo Builder base class is available for single product widgets.
1344 + $abstract_single_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Single_Widget.php';
1345 + if (file_exists($abstract_single_widget)) {
1346 + require_once $abstract_single_widget;
1347 + }
1348 +
1349 + // Ensure Woo Builder base class is available for archive widgets.
1350 + $abstract_archive_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Archive_Widget.php';
1351 + if (file_exists($abstract_archive_widget)) {
1352 + require_once $abstract_archive_widget;
1353 + }
1354 +
1355 + // Ensure Woo Builder base class is available for cart widgets.
1356 + $abstract_cart_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Cart_Widget.php';
1357 + if (file_exists($abstract_cart_widget)) {
1358 + require_once $abstract_cart_widget;
1359 + }
1360 +
1361 + // Ensure Woo Builder base class is available for checkout widgets.
1362 + $abstract_checkout_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Checkout_Widget.php';
1363 + if (file_exists($abstract_checkout_widget)) {
1364 + require_once $abstract_checkout_widget;
1365 + }
1366 +
1367 + // Ensure Woo Builder base class is available for My Account widgets.
1368 + $abstract_my_account_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_My_Account_Widget.php';
1369 + if (file_exists($abstract_my_account_widget)) {
1370 + require_once $abstract_my_account_widget;
1371 + }
1372 +
375 1373 /**
376 1374 * Retrieve the array of broken widgets from the WordPress options.
377 1375 * The structure is expected to be something like:
378 1376 *
@@ -427,13 +1425,25 @@
427 1425 * STEP 3: Now we iterate through all widgets in our modules map and try to load them.
428 1426 * If a widget is in the broken list, we skip it to avoid repeated fatal errors.
429 1427 */
430 1428 foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
1429 + // Hard-disable via constant (used to QA/rollout new widgets).
1430 + $widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
1431 + if (defined($widget_constant) && constant($widget_constant) === false) {
1432 + continue;
1433 + }
1434 +
431 1435 // Check if the widget is enabled in the options
432 1436 if (!isset($options[$widget_id]) || $options[$widget_id] !== 'enabled') {
433 1437 continue;
434 1438 }
435 1439
1440 + // Skip Wishlist widgets when Wishlist extension is disabled.
1441 + // This prevents fatals when wishlist classes aren't loaded.
1442 + if (!$wishlist_extension_enabled && strpos((string) $widget_id, 'wishlist-') === 0) {
1443 + continue;
1444 + }
1445 +
436 1446 // If this widget is listed as broken, skip it
437 1447 if (array_key_exists($widget_id, $brokenWidgets)) {
438 1448 // Log something here if needed:
439 1449 // error_log("Skipping widget {$widget_id}, it previously caused a fatal error.");
@@ -445,10 +1455,17 @@
445 1455
446 1456 // Include the base widget class
447 1457 $widget_class = $widget['php-class'];
448 1458 $path_widget_class = "King_Addons\\" . $widget_class;
449 - require_once(KING_ADDONS_PATH . 'includes/widgets/' . $widget_class . '/' . $widget_class . '.php');
1459 + $widget_file = KING_ADDONS_PATH . 'includes/widgets/' . $widget_class . '/' . $widget_class . '.php';
1460 + if (!file_exists($widget_file)) {
1461 + // Skip missing widget files to avoid fatal errors if registry is ahead of implementation.
1462 + $currentlyLoadingWidgetId = null;
1463 + continue;
1464 + }
450 1465
1466 + require_once $widget_file;
1467 +
451 1468 // Check if we can load the Pro version
452 1469 if (
453 1470 function_exists('king_addons_freemius')
454 1471 && king_addons_freemius()->can_use_premium_code__premium_only()
@@ -484,8 +1501,14 @@
484 1501 $options = get_option('king_addons_options');
485 1502
486 1503 foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
487 1504
1505 + // Hard-disable via constant (used to QA/rollout new widgets).
1506 + $widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
1507 + if (defined($widget_constant) && constant($widget_constant) === false) {
1508 + continue;
1509 + }
1510 +
488 1511 if (!($options[$widget_id] ?? null)) {
489 1512 $options[$widget_id] = 'enabled';
490 1513 update_option('king_addons_options', $options);
491 1514 }
@@ -491,13 +1514,25 @@
491 1514 }
492 1515 }
493 1516 }
494 1517
495 - function enableFeatures(): void
1518 + /**
1519 + * Enable and bootstrap registered features.
1520 + *
1521 + * Loads free feature classes and, when available and licensed, their Pro counterparts.
1522 + *
1523 + * @return void
1524 + */
1525 + public function enableFeatures(): void
496 1526 {
497 1527 $options = get_option('king_addons_options');
498 1528
499 1529 foreach (ModulesMap::getModulesMapArray()['features'] as $feature_id => $feature) {
1530 + // Hard-disable via constant (used to QA/rollout new features).
1531 + $feature_constant = 'KING_ADDONS_FEAT_' . strtoupper(str_replace('-', '_', (string) $feature_id));
1532 + if (defined($feature_constant) && constant($feature_constant) === false) {
1533 + continue;
1534 + }
500 1535
501 1536 if (!($options[$feature_id] ?? null)) {
502 1537 $options[$feature_id] = 'enabled';
503 1538 update_option('king_addons_options', $options);
@@ -502,14 +1537,44 @@
502 1537 $options[$feature_id] = 'enabled';
503 1538 update_option('king_addons_options', $options);
504 1539 }
505 1540
506 - if ($options[$feature_id] === 'enabled') {
507 - $feature_class = $feature['php-class'];
508 - $path_feature_class = "King_Addons\\" . $feature_class;
509 - require_once(KING_ADDONS_PATH . 'includes/features/' . $feature_class . '/' . $feature_class . '.php');
510 - new $path_feature_class;
1541 + if ($options[$feature_id] !== 'enabled') {
1542 + continue;
511 1543 }
1544 +
1545 + $feature_class = $feature['php-class'];
1546 + $path_feature_class = "King_Addons\\" . $feature_class;
1547 + $feature_file = KING_ADDONS_PATH . 'includes/features/' . $feature_class . '/' . $feature_class . '.php';
1548 +
1549 + if (file_exists($feature_file)) {
1550 + require_once $feature_file;
1551 + }
1552 +
1553 + $pro_loaded = false;
1554 +
1555 + if (
1556 + !empty($feature['has-pro'])
1557 + && function_exists('king_addons_freemius')
1558 + && king_addons_freemius()->can_use_premium_code__premium_only()
1559 + && defined('KING_ADDONS_PRO_PATH')
1560 + ) {
1561 + $pro_file_path = KING_ADDONS_PRO_PATH . 'includes/features/' . $feature_class . '_Pro/' . $feature_class . '_Pro.php';
1562 +
1563 + if (file_exists($pro_file_path)) {
1564 + require_once $pro_file_path;
1565 +
1566 + $path_feature_class_pro = "King_Addons\\" . $feature_class . '_Pro';
1567 + if (class_exists($path_feature_class_pro)) {
1568 + new $path_feature_class_pro();
1569 + $pro_loaded = true;
1570 + }
1571 + }
1572 + }
1573 +
1574 + if (!$pro_loaded && class_exists($path_feature_class)) {
1575 + new $path_feature_class();
1576 + }
512 1577 }
513 1578 }
514 1579
515 1580 public function registerControls(Controls_Manager $controls_manager): void
@@ -540,8 +1605,40 @@
540 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);
541 1606 }
542 1607 }
543 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 +
544 1641 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
545 1642 {
546 1643 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
547 1644 return;
@@ -568,8 +1665,34 @@
568 1665
569 1666 $module->end_controls_section();
570 1667 }
571 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 +
572 1695 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
573 1696 {
574 1697 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
575 1698 return;
@@ -932,9 +2055,14 @@
932 2055 if (!$api_key) {
933 2056 return $mailchimp_list;
934 2057 }
935 2058
936 - $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/';
937 2065 $response = wp_remote_get($url, [
938 2066 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
939 2067 ]);
940 2068
@@ -949,12 +2077,22 @@
949 2077 }
950 2078
951 2079 public static function getMailchimpGroups()
952 2080 {
953 - $apiKey = get_option('king_addons_mailchimp_api_key');
954 - $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/';
955 2094 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
956 - $groups = ['def' => 'Select Group'];
957 2095 $mailchimpIDs = Core::getMailchimpLists();
958 2096
959 2097 foreach ($mailchimpIDs as $audience => $ignore) {
960 2098 if ($audience === 'def') {
@@ -960,14 +2098,20 @@
960 2098 if ($audience === 'def') {
961 2099 continue;
962 2100 }
963 2101
964 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
965 - $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 ?? [];
966 2107
967 2108 foreach ($cats as $cat) {
968 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
969 - $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 ?? [];
970 2114
971 2115 foreach ($interests as $int) {
972 2116 $groups[$int->id] = $int->name;
973 2117 }
@@ -1052,9 +2196,9 @@
1052 2196 array_map(function ($slug) {
1053 2197 $keys = [];
1054 2198 foreach (get_posts(['post_type' => $slug, 'posts_per_page' => -1]) as $post) {
1055 2199 // get_post_custom_keys can return null, so cast to array:
1056 - foreach ((array)get_post_custom_keys($post->ID) as $meta_key) {
2200 + foreach ((array) get_post_custom_keys($post->ID) as $meta_key) {
1057 2201 // Exclude protected keys (those beginning with "_").
1058 2202 if ($meta_key[0] !== '_') {
1059 2203 $keys[] = $meta_key;
1060 2204 }
@@ -1103,8 +2247,10 @@
1103 2247 * @return void
1104 2248 */
1105 2249 public function enqueueAiFieldScript(): void
1106 2250 {
2251 + $ai_options = get_option('king_addons_ai_options', []);
2252 +
1107 2253 wp_enqueue_script(
1108 2254 'king-addons-ai-field',
1109 2255 KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
1110 2256 ['jquery', 'elementor-editor'],
@@ -1116,20 +2262,61 @@
1116 2262 wp_localize_script(
1117 2263 'king-addons-ai-field',
1118 2264 'KingAddonsAiField',
1119 2265 [
1120 - 'ajax_url' => admin_url('admin-ajax.php'),
1121 - 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_nonce'),
1122 - 'change_nonce' => wp_create_nonce('king_addons_ai_change_nonce'),
2266 + 'ajax_url' => admin_url('admin-ajax.php'),
2267 + 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_nonce'),
2268 + 'change_nonce' => wp_create_nonce('king_addons_ai_change_nonce'),
1123 2269 'generate_action' => 'king_addons_ai_generate_text',
1124 - 'change_action' => 'king_addons_ai_change_text',
1125 - 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
2270 + 'change_action' => 'king_addons_ai_change_text',
2271 + 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1126 2272 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1127 - 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1128 - 'plugin_url' => KING_ADDONS_URL,
1129 - 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1130 - 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
2273 + 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
2274 + 'plugin_url' => KING_ADDONS_URL,
2275 + 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
2276 + 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1131 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 + ),
1132 2319 ]
1133 2320 );
1134 2321 }
1135 2322
@@ -1139,10 +2326,8 @@
1139 2326 * @return void
1140 2327 */
1141 2328 public function enqueueAiImageGenerationScript(): void
1142 2329 {
1143 - // Retrieve AI options and ensure it's an array to prevent warnings.
1144 - $ai_options = get_option('king_addons_ai_options', []);
1145 2330 wp_enqueue_script(
1146 2331 'king-addons-ai-image-field',
1147 2332 KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
1148 2333 ['jquery', 'elementor-editor'],
@@ -1154,16 +2339,30 @@
1154 2339 wp_localize_script(
1155 2340 'king-addons-ai-image-field',
1156 2341 'KingAddonsAiImageField',
1157 2342 [
1158 - 'ajax_url' => admin_url('admin-ajax.php'),
1159 - 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
2343 + 'ajax_url' => admin_url('admin-ajax.php'),
2344 + 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
1160 2345 'generate_action' => 'king_addons_ai_generate_image',
1161 - 'image_model' => sanitize_text_field($ai_options['openai_image_model'] ?? ''),
1162 - 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
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(),
2356 + 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1163 2357 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1164 - 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1165 - 'plugin_url' => KING_ADDONS_URL,
2358 + 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
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 + ),
1166 2365 ]
1167 2366 );
1168 2367 }
1169 2368
@@ -1223,8 +2422,87 @@
1223 2422
1224 2423 // Note: Using existing KingAddonsAiField localization
1225 2424 // The translator script will use the same AJAX endpoints and settings
1226 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);
1227 2505 }
1228 2506 }
1229 2507
1230 2508 Core::instance();