';
}
echo $message;
}
function showAdminNotice_ElementorMinimumVersion(): void
{
$message = sprintf(
/* translators: 1: Plugin name 2: Elementor 3: Required Elementor version */
esc_html__('%1$s plugin requires %2$s plugin version %3$s or greater.', 'king-addons'),
esc_html__('King Addons', 'king-addons'),
esc_html__('Elementor', 'king-addons'),
'3.19.0'
);
echo '
' . esc_html($message) . '
';
}
public function initElementor(): void
{
add_action('elementor/widgets/register', [$this, 'registerWidgets']);
add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueEditorStyles']);
add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueEditorScripts']);
add_action('elementor/preview/enqueue_styles', [$this, 'enqueueEditorPreviewStyles']);
}
function enqueueEditorPreviewStyles(): void
{
wp_enqueue_style(
KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-preview',
KING_ADDONS_URL . 'includes/admin/css/elementor-preview.css',
[],
KING_ADDONS_VERSION
);
}
function addWidgetCategory(): void
{
$elements_manager = Plugin::instance()->elements_manager;
$elements_manager->add_category(
'king-addons',
[
'title' => esc_html__('King Addons', 'king-addons'),
'icon' => 'fa fa-plug',
]
);
if (class_exists('WooCommerce') && function_exists('WC')) {
$elements_manager->add_category(
'king-addons-woo',
[
'title' => esc_html__('King Addons WooCommerce', 'king-addons'),
'icon' => 'fa fa-shopping-cart',
'hideIfEmpty' => true,
]
);
$elements_manager->add_category(
'king-addons-woo-builder',
[
'title' => esc_html__('King Addons WooCommerce Builder', 'king-addons'),
'icon' => 'fa fa-shopping-cart',
'hideIfEmpty' => true,
]
);
}
$elements_manager->add_category(
'king-addons-theme-builder',
[
'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
'icon' => 'fa fa-plug',
'hideIfEmpty' => true,
]
);
}
/**
* Apply the panel category order after Elementor has finished registering its own.
*
* @return void
*/
public function reorderWidgetCategoriesLate(): void
{
if (!did_action('elementor/loaded')) {
return;
}
$elements_manager = Plugin::instance()->elements_manager;
$elements_manager->get_categories();
$this->reorderWidgetCategories($elements_manager);
if (!$this->shouldApplyEditorPanelVisibility()) {
return;
}
$post_id = $this->getCurrentEditorPostId();
if ($post_id > 0) {
$this->applyContextCategoryVisibility($elements_manager, $post_id);
}
}
/**
* Overwrite the editor panel category snapshot after Elementor has built document config.
*
* @param array $additional_config
* @param int $post_id
* @return array
*/
public function filterDocumentPanelCategories($additional_config, $post_id = 0)
{
$post_id = (int) $post_id;
if ($post_id <= 0) {
$post_id = $this->getCurrentEditorPostId();
}
if (!is_array($additional_config)) {
$additional_config = [];
}
if (!did_action('elementor/loaded')) {
return $additional_config;
}
$elements_manager = Plugin::instance()->elements_manager;
$elements_manager->get_categories();
$this->reorderWidgetCategories($elements_manager);
if ($post_id > 0) {
$this->applyContextCategoryVisibility($elements_manager, $post_id);
}
$categories = \Elementor\Core\Base\Document::get_filtered_editor_panel_categories();
foreach ($this->getHiddenPanelCategoryKeys($post_id) as $hidden_key) {
unset($categories[$hidden_key]);
}
$additional_config['panel']['elements_categories'] = $categories;
foreach ($this->getHiddenPanelWidgetNames($post_id) as $widget_name) {
$additional_config['widgets'][$widget_name]['show_in_panel'] = false;
}
return $additional_config;
}
/**
* Reorder widget categories: Layout, then King Addons groups, then the rest of Elementor.
*
* @param \Elementor\Elements_Manager $elements_manager
* @return void
*/
private function reorderWidgetCategories($elements_manager): void
{
try {
$reflection = new \ReflectionClass($elements_manager);
$categories_property = $reflection->getProperty('categories');
if (PHP_VERSION_ID < 80100) {
$categories_property->setAccessible(true);
}
$categories = $categories_property->getValue($elements_manager);
if (!is_array($categories) || $categories === []) {
return;
}
$layout = [];
$ours = [];
$rest = [];
foreach ($categories as $key => $value) {
if ('layout' === $key) {
$layout[$key] = $value;
} elseif (str_starts_with((string) $key, 'king-addons')) {
$ours[$key] = $value;
} else {
$rest[$key] = $value;
}
}
$ours_sorted = [];
foreach (['king-addons', 'king-addons-woo', 'king-addons-woo-builder', 'king-addons-theme-builder'] as $preferred) {
if (isset($ours[$preferred])) {
$ours_sorted[$preferred] = $ours[$preferred];
unset($ours[$preferred]);
}
}
$ours_sorted += $ours;
$categories_property->setValue($elements_manager, $layout + $ours_sorted + $rest);
} catch (\ReflectionException $e) {
// Silently fail if reflection doesn't work (e.g., future Elementor changes)
}
}
/**
* Drop context-only categories from the manager for the current editor document.
*
* @param \Elementor\Elements_Manager $elements_manager
* @param int $post_id
* @return void
*/
private function applyContextCategoryVisibility($elements_manager, int $post_id): void
{
$hidden = $this->getHiddenPanelCategoryKeys($post_id);
if ($hidden === []) {
return;
}
try {
$reflection = new \ReflectionClass($elements_manager);
$categories_property = $reflection->getProperty('categories');
if (PHP_VERSION_ID < 80100) {
$categories_property->setAccessible(true);
}
$categories = $categories_property->getValue($elements_manager);
if (!is_array($categories) || $categories === []) {
return;
}
foreach ($hidden as $key) {
unset($categories[$key]);
}
$categories_property->setValue($elements_manager, $categories);
} catch (\ReflectionException $e) {
// Silently fail if reflection doesn't work (e.g., future Elementor changes)
}
}
/**
* Category keys that should not appear in the current editor document.
*
* @param int $post_id
* @return array
*/
private function getHiddenPanelCategoryKeys(int $post_id): array
{
$hidden = [];
$is_woo_builder = $this->isWooBuilderDocument($post_id);
$is_theme_builder = $this->isThemeBuilderDocument($post_id);
$is_loop_item = $this->isLoopItemDocument($post_id);
$woocommerce_active = class_exists('WooCommerce') && function_exists('WC');
if (!$woocommerce_active) {
$hidden[] = 'king-addons-woo';
$hidden[] = 'king-addons-woo-builder';
} elseif (!$is_woo_builder && !$is_loop_item) {
$hidden[] = 'king-addons-woo-builder';
}
if (!$is_theme_builder && !$is_loop_item) {
$hidden[] = 'king-addons-theme-builder';
}
return $hidden;
}
/**
* Widget names that belong to hidden context categories.
*
* @param int $post_id
* @return array
*/
private function getHiddenPanelWidgetNames(int $post_id): array
{
$hidden_categories = $this->getHiddenPanelCategoryKeys($post_id);
if ($hidden_categories === [] || !did_action('elementor/loaded')) {
return [];
}
$hidden_lookup = array_fill_keys($hidden_categories, true);
$names = [];
foreach (Plugin::instance()->widgets_manager->get_widget_types() as $widget_name => $widget) {
if (!is_object($widget) || !method_exists($widget, 'get_categories')) {
continue;
}
foreach ((array) $widget->get_categories() as $category) {
if (isset($hidden_lookup[$category])) {
$names[] = (string) $widget_name;
break;
}
}
}
return $names;
}
/**
* Whether the current request is the Elementor editor (or its AJAX).
*
* @return bool
*/
private function shouldApplyEditorPanelVisibility(): bool
{
if (!empty($_GET['action']) && 'elementor' === $_GET['action']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return true;
}
if (!empty($_REQUEST['editor_post_id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return true;
}
return class_exists('\Elementor\Plugin')
&& Plugin::instance()->editor
&& Plugin::instance()->editor->is_edit_mode();
}
/**
* Current Elementor editor post ID, if any.
*
* @return int
*/
private function getCurrentEditorPostId(): int
{
foreach (['editor_post_id', 'post_id', 'post'] as $key) {
if (!empty($_REQUEST[$key])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$post_id = absint(wp_unslash($_REQUEST[$key])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ($post_id > 0) {
return $post_id;
}
}
}
$post = get_post();
return $post ? (int) $post->ID : 0;
}
/**
* Whether the post is a King Addons Woo Builder template.
*
* @param int $post_id
* @return bool
*/
private function isWooBuilderDocument(int $post_id): bool
{
if ($post_id <= 0) {
return false;
}
$elementor_type = (string) get_post_meta($post_id, '_elementor_template_type', true);
if ('king-addons-woo-builder' === $elementor_type) {
return true;
}
return (string) get_post_meta($post_id, 'ka_woo_template_type', true) !== '';
}
/**
* Whether the post is a King Addons Theme Builder template.
*
* @param int $post_id
* @return bool
*/
private function isThemeBuilderDocument(int $post_id): bool
{
if ($post_id <= 0) {
return false;
}
$location_key = '_ka_theme_builder_location';
if (class_exists('\King_Addons\Theme_Builder\Meta_Keys')) {
$location_key = \King_Addons\Theme_Builder\Meta_Keys::LOCATION;
}
return (string) get_post_meta($post_id, $location_key, true) !== '';
}
/**
* Whether the post is a King Addons Loop Item template.
*
* @param int $post_id
* @return bool
*/
private function isLoopItemDocument(int $post_id): bool
{
if ($post_id <= 0) {
return false;
}
return 'king-addons-loop-item' === (string) get_post_meta($post_id, '_elementor_template_type', true);
}
/**
* Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
* that caused a fatal error previously, and try them again if the plugin version is updated.
*
* @param Widgets_Manager $widgets_manager
* @return void
*/
function registerWidgets(Widgets_Manager $widgets_manager): void
{
// Used to track which widget is currently being loaded when a fatal error occurs
static $currentlyLoadingWidgetId = null;
$currentPluginVersion = KING_ADDONS_VERSION;
// Get plugin options to check if a widget is enabled
$options = get_option('king_addons_options');
$options = is_array($options) ? $options : [];
// Extension toggles (used to prevent loading dependent widgets when extension is disabled).
$wishlist_extension_enabled = !isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled';
if (defined('KING_ADDONS_EXT_WISHLIST') && KING_ADDONS_EXT_WISHLIST === false) {
$wishlist_extension_enabled = false;
}
// Ensure Woo Builder base class is available for single product widgets.
$abstract_single_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Single_Widget.php';
if (file_exists($abstract_single_widget)) {
require_once $abstract_single_widget;
}
// Ensure Woo Builder base class is available for archive widgets.
$abstract_archive_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Archive_Widget.php';
if (file_exists($abstract_archive_widget)) {
require_once $abstract_archive_widget;
}
// Ensure Woo Builder base class is available for cart widgets.
$abstract_cart_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Cart_Widget.php';
if (file_exists($abstract_cart_widget)) {
require_once $abstract_cart_widget;
}
// Ensure Woo Builder base class is available for checkout widgets.
$abstract_checkout_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Checkout_Widget.php';
if (file_exists($abstract_checkout_widget)) {
require_once $abstract_checkout_widget;
}
// Ensure Woo Builder base class is available for My Account widgets.
$abstract_my_account_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_My_Account_Widget.php';
if (file_exists($abstract_my_account_widget)) {
require_once $abstract_my_account_widget;
}
/**
* Retrieve the array of broken widgets from the WordPress options.
* The structure is expected to be something like:
*
* 'widget_id' => [
* 'version' => '1.2.0',
* 'error' => 'Some fatal error message'
* ],
* ...
*
*/
$brokenWidgets = get_option('king_addons_broken_widgets', []);
/**
* STEP 1: Clear out any "broken widgets" where the stored version is
* less than the current plugin version. This gives them a second chance
* after an update, assuming the issue may have been fixed.
*/
foreach ($brokenWidgets as $brokenId => $brokenData) {
if (
isset($brokenData['version'])
&& version_compare($currentPluginVersion, $brokenData['version'], '>')
) {
// If the plugin version is now higher, we remove the widget from the blacklist
unset($brokenWidgets[$brokenId]);
}
}
// Update the option after cleaning up
update_option('king_addons_broken_widgets', $brokenWidgets);
/**
* STEP 2: Use register_shutdown_function to detect any fatal errors (E_ERROR, E_PARSE, etc.)
* that might occur during the loading of a widget. If an error is detected, store that widget
* in the "broken" list with the current plugin version and the error message.
*/
register_shutdown_function(function () use (&$currentlyLoadingWidgetId, $currentPluginVersion) {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
// If a fatal error occurred while loading a specific widget
if (!empty($currentlyLoadingWidgetId)) {
$brokenWidgetsLocal = get_option('king_addons_broken_widgets', []);
$brokenWidgetsLocal[$currentlyLoadingWidgetId] = [
'version' => $currentPluginVersion,
'error' => $error['message'] ?? ''
];
update_option('king_addons_broken_widgets', $brokenWidgetsLocal);
}
}
});
/**
* STEP 3: Now we iterate through all widgets in our modules map and try to load them.
* If a widget is in the broken list, we skip it to avoid repeated fatal errors.
*/
foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
// Hard-disable via constant (used to QA/rollout new widgets).
$widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
if (defined($widget_constant) && constant($widget_constant) === false) {
continue;
}
// Check if the widget is enabled in the options
if (!isset($options[$widget_id]) || $options[$widget_id] !== 'enabled') {
continue;
}
// Skip Wishlist widgets when Wishlist extension is disabled.
// This prevents fatals when wishlist classes aren't loaded.
if (!$wishlist_extension_enabled && strpos((string) $widget_id, 'wishlist-') === 0) {
continue;
}
// If this widget is listed as broken, skip it
if (array_key_exists($widget_id, $brokenWidgets)) {
// Log something here if needed:
// error_log("Skipping widget {$widget_id}, it previously caused a fatal error.");
continue;
}
// Track which widget we're loading
$currentlyLoadingWidgetId = $widget_id;
// Include the base widget class
$widget_class = $widget['php-class'];
$path_widget_class = "King_Addons\\" . $widget_class;
$widget_file = KING_ADDONS_PATH . 'includes/widgets/' . $widget_class . '/' . $widget_class . '.php';
if (!file_exists($widget_file)) {
// Skip missing widget files to avoid fatal errors if registry is ahead of implementation.
$currentlyLoadingWidgetId = null;
continue;
}
require_once $widget_file;
// Check if we can load the Pro version
if (
function_exists('king_addons_freemius')
&& king_addons_freemius()->can_use_premium_code__premium_only()
&& defined('KING_ADDONS_PRO_PATH')
) {
if (!empty($widget['has-pro'])) {
$pro_file_path = KING_ADDONS_PRO_PATH . 'includes/widgets/' . $widget_class . '_Pro/' . $widget_class . '_Pro.php';
if (file_exists($pro_file_path)) {
require_once($pro_file_path);
$path_widget_class_pro = "King_Addons\\" . $widget_class . '_Pro';
$widgets_manager->register(new $path_widget_class_pro);
} else {
// If Pro file doesn't exist, register the base widget
$widgets_manager->register(new $path_widget_class);
}
} else {
// No 'has-pro', register the base widget
$widgets_manager->register(new $path_widget_class);
}
} else {
// No Freemius Pro available, register the base widget
$widgets_manager->register(new $path_widget_class);
}
// Clear the tracking variable after successful load
$currentlyLoadingWidgetId = null;
}
}
function enableWidgetsByDefault(): void
{
$options = get_option('king_addons_options');
foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
// Hard-disable via constant (used to QA/rollout new widgets).
$widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
if (defined($widget_constant) && constant($widget_constant) === false) {
continue;
}
if (!($options[$widget_id] ?? null)) {
$options[$widget_id] = 'enabled';
update_option('king_addons_options', $options);
}
}
}
/**
* Enable and bootstrap registered features.
*
* Loads free feature classes and, when available and licensed, their Pro counterparts.
*
* @return void
*/
public function enableFeatures(): void
{
$options = get_option('king_addons_options');
foreach (ModulesMap::getModulesMapArray()['features'] as $feature_id => $feature) {
// Hard-disable via constant (used to QA/rollout new features).
$feature_constant = 'KING_ADDONS_FEAT_' . strtoupper(str_replace('-', '_', (string) $feature_id));
if (defined($feature_constant) && constant($feature_constant) === false) {
continue;
}
if (!($options[$feature_id] ?? null)) {
$options[$feature_id] = 'enabled';
update_option('king_addons_options', $options);
}
if ($options[$feature_id] !== 'enabled') {
continue;
}
$feature_class = $feature['php-class'];
$path_feature_class = "King_Addons\\" . $feature_class;
$feature_file = KING_ADDONS_PATH . 'includes/features/' . $feature_class . '/' . $feature_class . '.php';
if (file_exists($feature_file)) {
require_once $feature_file;
}
$pro_loaded = false;
if (
!empty($feature['has-pro'])
&& function_exists('king_addons_freemius')
&& king_addons_freemius()->can_use_premium_code__premium_only()
&& defined('KING_ADDONS_PRO_PATH')
) {
$pro_file_path = KING_ADDONS_PRO_PATH . 'includes/features/' . $feature_class . '_Pro/' . $feature_class . '_Pro.php';
if (file_exists($pro_file_path)) {
require_once $pro_file_path;
$path_feature_class_pro = "King_Addons\\" . $feature_class . '_Pro';
if (class_exists($path_feature_class_pro)) {
new $path_feature_class_pro();
$pro_loaded = true;
}
}
}
if (!$pro_loaded && class_exists($path_feature_class)) {
new $path_feature_class();
}
}
}
public function registerControls(Controls_Manager $controls_manager): void
{
$controls_manager->register(new AJAX_Select2\Ajax_Select2());
$controls_manager->register(new Animations\Animations());
$controls_manager->register(new Animations\Animations_Alternative());
$controls_manager->register(new Button_Animations\Button_Animations());
}
function enqueueEditorStyles(): void
{
wp_enqueue_style(KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-editor', KING_ADDONS_URL . 'includes/admin/css/elementor-editor.css', '', KING_ADDONS_VERSION);
}
function enqueueEditorScripts(): void
{
wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-editor', KING_ADDONS_URL . 'includes/admin/js/elementor-editor.js', '', KING_ADDONS_VERSION);
// Localize script with PRO status
wp_localize_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-editor', 'kingAddonsEditor', [
'isPro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false
]);
wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-data-table-export', KING_ADDONS_URL . 'includes/widgets/Data_Table/preview-handler.js', '', KING_ADDONS_VERSION);
if (KING_ADDONS_WGT_FORM_BUILDER) {
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);
}
}
/**
* Widget settings with dynamic tags resolved, and nothing missing.
*
* get_settings() never parses dynamic tags, so a tag placed in such a
* widget printed its placeholder instead of the value. get_settings_for_display()
* does parse them, but it also drops every setting whose control condition
* is currently false - and widgets written against the raw array index into
* those keys without checking, which turns the switch into a batch of
* "Trying to access array offset on null" warnings.
*
* A hidden control comes back as null rather than missing, so the parsed
* value is preferred only when there is one, and the raw value fills in
* everywhere else.
*
* @param \Elementor\Controls_Stack $widget Widget being rendered.
*
* @return array
*/
public static function displaySettings($widget): array
{
$raw = (array) $widget->get_settings();
$display = (array) $widget->get_settings_for_display();
foreach ($display as $key => $value) {
if (null !== $value) {
$raw[$key] = $value;
}
}
return $raw;
}
public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
{
if (king_addons_freemius()->can_use_premium_code__premium_only()) {
return;
}
$module->start_controls_section(
'king_addons_pro_features_section',
[
'label' => KING_ADDONS_ELEMENTOR_ICON_PRO . '' . esc_html__('Pro Features', 'king-addons') . '',
'tab' => $section ?: null,
]
);
$list_html = '
' . implode('', array_map(fn($feature) => "
$feature
", $features)) . '
';
$module->add_control(
'king_addons_pro_features_list',
[
'type' => $type,
'raw' => $list_html . '' . esc_html__('Upgrade Now', 'king-addons') . '',
'content_classes' => 'king-addons-pro-features-list',
]
);
$module->end_controls_section();
}
/**
* Same upgrade notice as renderUpgradeProNotice(), but not tied to another
* control's value. Use it where a whole section is Pro-only, so the free
* build does not show a section header that opens onto nothing.
*
* @param mixed $module Widget or repeater the control belongs to.
* @param string $controls_manager Control type to render the notice with.
* @param string $widget_name Widget slug, used for the campaign link.
* @param string $control_id Unique control id for the notice.
*/
public static function renderUpgradeProSection($module, $controls_manager, string $widget_name, string $control_id): void
{
if (king_addons_freemius()->can_use_premium_code__premium_only()) {
return;
}
$module->add_control(
$control_id,
[
'raw' => 'Upgrade to the Pro version now and unlock this feature!',
'type' => $controls_manager,
'content_classes' => 'king-addons-pro-notice',
]
);
}
public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
{
if (king_addons_freemius()->can_use_premium_code__premium_only()) {
return;
}
$module->add_control(
$option . '_pro_notice_',
[
'raw' => 'Upgrade to the Pro version now and unlock this feature!',
'type' => $controls_manager,
'content_classes' => 'king-addons-pro-notice',
'condition' => [
$option => $condition,
]
]
);
}
public static function getCustomTypes($query, $exclude_defaults = true): array
{
$custom_types = $query === 'tax'
? get_taxonomies(['show_in_nav_menus' => true], 'objects')
: get_post_types(['show_in_nav_menus' => true], 'objects');
return array_filter(
array_map(fn($type) => $type->label, $custom_types),
fn($label, $key) => !$exclude_defaults || !in_array($key, ['post', 'page', 'category', 'post_tag']),
ARRAY_FILTER_USE_BOTH
);
}
public static function getShareIcon($args = []): string
{
$args = wp_parse_args($args, [
'network' => '',
'url' => '',
'title' => '',
'text' => '',
'image' => '',
'show_whatsapp_title' => 'no',
'show_whatsapp_excerpt' => 'no',
'tooltip' => 'no',
'icons' => 'no',
'labels' => 'no',
'custom_label' => '',
]);
$url = esc_url($args['url']);
$title = wp_strip_all_tags($args['title']);
$text = wp_strip_all_tags($args['text']);
$image = esc_url($args['image']);
$network = $args['network'];
$get_whatsapp_url = function ($a) {
if ('yes' === $a['show_whatsapp_title'] && 'yes' === $a['show_whatsapp_excerpt']) {
return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['text'] . '%0a' . $a['url'];
} elseif ('yes' === $a['show_whatsapp_title']) {
return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['url'];
} elseif ('yes' === $a['show_whatsapp_excerpt']) {
return 'https://api.whatsapp.com/send?text=*' . $a['text'] . '%0a' . $a['url'];
}
return 'https://api.whatsapp.com/send?text=' . $a['url'];
};
$networks_map = [
'facebook-f' => [
'url' => "https://www.facebook.com/sharer.php?u=$url",
'title' => esc_html__('Facebook', 'king-addons'),
'icon' => 'fab',
],
'x-twitter' => [
'url' => "https://twitter.com/intent/tweet?url=$url",
'title' => esc_html__('X (Twitter)', 'king-addons'),
'icon' => 'fab',
],
'linkedin-in' => [
'url' => "https://www.linkedin.com/shareArticle?mini=true&url=$url&title=$title&summary=$text&source=$url",
'title' => esc_html__('LinkedIn', 'king-addons'),
'icon' => 'fab',
],
'pinterest-p' => [
'url' => "https://www.pinterest.com/pin/create/button/?url=$url&media=$image",
'title' => esc_html__('Pinterest', 'king-addons'),
'icon' => 'fab',
],
'reddit' => [
'url' => "https://reddit.com/submit?url=$url&title=$title",
'title' => esc_html__('Reddit', 'king-addons'),
'icon' => 'fab',
],
'tumblr' => [
'url' => "https://tumblr.com/share/link?url=$url",
'title' => esc_html__('Tumblr', 'king-addons'),
'icon' => 'fab',
],
'digg' => [
'url' => "https://digg.com/submit?url=$url",
'title' => esc_html__('Digg', 'king-addons'),
'icon' => 'fab',
],
'xing' => [
'url' => "https://www.xing.com/app/user?op=share&url=$url",
'title' => esc_html__('Xing', 'king-addons'),
'icon' => 'fab',
],
'vk' => [
'url' => "https://vk.ru/share.php?url=$url&title=$title&description=" . wp_trim_words($text, 250) . "&image=$image/",
'title' => esc_html__('VK', 'king-addons'),
'icon' => 'fab',
],
'odnoklassniki' => [
'url' => "https://connect.ok.ru/offer?url=$url",
'title' => esc_html__('OK', 'king-addons'),
'icon' => 'fab',
],
'get-pocket' => [
'url' => "https://getpocket.com/edit?url=$url",
'title' => esc_html__('Pocket', 'king-addons'),
'icon' => 'fab',
],
'skype' => [
'url' => "https://web.skype.com/share?url=$url",
'title' => esc_html__('Skype', 'king-addons'),
'icon' => 'fab',
],
'whatsapp' => [
'url' => $get_whatsapp_url($args),
'title' => esc_html__('WhatsApp', 'king-addons'),
'icon' => 'fab',
],
'telegram' => [
'url' => "https://telegram.me/share/url?url=$url&text=$text",
'title' => esc_html__('Telegram', 'king-addons'),
'icon' => 'fab',
],
'envelope' => [
'url' => "mailto:?subject=$title&body=$url",
'title' => esc_html__('Email', 'king-addons'),
'icon' => 'fas',
],
'print' => [
'url' => "javascript:window.print()",
'title' => esc_html__('Print', 'king-addons'),
'icon' => 'fas',
],
];
if (!isset($networks_map[$network])) {
return '';
}
$share_url = $networks_map[$network]['url'];
$network_title = $networks_map[$network]['title'];
$icon_category = $networks_map[$network]['icon'];
$output = '';
if ('yes' === $args['tooltip']) {
$output .= '' . esc_html($network_title) . '';
}
if ('yes' === $args['icons']) {
$output .= '';
}
if ('yes' === $args['labels']) {
$label = !empty($args['custom_label']) ? $args['custom_label'] : $network_title;
$output .= '' . esc_html($label) . '';
}
$output .= '';
return $output;
}
public static function validateHTMLTags($setting, $default, $tags_whitelist)
{
$value = $setting;
if (!in_array($value, $tags_whitelist)) {
$value = $default;
}
return $value;
}
public static function getIcon($icon, $dir)
{
if (empty($icon) || strpos($icon, 'fa-') === false) {
return '';
}
$dir = $dir ? "-$dir" : '';
return wp_kses(
'',
['i' => ['class' => []]]
);
}
public static function getPluginName()
{
return 'King Addons';
}
public static function getAnimationTimings(): array
{
/** @noinspection DuplicatedCode */
$timings = [
'ease-default' => 'Default',
'linear' => 'Linear',
'ease-in' => 'Ease In',
'ease-out' => 'Ease Out',
'pro-eio' => 'EI Out (Pro)',
'pro-eiqd' => 'EI Quad (Pro)',
'pro-eicb' => 'EI Cubic (Pro)',
'pro-eiqrt' => 'EI Quart (Pro)',
'pro-eiqnt' => 'EI Quint (Pro)',
'pro-eisn' => 'EI Sine (Pro)',
'pro-eiex' => 'EI Expo (Pro)',
'pro-eicr' => 'EI Circ (Pro)',
'pro-eibk' => 'EI Back (Pro)',
'pro-eoqd' => 'EO Quad (Pro)',
'pro-eocb' => 'EO Cubic (Pro)',
'pro-eoqrt' => 'EO Quart (Pro)',
'pro-eoqnt' => 'EO Quint (Pro)',
'pro-eosn' => 'EO Sine (Pro)',
'pro-eoex' => 'EO Expo (Pro)',
'pro-eocr' => 'EO Circ (Pro)',
'pro-eobk' => 'EO Back (Pro)',
'pro-eioqd' => 'EIO Quad (Pro)',
'pro-eiocb' => 'EIO Cubic (Pro)',
'pro-eioqrt' => 'EIO Quart (Pro)',
'pro-eioqnt' => 'EIO Quint (Pro)',
'pro-eiosn' => 'EIO Sine (Pro)',
'pro-eioex' => 'EIO Expo (Pro)',
'pro-eiocr' => 'EIO Circ (Pro)',
'pro-eiobk' => 'EIO Back (Pro)',
];
if (king_addons_freemius()->can_use_premium_code__premium_only()) {
/** @noinspection DuplicatedCode */
$timings = [
'ease-default' => 'Default',
'linear' => 'Linear',
'ease-in' => 'Ease In',
'ease-out' => 'Ease Out',
'ease-in-out' => 'Ease In Out',
'ease-in-quad' => 'Ease In Quad',
'ease-in-cubic' => 'Ease In Cubic',
'ease-in-quart' => 'Ease In Quart',
'ease-in-quint' => 'Ease In Quint',
'ease-in-sine' => 'Ease In Sine',
'ease-in-expo' => 'Ease In Expo',
'ease-in-circ' => 'Ease In Circ',
'ease-in-back' => 'Ease In Back',
'ease-out-quad' => 'Ease Out Quad',
'ease-out-cubic' => 'Ease Out Cubic',
'ease-out-quart' => 'Ease Out Quart',
'ease-out-quint' => 'Ease Out Quint',
'ease-out-sine' => 'Ease Out Sine',
'ease-out-expo' => 'Ease Out Expo',
'ease-out-circ' => 'Ease Out Circ',
'ease-out-back' => 'Ease Out Back',
'ease-in-out-quad' => 'Ease In Out Quad',
'ease-in-out-cubic' => 'Ease In Out Cubic',
'ease-in-out-quart' => 'Ease In Out Quart',
'ease-in-out-quint' => 'Ease In Out Quint',
'ease-in-out-sine' => 'Ease In Out Sine',
'ease-in-out-expo' => 'Ease In Out Expo',
'ease-in-out-circ' => 'Ease In Out Circ',
'ease-in-out-back' => 'Ease In Out Back',
];
}
return $timings;
}
public static function getAnimationTimingsConditionsPro()
{
return [
'pro-eibk',
'pro-eicb',
'pro-eicr',
'pro-eiex',
'pro-eio',
'pro-eiobk',
'pro-eiocb',
'pro-eiocr',
'pro-eioex',
'pro-eioqd',
'pro-eioqnt',
'pro-eioqrt',
'pro-eiosn',
'pro-eiqd',
'pro-eiqnt',
'pro-eiqrt',
'pro-eisn',
'pro-eobk',
'pro-eocb',
'pro-eocr',
'pro-eoex',
'pro-eoqd',
'pro-eoqnt',
'pro-eoqrt',
'pro-eosn',
];
}
public static function isBlogArchive()
{
return (
is_home()
&& '0' === get_option('page_on_front')
&& '0' === get_option('page_for_posts')
) || (
intval(get_option('page_for_posts')) === get_queried_object_id()
&& !is_404()
);
}
public static function filterOembedResults($html)
{
preg_match('/src="([^"]+)"/', $html, $m);
return $m[1] . '&auto_play=true';
}
public static function getWooCommerceTaxonomies()
{
$filtered = array_filter(get_object_taxonomies('product'), fn($t) => get_taxonomy($t)->show_ui);
return array_combine($filtered, array_map(fn($t) => get_taxonomy($t)->label, $filtered));
}
public static function getCustomMetaKeysTaxonomies()
{
$data = [];
$tax_types = Core::getCustomTypes('tax', false);
foreach ($tax_types as $taxonomy_slug => $post_type_name) {
$meta_keys = [];
foreach (get_terms($taxonomy_slug) as $tax) {
$keys = array_keys(get_term_meta($tax->term_id));
$keys = array_filter($keys, fn($key) => '_' !== $key[0]);
$meta_keys = array_merge($meta_keys, $keys);
}
$data[$taxonomy_slug] = array_unique($meta_keys);
}
$merged = call_user_func_array('array_merge', array_values($data));
$merged_meta_keys = array_values(array_unique($merged));
$options = array_combine($merged_meta_keys, $merged_meta_keys);
return [$data, $options];
}
public static function getMailchimpLists()
{
$api_key = get_option('king_addons_mailchimp_api_key', '');
$mailchimp_list = ['def' => esc_html__('Select List', 'king-addons')];
if (!$api_key) {
return $mailchimp_list;
}
$parts = explode('-', (string) $api_key);
if (count($parts) < 2 || '' === $parts[1]) {
return $mailchimp_list;
}
$url = 'https://' . $parts[1] . '.api.mailchimp.com/3.0/lists/';
$response = wp_remote_get($url, [
'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
]);
$body = json_decode(wp_remote_retrieve_body($response));
if (!empty($body->lists)) {
foreach ($body->lists as $list) {
$mailchimp_list[$list->id] = $list->name . ' (' . $list->stats->member_count . ')';
}
}
return $mailchimp_list;
}
public static function getMailchimpGroups()
{
$apiKey = (string) get_option('king_addons_mailchimp_api_key', '');
$groups = ['def' => 'Select Group'];
if ('' === $apiKey || false === strpos($apiKey, '-')) {
return $groups;
}
$dc = substr($apiKey, strpos($apiKey, '-') + 1);
if ('' === $dc) {
return $groups;
}
$domain = 'https://' . $dc . '.api.mailchimp.com/3.0/';
$authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
$mailchimpIDs = Core::getMailchimpLists();
foreach ($mailchimpIDs as $audience => $ignore) {
if ($audience === 'def') {
continue;
}
$cats_res = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
if (is_wp_error($cats_res)) {
continue;
}
$cats = json_decode((string) wp_remote_retrieve_body($cats_res))->categories ?? [];
foreach ($cats as $cat) {
$interests_res = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
if (is_wp_error($interests_res)) {
continue;
}
$interests = json_decode((string) wp_remote_retrieve_body($interests_res))->interests ?? [];
foreach ($interests as $int) {
$groups[$int->id] = $int->name;
}
}
}
return $groups;
}
public static function getShopURL($settings)
{
global $wp;
$url = ('' === get_option('permalink_structure'))
? remove_query_arg(['page', 'paged'], add_query_arg($wp->query_string, '', home_url($wp->request)))
: preg_replace('%/page/[0-9]+%', '', home_url(trailingslashit($wp->request)));
$url = add_query_arg('kingaddonsfilters', '', $url);
$single_params = [
'min_price' => true,
'max_price' => true,
'orderby' => false,
'psearch' => false,
'filter_product_cat' => false,
'filter_product_tag' => false,
'filter_rating' => false,
];
foreach ($single_params as $param => $needs_clean) {
if (isset($_GET[$param])) {
$value = wp_unslash($_GET[$param]);
$value = $needs_clean ? wc_clean($value) : $value;
$url = add_query_arg($param, $value, $url);
}
}
/** @noinspection DuplicatedCode */
if ($chosen_attrs = WC()->query->get_layered_nav_chosen_attributes()) {
foreach ($chosen_attrs as $name => $data) {
$filter_name = wc_attribute_taxonomy_slug($name);
if (!empty($data['terms'])) {
$url = add_query_arg('filter_' . $filter_name, implode(',', $data['terms']), $url);
}
if (!empty($settings)) {
if ('or' === $settings['tax_query_type'] || isset($_GET['query_type_' . $filter_name])) {
$url = add_query_arg('query_type_' . $filter_name, 'or', $url);
}
}
}
}
return $url;
}
public static function getClientIP()
{
$server_ip_keys = [
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR',
];
foreach ($server_ip_keys as $key) {
if (isset($_SERVER[$key])) {
$ip = wp_kses_post_deep(wp_unslash($_SERVER[$key]));
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return '127.0.0.1';
}
public static function getCustomMetaKeys()
{
// Get all custom post types (slug => name).
$post_types = Core::getCustomTypes('post', false);
// Build $data with each post type's unique custom meta keys (excluding keys beginning with "_").
$data = array_combine(
array_keys($post_types),
array_map(function ($slug) {
$keys = [];
foreach (get_posts(['post_type' => $slug, 'posts_per_page' => -1]) as $post) {
// get_post_custom_keys can return null, so cast to array:
foreach ((array) get_post_custom_keys($post->ID) as $meta_key) {
// Exclude protected keys (those beginning with "_").
if ($meta_key[0] !== '_') {
$keys[] = $meta_key;
}
}
}
return array_values(array_unique($keys));
}, array_keys($post_types))
);
// Flatten all meta keys across all post types, remove duplicates, and reindex.
$merged_meta_keys = array_values(array_unique(array_merge([], ...$data)));
// Create an associative array where key == value (for convenient dropdowns, etc.).
$options = array_combine($merged_meta_keys, $merged_meta_keys);
// Return both the per-post-type data and the merged, deduplicated options.
return [$data, $options];
}
public function enqueueLightboxDynamicStyles()
{
wp_register_style('king-addons-lightbox-dynamic-style', false);
wp_enqueue_style('king-addons-lightbox-dynamic-style');
$bg = esc_html(get_option('king_addons_lightbox_bg_color', 'rgba(0,0,0,0.6)'));
$toolbar = esc_html(get_option('king_addons_lightbox_toolbar_color', 'rgba(0,0,0,0.8)'));
$caption = esc_html(get_option('king_addons_lightbox_caption_color', 'rgba(0,0,0,0.8)'));
$gallery = esc_html(get_option('king_addons_lightbox_gallery_color', '#444444'));
$progress_bar = esc_html(get_option('king_addons_lightbox_pb_color', '#8a8a8a'));
$ui_color = esc_html(get_option('king_addons_lightbox_ui_color', '#efefef'));
$icon_size = floatval(get_option('king_addons_lightbox_icon_size', 20));
$icon_size_big = $icon_size + 4;
$ui_hover = esc_html(get_option('king_addons_lightbox_ui_hover_color', '#ffffff'));
$text_color = esc_html(get_option('king_addons_lightbox_text_color', '#efefef'));
$text_size = esc_html(get_option('king_addons_lightbox_text_size', 14));
$arrow_size = esc_html(get_option('king_addons_lightbox_arrow_size', 35));
$custom_css = "#lg-counter { color: $text_color !important; font-size: {$text_size}px !important; opacity: 0.9; } .lg-backdrop { background-color: $bg !important; } .lg-dropdown:after { border-bottom-color: $toolbar !important; } .lg-icon { color: $ui_color !important; font-size: {$icon_size}px !important; background-color: transparent !important; } .lg-icon.lg-toogle-thumb { font-size: {$icon_size_big}px !important; } .lg-icon:hover, .lg-dropdown-text:hover { color: $ui_hover !important; } .lg-prev, .lg-next { font-size: {$arrow_size}px !important; } .lg-progress { background-color: $progress_bar !important; } .lg-sub-html { background-color: $caption !important; } .lg-sub-html, .lg-dropdown-text { color: $text_color !important; font-size: {$text_size}px !important; } .lg-thumb-item { border-radius: 0 !important; border: none !important; opacity: 0.5; } .lg-thumb-item.active { opacity: 1; } .lg-thumb-outer, .lg-progress-bar { background-color: $gallery !important; } .lg-thumb-outer { padding: 0 10px; } .lg-toolbar, .lg-dropdown { background-color: $toolbar !important; }";
wp_add_inline_style('king-addons-lightbox-dynamic-style', $custom_css);
}
/**
* Enqueues the AI button injection script in the Elementor editor panel.
*
* @return void
*/
public function enqueueAiFieldScript(): void
{
$ai_options = get_option('king_addons_ai_options', []);
wp_enqueue_script(
'king-addons-ai-field',
KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
['jquery', 'elementor-editor'],
KING_ADDONS_VERSION,
true
);
// Localize for AJAX
wp_localize_script(
'king-addons-ai-field',
'KingAddonsAiField',
[
'ajax_url' => admin_url('admin-ajax.php'),
'generate_nonce' => wp_create_nonce('king_addons_ai_generate_nonce'),
'change_nonce' => wp_create_nonce('king_addons_ai_change_nonce'),
'generate_action' => 'king_addons_ai_generate_text',
'change_action' => 'king_addons_ai_change_text',
'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
'plugin_url' => KING_ADDONS_URL,
'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
'translator_enabled' => isset($ai_options['enable_ai_page_translator']) ? (bool) $ai_options['enable_ai_page_translator'] : true,
// Editor prompts name the configured provider rather than always OpenAI.
'provider' => \King_Addons\AI_Provider::getProvider(),
'provider_label' => \King_Addons\AI_Provider::getLabel(),
'api_keys_url' => \King_Addons\AI_Provider::getApiKeysUrl(),
'api_keys_label' => \King_Addons\AI_Provider::isOpenRouter()
? esc_html__('OpenRouter Keys', 'king-addons')
: esc_html__('OpenAI Platform', 'king-addons'),
'setup_billing_note' => \King_Addons\AI_Provider::isOpenRouter()
? esc_html__('Free models work without adding any credit.', 'king-addons')
: esc_html__('and top up your OpenAI account balance by at least $5', 'king-addons'),
'setup_cost_note' => \King_Addons\AI_Provider::isOpenRouter()
? esc_html__('Free models cost nothing. Paid models cost pennies (about $0.01 per full page).', 'king-addons')
: esc_html__('Processing a page costs pennies (about $0.01 per full page).', 'king-addons'),
/**
* Before either provider has a key there is no "configured
* provider" to describe - the setting is just its default. The
* setup dialog then has to present the choice instead of sending
* everyone to OpenAI.
*/
'has_any_key' => (
'' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENAI)
|| '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENROUTER)
),
'setup_providers' => [
[
'name' => esc_html__('OpenRouter', 'king-addons'),
'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENROUTER),
'note' => esc_html__('has free models, no credit needed to start', 'king-addons'),
],
[
'name' => esc_html__('OpenAI', 'king-addons'),
'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENAI),
'note' => esc_html__('needs at least $5 on your account balance', 'king-addons'),
],
],
'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'),
'missing_key_message' => sprintf(
/* translators: %s: provider name */
esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
\King_Addons\AI_Provider::getLabel()
),
]
);
}
/**
* Enqueues the AI image generation field script in the Elementor editor panel.
*
* @return void
*/
public function enqueueAiImageGenerationScript(): void
{
wp_enqueue_script(
'king-addons-ai-image-field',
KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
['jquery', 'elementor-editor'],
KING_ADDONS_VERSION,
true
);
// Localize for AJAX
wp_localize_script(
'king-addons-ai-image-field',
'KingAddonsAiImageField',
[
'ajax_url' => admin_url('admin-ajax.php'),
'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
'generate_action' => 'king_addons_ai_generate_image',
'image_model' => \King_Addons\AI_Provider::getImageModel(),
// The editor builds its model dropdown from this list, so the
// options follow whichever provider is configured.
'image_models' => array_map(
static function ($model) {
return ['value' => $model['id'], 'label' => $model['label']];
},
\King_Addons\AI_Provider::getModelsFor('image')
),
'provider' => \King_Addons\AI_Provider::getProvider(),
'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
'plugin_url' => KING_ADDONS_URL,
'missing_key_message' => sprintf(
/* translators: %s: provider name */
esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
\King_Addons\AI_Provider::getLabel()
),
]
);
}
/**
* Enqueues the styles for AI prompt UI in the Elementor editor panel.
*
* @return void
*/
public function enqueueAiFieldStyles(): void
{
// Enqueue CSS for the AI prompt UI
wp_enqueue_style(
'king-addons-ai-field-css',
KING_ADDONS_URL . 'includes/admin/css/ai-textfield.css',
[],
KING_ADDONS_VERSION
);
}
/**
* Enqueues styles for AI Image Generation UI in the Elementor editor panel.
*
* @return void
*/
public function enqueueAiImageFieldStyles(): void
{
wp_enqueue_style(
'king-addons-ai-imagefield',
KING_ADDONS_URL . 'includes/admin/css/ai-imagefield.css',
[],
KING_ADDONS_VERSION
);
}
/**
* Enqueues the AI page translator script in the Elementor editor panel.
*
* @return void
*/
public function enqueueAiTranslatorScript(): void
{
// Check if AI Page Translator is enabled in settings
$ai_options = get_option('king_addons_ai_options', []);
$translator_enabled = isset($ai_options['enable_ai_page_translator']) ? (bool) $ai_options['enable_ai_page_translator'] : true;
if (!$translator_enabled) {
return; // Don't load script if translator is disabled
}
wp_enqueue_script(
'king-addons-ai-translator',
KING_ADDONS_URL . 'includes/admin/js/ai-page-translator.js',
['jquery', 'elementor-editor'],
KING_ADDONS_VERSION,
true
);
// Note: Using existing KingAddonsAiField localization
// The translator script will use the same AJAX endpoints and settings
// No need for separate localization as it reuses existing AI infrastructure
}
/**
* Render an attachment through an Elementor image-size group control.
*
* Group_Control_Image_Size::get_attachment_image_html() takes a *key into
* $settings* as its third argument, not an attachment ID. Handing it an ID
* makes Elementor read $settings[], which raises two PHP warnings and
* returns nothing, so callers silently lose the image.
*
* @param array $settings Widget settings; carries "<$size_key>_size" and friends.
* @param string $size_key Name of the image-size group control.
* @param int $attachment_id Attachment to render.
*
* @return string Image HTML, or an empty string when it cannot be rendered.
*/
/**
* Show the person building the page why a widget rendered nothing.
*
* Only ever printed inside the Elementor editor, so the public page keeps
* rendering exactly what it rendered before.
*
* @param string $message Plain text explaining what the widget still needs.
*/
public static function renderEditorHint(string $message): void
{
if (!Plugin::$instance->editor->is_edit_mode()) {
return;
}
$style = 'display:block;padding:12px 14px;border:1px dashed #c3c4c7;border-radius:4px;'
. 'background:#f6f7f7;color:#50575e;font-size:13px;line-height:1.5;';
echo '
'
. esc_html($message) . '
';
}
/**
* A setting written straight into a JavaScript object literal has to be a
* number. Elementor stores a number field the user cleared as an empty
* string, which would emit `slidesPerView: ,` - a syntax error that stops
* the whole carousel from initialising - or blow up in PHP when the value
* is used in arithmetic.
*
* @param array $settings Widget settings.
* @param string $key Setting to read.
* @param int|float $fallback Value to use when the setting is empty or not a number.
*
* @return string Numeric string, safe to interpolate into JS.
*/
public static function jsNumber(array $settings, string $key, $fallback = 0): string
{
$value = $settings[$key] ?? null;
// Slider controls keep their number under 'size'.
if (is_array($value)) {
$value = $value['size'] ?? null;
}
if (!is_numeric($value)) {
$value = $fallback;
}
return (string) (0 + $value);
}
public static function getAttachmentImageHTML(array $settings, string $size_key, int $attachment_id): string
{
if ($attachment_id < 1) {
return '';
}
$image_key = '__king_addons_image';
$settings[$image_key] = [
'id' => $attachment_id,
'url' => wp_get_attachment_image_url($attachment_id, 'full') ?: '',
];
return (string) Group_Control_Image_Size::get_attachment_image_html($settings, $size_key, $image_key);
}
}
Core::instance();