PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.86
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.86
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
← All changes | includes/Core.php +934 -31 51.1.44 → 51.1.86 View file →
@@ -8,8 +8,9 @@
8 8
9 9 use Elementor\Plugin;
10 10 use Elementor\Widgets_Manager;
11 11 use Elementor\Controls_Manager;
12 +use Elementor\Group_Control_Image_Size;
12 13 use King_Addons\Wishlist\Wishlist_Module;
13 14
14 15 if (!defined('ABSPATH')) {
15 16 exit; // Exit if accessed directly.
@@ -59,8 +60,13 @@
59 60 * @since 1.0.0
60 61 */
61 62 private function isExtensionEnabled(string $extension_id, string $constant_name): bool
62 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 +
63 69 // Get options from database
64 70 $options = get_option('king_addons_options', []);
65 71
66 72 // If a constant is defined and explicitly false, treat it as a hard disable.
@@ -98,8 +104,10 @@
98 104 public function __construct()
99 105 {
100 106 require_once(KING_ADDONS_PATH . 'includes/ModulesMap.php');
101 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();
102 110
103 111 if ($this->hasElementorCompatibility()) {
104 112
105 113 // Initial requirements check
@@ -133,8 +141,26 @@
133 141 require_once(KING_ADDONS_PATH . 'includes/extensions/Cookie_Consent/Cookie_Consent.php');
134 142 Cookie_Consent::instance();
135 143 }
136 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 +
137 163 // WooCommerce Builder
138 164 if ($this->isExtensionEnabled('woo-builder', 'KING_ADDONS_EXT_WOO_BUILDER')) {
139 165 require_once(KING_ADDONS_PATH . 'includes/extensions/Woo_Builder/Woo_Builder.php');
140 166 if (class_exists('King_Addons\\Woo_Builder')) {
@@ -271,11 +297,40 @@
271 297 require_once(KING_ADDONS_PATH . 'includes/extensions/Site_Preloader/Site_Preloader.php');
272 298 Site_Preloader::instance();
273 299 }
274 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 +
275 323 // Admin
276 324 require_once(KING_ADDONS_PATH . 'includes/Admin.php');
277 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 +
278 333 // Additional - Controls
279 334 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2.php');
280 335 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2_API.php');
281 336 require_once(KING_ADDONS_PATH . 'includes/controls/Animations/Animations.php');
@@ -285,8 +340,9 @@
285 340 require_once(KING_ADDONS_PATH . 'includes/widgets/Search/Search_Ajax.php');
286 341 require_once(KING_ADDONS_PATH . 'includes/widgets/MailChimp/MailChimp_Ajax.php');
287 342
288 343 // Additional - Grids, Magazine Grid
344 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Grid_Ajax_Security.php');
289 345 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_Posts_Ajax.php');
290 346 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_WooCommerce_Products_Ajax.php');
291 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
292 348
@@ -291,8 +347,20 @@
291 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
292 348
293 349 // Additional - Form Builder
294 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');
295 363 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
296 364 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
297 365 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
298 366 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Subscribe_Mailchimp.php');
@@ -303,11 +371,15 @@
303 371 }
304 372
305 373 // ADDITIONAL CLASSES
306 374
307 - // Alt Text Generator for Media Library
308 - require_once(KING_ADDONS_PATH . 'includes/extensions/alt-text-generator/Alt_Text_Generator.php');
309 - 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 + }
310 382
311 383 // Wishlist module - check extension toggle
312 384 if ($this->isExtensionEnabled('wishlist', 'KING_ADDONS_EXT_WISHLIST')) {
313 385 require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_DB.php';
@@ -337,8 +409,13 @@
337 409
338 410 add_action('elementor/init', [$this, 'initElementor']);
339 411
340 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);
341 418 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
342 419
343 420 self::enableFeatures();
344 421
@@ -377,9 +454,23 @@
377 454
378 455 // Notice - Upgrade Suggestion
379 456 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
380 457 add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
381 - 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 + );
382 473 }
383 474
384 475 // Dashboard UI settings AJAX handler
385 476 add_action('wp_ajax_king_addons_save_dashboard_ui', [$this, 'king_addons_save_dashboard_ui_callback']);
@@ -452,8 +543,12 @@
452 543 wp_send_json_error(['message' => 'Invalid theme mode'], 400);
453 544 }
454 545
455 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 +
456 551 wp_send_json_success(['key' => $key, 'value' => $mode]);
457 552 }
458 553
459 554 // Backward compatibility: old boolean dark_theme maps to theme_mode.
@@ -491,23 +586,44 @@
491 586 return;
492 587 }
493 588 ?>
494 589 <div class="king-addons-upgrade-notice notice notice-info is-dismissible"
495 - style="border-left: 4px solid #FF4040;padding: 10px 15px;">
590 + style="border-left: 4px solid #0071e3;padding: 10px 15px;">
496 591 <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
497 - <span>Unlock <strong style="font-weight: 700;">650+</strong> premium templates and <strong
498 - style="font-weight: 700;">200+</strong> advanced features for only $<strong
499 - 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;">100+</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;">6.99</strong>/mo.
598 +</span>
500 599 </p>
501 600 <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
502 601 <p style="display: flex;">
503 - <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;"
504 625 href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
505 - class="button button-primary"><img style="margin-right: 7px;width: 15px;height: 15px;"
506 - src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/icon-for-admin.svg'; ?>"
507 - alt="<?php echo esc_html__('Upgrade Now', 'king-addons'); ?>">Upgrade Now</a>
508 - <a style="margin-left: 20px;display: flex;align-items: center;"
509 - href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
510 626 class="link">Learn More</a>
511 627 </p>
512 628 </div>
513 629 <script>
@@ -527,8 +643,241 @@
527 643 </script>
528 644 <?php
529 645 }
530 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 +
531 880 function enqueueFrontendStyles()
532 881 {
533 882 /**
534 883 * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
@@ -604,30 +953,371 @@
604 953 {
605 954 add_action('elementor/widgets/register', [$this, 'registerWidgets']);
606 955 add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueEditorStyles']);
607 956 add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueEditorScripts']);
957 + add_action('elementor/preview/enqueue_styles', [$this, 'enqueueEditorPreviewStyles']);
608 958 }
609 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 +
610 970 function addWidgetCategory(): void
611 971 {
612 - Plugin::instance()->elements_manager->add_category(
972 + $elements_manager = Plugin::instance()->elements_manager;
973 +
974 + $elements_manager->add_category(
613 975 'king-addons',
614 976 [
615 977 'title' => esc_html__('King Addons', 'king-addons'),
616 - 'icon' => 'fa fa-plug'
978 + 'icon' => 'fa fa-plug',
617 979 ]
618 980 );
619 981
620 - Plugin::instance()->elements_manager->add_category(
621 - 'king-addons-woo-builder',
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',
622 1004 [
623 - 'title' => esc_html__('King Addons Woo Builder', 'king-addons'),
624 - 'icon' => 'fa fa-shopping-cart'
1005 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1006 + 'icon' => 'fa fa-plug',
1007 + 'hideIfEmpty' => true,
625 1008 ]
626 1009 );
627 1010 }
628 1011
629 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 + /**
630 1320 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
631 1321 * that caused a fatal error previously, and try them again if the plugin version is updated.
632 1322 *
633 1323 * @param Widgets_Manager $widgets_manager
@@ -915,8 +1605,40 @@
915 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);
916 1606 }
917 1607 }
918 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 +
919 1641 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
920 1642 {
921 1643 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
922 1644 return;
@@ -943,8 +1665,34 @@
943 1665
944 1666 $module->end_controls_section();
945 1667 }
946 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 +
947 1695 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
948 1696 {
949 1697 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
950 1698 return;
@@ -1307,9 +2055,14 @@
1307 2055 if (!$api_key) {
1308 2056 return $mailchimp_list;
1309 2057 }
1310 2058
1311 - $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/';
1312 2065 $response = wp_remote_get($url, [
1313 2066 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
1314 2067 ]);
1315 2068
@@ -1324,12 +2077,22 @@
1324 2077 }
1325 2078
1326 2079 public static function getMailchimpGroups()
1327 2080 {
1328 - $apiKey = get_option('king_addons_mailchimp_api_key');
1329 - $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/';
1330 2094 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
1331 - $groups = ['def' => 'Select Group'];
1332 2095 $mailchimpIDs = Core::getMailchimpLists();
1333 2096
1334 2097 foreach ($mailchimpIDs as $audience => $ignore) {
1335 2098 if ($audience === 'def') {
@@ -1335,14 +2098,20 @@
1335 2098 if ($audience === 'def') {
1336 2099 continue;
1337 2100 }
1338 2101
1339 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
1340 - $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 ?? [];
1341 2107
1342 2108 foreach ($cats as $cat) {
1343 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
1344 - $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 ?? [];
1345 2114
1346 2115 foreach ($interests as $int) {
1347 2116 $groups[$int->id] = $int->name;
1348 2117 }
@@ -1478,8 +2247,10 @@
1478 2247 * @return void
1479 2248 */
1480 2249 public function enqueueAiFieldScript(): void
1481 2250 {
2251 + $ai_options = get_option('king_addons_ai_options', []);
2252 +
1482 2253 wp_enqueue_script(
1483 2254 'king-addons-ai-field',
1484 2255 KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
1485 2256 ['jquery', 'elementor-editor'],
@@ -1503,8 +2274,49 @@
1503 2274 'plugin_url' => KING_ADDONS_URL,
1504 2275 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1505 2276 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1506 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 + ),
1507 2319 ]
1508 2320 );
1509 2321 }
1510 2322
@@ -1514,10 +2326,8 @@
1514 2326 * @return void
1515 2327 */
1516 2328 public function enqueueAiImageGenerationScript(): void
1517 2329 {
1518 - // Retrieve AI options and ensure it's an array to prevent warnings.
1519 - $ai_options = get_option('king_addons_ai_options', []);
1520 2330 wp_enqueue_script(
1521 2331 'king-addons-ai-image-field',
1522 2332 KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
1523 2333 ['jquery', 'elementor-editor'],
@@ -1532,13 +2342,27 @@
1532 2342 [
1533 2343 'ajax_url' => admin_url('admin-ajax.php'),
1534 2344 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
1535 2345 'generate_action' => 'king_addons_ai_generate_image',
1536 - 'image_model' => sanitize_text_field($ai_options['openai_image_model'] ?? ''),
2346 + 'image_model' => \King_Addons\AI_Provider::getImageModel(),
2347 + // The editor builds its model dropdown from this list, so the
2348 + // options follow whichever provider is configured.
2349 + 'image_models' => array_map(
2350 + static function ($model) {
2351 + return ['value' => $model['id'], 'label' => $model['label']];
2352 + },
2353 + \King_Addons\AI_Provider::getModelsFor('image')
2354 + ),
2355 + 'provider' => \King_Addons\AI_Provider::getProvider(),
1537 2356 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1538 2357 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1539 2358 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1540 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 + ),
1541 2365 ]
1542 2366 );
1543 2367 }
1544 2368
@@ -1598,8 +2422,87 @@
1598 2422
1599 2423 // Note: Using existing KingAddonsAiField localization
1600 2424 // The translator script will use the same AJAX endpoints and settings
1601 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);
1602 2505 }
1603 2506 }
1604 2507
1605 2508 Core::instance();