'array', 'sanitize_callback' => [$this, 'sanitize_settings'], 'default' => $this->get_default_settings(), ] ); } /** * Register settings page. * * @return void */ public function register_settings_page(): void { $this->settings_page_hook = (string) add_submenu_page( 'king-addons', esc_html__('Custom Cursor', 'king-addons'), esc_html__('Custom Cursor', 'king-addons'), 'manage_options', 'king-addons-custom-cursor', [$this, 'render_settings_page'] ); } /** * Enqueue admin assets for the Custom Cursor settings page. * * @param string $hook_suffix Current admin page hook suffix. * * @return void */ public function enqueue_admin_assets(string $hook_suffix): void { if ('' === $this->settings_page_hook) { return; } if ($hook_suffix !== $this->settings_page_hook) { return; } // Required for wp.media modal. wp_enqueue_media(); } /** * Render admin settings page. * * @return void */ public function render_settings_page(): void { if (!current_user_can('manage_options')) { return; } $settings = $this->get_settings(); $is_pro = $this->can_use_pro(); // Theme mode is per-user $theme_mode = get_user_meta(get_current_user_id(), 'king_addons_theme_mode', true); $allowed_theme_modes = ['dark', 'light', 'auto']; if (!in_array($theme_mode, $allowed_theme_modes, true)) { $theme_mode = 'dark'; } // Enqueue shared V3 styles wp_enqueue_style( 'king-addons-admin-v3', KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css', [], KING_ADDONS_VERSION ); ?>

px
px
PRO
id="ka-cc-blur" /> px

PRO
/>
PRO

PRO
/>
PRO
/>
px
get_settings(); if (!$this->should_activate(false, $settings)) { return; } $style_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-style'; $script_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-script'; wp_enqueue_style( $style_handle, KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/style.css', [], KING_ADDONS_VERSION ); // Add inline CSS for cursor customization $inline_css = $this->generate_inline_css($settings); if ($inline_css) { wp_add_inline_style($style_handle, $inline_css); } wp_enqueue_script( $script_handle, KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/script.js', [], KING_ADDONS_VERSION, true ); wp_localize_script( $script_handle, 'KingAddonsCustomCursorData', $this->build_frontend_settings($settings, false) ); } /** * Generate inline CSS for cursor customization. * * @param array $settings Settings array. * * @return string CSS string. */ private function generate_inline_css(array $settings): string { $preset = $settings['preset']; $states = $settings['states']; $css = ':root {'; $css .= '--ka-cursor-size: ' . esc_attr($preset['size']) . 'px;'; $css .= '--ka-cursor-border-width: ' . esc_attr($preset['border_width']) . 'px;'; $css .= '--ka-cursor-fill: ' . esc_attr($preset['fill_color']) . ';'; $css .= '--ka-cursor-border-color: ' . esc_attr($preset['border_color']) . ';'; $css .= '--ka-cursor-opacity: ' . esc_attr($states['normal']['opacity']) . ';'; $css .= '--ka-cursor-scale: ' . esc_attr($states['normal']['scale']) . ';'; $css .= '--ka-cursor-blur: ' . esc_attr($preset['blur']) . 'px;'; $css .= '--ka-cursor-mix-blend: ' . esc_attr($preset['blend_mode'] ?: 'normal') . ';'; $css .= '}'; return $css; } /** * Enqueue assets in Elementor preview. * * @return void */ public function enqueue_preview_assets(): void { $settings = $this->get_settings(); $style_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-style'; $script_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-script'; $preview_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-preview'; wp_enqueue_style( $style_handle, KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/style.css', [], KING_ADDONS_VERSION ); wp_enqueue_script( $script_handle, KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/script.js', [], KING_ADDONS_VERSION, true ); wp_enqueue_script( $preview_handle, KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/preview-handler.js', ['elementor-frontend', $script_handle], KING_ADDONS_VERSION, true ); $localized = $this->build_frontend_settings($settings, true); wp_localize_script( $script_handle, 'KingAddonsCustomCursorData', $localized ); wp_localize_script( $preview_handle, 'KingAddonsCustomCursorPreview', [ 'enabled' => $localized['enabled'], ] ); } /** * Render cursor container. * * @return void */ public function render_cursor_markup(): void { $settings = $this->get_settings(); if (!$this->should_activate(false, $settings)) { return; } ?> $classes Body classes. * * @return array */ public function filter_body_classes(array $classes): array { $settings = $this->get_settings(); if ($this->should_activate(false, $settings)) { $classes[] = 'ka-custom-cursor-enabled'; if (!empty($settings['hide_original_cursor'])) { $classes[] = 'ka-cursor-hide-original'; } } return $classes; } /** * Register Elementor controls. * * @param Element_Base $element Elementor element. * @param array $args Args. * * @return void */ public function register_elementor_controls(Element_Base $element, $args): void { $element->start_controls_section( 'king_addons_custom_cursor_section', [ 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Custom Cursor', 'king-addons'), 'tab' => Controls_Manager::TAB_ADVANCED, ] ); $element->add_control( 'kng_cursor_interaction', [ 'label' => esc_html__('Cursor Interaction', 'king-addons'), 'type' => Controls_Manager::SELECT, 'options' => [ 'default' => esc_html__('Default', 'king-addons'), 'hover' => esc_html__('Hover', 'king-addons'), 'drag' => esc_html__('Drag', 'king-addons'), 'zoom' => esc_html__('Zoom', 'king-addons'), 'hide' => esc_html__('Hide Cursor', 'king-addons'), ], 'default' => 'default', 'frontend_available' => true, ] ); $element->add_control( 'kng_cursor_magnetic', [ 'label' => esc_html__('Magnetic Behavior', 'king-addons'), 'type' => Controls_Manager::SELECT, 'options' => [ 'none' => esc_html__('None', 'king-addons'), 'light' => esc_html__('Light', 'king-addons'), 'strong' => sprintf(__('Strong %s', 'king-addons'), ''), 'follow' => sprintf(__('Follow Center %s', 'king-addons'), ''), ], 'default' => 'none', 'frontend_available' => true, ] ); $element->add_control( 'kng_cursor_color_override', [ 'label' => esc_html__('Cursor Color', 'king-addons'), 'type' => Controls_Manager::COLOR, 'frontend_available' => true, 'condition' => [ 'kng_cursor_interaction!' => 'hide', ], ] ); $element->add_control( 'kng_cursor_size_multiplier', [ 'label' => esc_html__('Cursor Size Multiplier', 'king-addons'), 'type' => Controls_Manager::SLIDER, 'size_units' => ['px'], 'range' => [ 'px' => [ 'min' => 0.5, 'max' => 3, 'step' => 0.1, ], ], 'default' => [ 'size' => 1, ], 'frontend_available' => true, 'condition' => [ 'kng_cursor_interaction!' => 'hide', ], ] ); if (!$this->can_use_pro()) { $element->add_control( 'kng_cursor_pro_notice', [ 'type' => Controls_Manager::RAW_HTML, 'raw' => wp_kses_post('
' . esc_html__('Upgrade to Pro to unlock magnetic strength variations and advanced overrides.', 'king-addons') . '
'), ] ); } $element->end_controls_section(); } /** * Apply Elementor attributes for runtime. * * @param Element_Base $element Elementor element. * * @return void */ public function apply_elementor_attributes(Element_Base $element): void { $settings = $element->get_settings_for_display(); $interaction = $settings['kng_cursor_interaction'] ?? 'default'; if ($interaction && 'default' !== $interaction) { $element->add_render_attribute('_wrapper', 'data-ka-cursor', esc_attr($interaction)); } $magnetic = $settings['kng_cursor_magnetic'] ?? 'none'; if (!$this->can_use_pro() && in_array($magnetic, ['strong', 'follow'], true)) { $magnetic = 'none'; } if ('none' !== $magnetic) { $element->add_render_attribute('_wrapper', 'data-ka-magnetic', esc_attr($magnetic)); } if (!empty($settings['kng_cursor_color_override'])) { $element->add_render_attribute('_wrapper', 'data-ka-cursor-color', esc_attr($settings['kng_cursor_color_override'])); } if (!empty($settings['kng_cursor_size_multiplier']['size'])) { $multiplier = (float) $settings['kng_cursor_size_multiplier']['size']; if ($multiplier > 0) { $element->add_render_attribute('_wrapper', 'data-ka-cursor-size', esc_attr((string) $multiplier)); } } } /** * Sanitize settings. * * @param array $input Raw input. * * @return array */ public function sanitize_settings($input): array { $defaults = $this->get_default_settings(); $output = $defaults; $output['enabled'] = !empty($input['enabled']); $output['disable_on_mobile'] = !empty($input['disable_on_mobile']); $output['hide_original_cursor'] = !empty($input['hide_original_cursor']); $output['rendering_mode'] = in_array($input['rendering_mode'] ?? 'css', ['css', 'enhanced'], true) ? $input['rendering_mode'] : 'css'; $preset = $input['preset'] ?? []; $output['preset'] = [ 'type' => sanitize_text_field($preset['type'] ?? $defaults['preset']['type']), 'size' => $this->sanitize_float($preset['size'] ?? $defaults['preset']['size'], 4, 200, (float) $defaults['preset']['size']), 'border_width' => $this->sanitize_float($preset['border_width'] ?? $defaults['preset']['border_width'], 0, 20, (float) $defaults['preset']['border_width']), 'fill_color' => $this->sanitize_color($preset['fill_color'] ?? $defaults['preset']['fill_color'], $defaults['preset']['fill_color']), 'border_color' => $this->sanitize_color($preset['border_color'] ?? $defaults['preset']['border_color'], $defaults['preset']['border_color']), 'opacity' => $this->sanitize_float($preset['opacity'] ?? $defaults['preset']['opacity'], 0, 1, (float) $defaults['preset']['opacity']), 'blur' => $this->sanitize_float($preset['blur'] ?? $defaults['preset']['blur'], 0, 80, (float) $defaults['preset']['blur']), 'blend_mode' => sanitize_text_field($preset['blend_mode'] ?? $defaults['preset']['blend_mode']), ]; $states = $input['states'] ?? []; $output['states'] = [ 'normal' => [ 'scale' => $this->sanitize_float($states['normal']['scale'] ?? $defaults['states']['normal']['scale'], 0.1, 5, (float) $defaults['states']['normal']['scale']), 'opacity' => $this->sanitize_float($states['normal']['opacity'] ?? $defaults['states']['normal']['opacity'], 0, 1, (float) $defaults['states']['normal']['opacity']), ], 'hover_link' => [ 'scale' => $this->sanitize_float($states['hover_link']['scale'] ?? $defaults['states']['hover_link']['scale'], 0.5, 5, (float) $defaults['states']['hover_link']['scale']), 'color' => $this->sanitize_color($states['hover_link']['color'] ?? $defaults['states']['hover_link']['color'], $defaults['states']['hover_link']['color']), 'border_color' => $this->sanitize_color($states['hover_link']['border_color'] ?? $defaults['states']['hover_link']['border_color'], $defaults['states']['hover_link']['border_color']), 'label' => sanitize_text_field($states['hover_link']['label'] ?? $defaults['states']['hover_link']['label']), ], 'click' => [ 'ripple' => !empty($states['click']['ripple']), 'scale' => $this->sanitize_float($states['click']['scale'] ?? $defaults['states']['click']['scale'], 0.4, 2, (float) $defaults['states']['click']['scale']), ], ]; $targeting = $input['targeting'] ?? []; $output['targeting'] = [ 'exclude_logged_in' => !empty($targeting['exclude_logged_in']), 'include_pages' => $this->normalize_ids($targeting['include_pages'] ?? []), 'include_post_types' => $this->sanitize_list($targeting['include_post_types'] ?? []), 'include_urls' => $this->sanitize_list($targeting['include_urls'] ?? []), 'exclude_pages' => $this->normalize_ids($targeting['exclude_pages'] ?? []), 'exclude_post_types' => $this->sanitize_list($targeting['exclude_post_types'] ?? []), 'exclude_selectors' => sanitize_text_field($targeting['exclude_selectors'] ?? $defaults['targeting']['exclude_selectors']), ]; $image = $input['image'] ?? []; $output['image'] = [ 'url' => esc_url_raw($image['url'] ?? $defaults['image']['url']), 'size' => $this->sanitize_float($image['size'] ?? $defaults['image']['size'], 8, 256, (float) $defaults['image']['size']), 'hotspot_x' => $this->sanitize_float($image['hotspot_x'] ?? $defaults['image']['hotspot_x'], -400, 400, (float) $defaults['image']['hotspot_x']), 'hotspot_y' => $this->sanitize_float($image['hotspot_y'] ?? $defaults['image']['hotspot_y'], -400, 400, (float) $defaults['image']['hotspot_y']), 'retina' => !empty($image['retina']), ]; $magnetic = $input['magnetic'] ?? []; $output['magnetic'] = [ 'enabled' => !empty($magnetic['enabled']), 'strength' => $this->sanitize_float($magnetic['strength'] ?? $defaults['magnetic']['strength'], 0, 1, (float) $defaults['magnetic']['strength']), 'radius' => (int) $this->sanitize_float($magnetic['radius'] ?? $defaults['magnetic']['radius'], 20, 600, (float) $defaults['magnetic']['radius']), 'selectors' => sanitize_text_field($magnetic['selectors'] ?? $defaults['magnetic']['selectors']), ]; $movement = $input['movement'] ?? []; $tail = $movement['tail'] ?? []; $output['movement'] = [ 'follow_speed' => $this->sanitize_float($movement['follow_speed'] ?? $defaults['movement']['follow_speed'], 0.01, 1, (float) $defaults['movement']['follow_speed']), 'tail' => [ 'points' => (int) $this->sanitize_float($tail['points'] ?? $defaults['movement']['tail']['points'], 0, 16, (float) $defaults['movement']['tail']['points']), 'decay' => $this->sanitize_float($tail['decay'] ?? $defaults['movement']['tail']['decay'], 0.1, 0.99, (float) $defaults['movement']['tail']['decay']), ], ]; return $this->apply_license_limitations($output); } /** * Get settings merged with defaults and license limitations. * * @return array */ public function get_settings(): array { $saved = get_option(self::OPTION_KEY, []); if (!is_array($saved)) { $saved = []; } $merged = wp_parse_args($saved, $this->get_default_settings()); return $this->apply_license_limitations($merged); } /** * Build settings passed to frontend. * * @param array $settings Settings. * @param bool $is_preview Preview mode. * * @return array */ private function build_frontend_settings(array $settings, bool $is_preview): array { $enabled = $this->should_activate($is_preview, $settings); return [ 'enabled' => $enabled, 'mode' => $settings['rendering_mode'], 'hideOriginalCursor' => !empty($settings['hide_original_cursor']), 'preset' => $settings['preset'], 'states' => $settings['states'], 'image' => $settings['image'], 'magnetic' => $settings['magnetic'], 'movement' => $settings['movement'], 'targeting' => [ 'excludeSelectors' => $settings['targeting']['exclude_selectors'], ], 'selectors' => [ 'hover' => 'a,button,.ka-hoverable', 'attribute' => '[data-ka-cursor]', 'magnetic' => '[data-ka-magnetic]', ], 'bodyClass' => 'ka-custom-cursor-enabled', 'isPro' => $this->can_use_pro(), 'isPreview' => $is_preview, ]; } /** * Determine if cursor should activate on current request. * * @param bool $is_preview Preview flag. * @param array|null $settings Settings. * * @return bool */ private function should_activate(bool $is_preview = false, ?array $settings = null): bool { $settings = $settings ?? $this->get_settings(); if (empty($settings['enabled'])) { return false; } if (!$is_preview && is_admin()) { return false; } if (!$is_preview && !empty($settings['disable_on_mobile']) && wp_is_mobile()) { return false; } if (!$is_preview && !empty($settings['targeting']['exclude_logged_in']) && is_user_logged_in()) { return false; } if (!$is_preview && class_exists(Plugin::class) && Plugin::instance()->editor->is_edit_mode()) { return false; } if (!$this->can_use_pro()) { return true; } $page_id = get_queried_object_id(); if (!empty($settings['targeting']['include_pages']) && !in_array((int) $page_id, $settings['targeting']['include_pages'], true)) { return false; } $post_type = get_post_type($page_id); if (!empty($settings['targeting']['include_post_types']) && !in_array((string) $post_type, $settings['targeting']['include_post_types'], true)) { return false; } if (!empty($settings['targeting']['include_urls'])) { $current_url = $this->get_current_url(); $matched = false; foreach ($settings['targeting']['include_urls'] as $fragment) { if ('' !== $fragment && false !== stripos($current_url, $fragment)) { $matched = true; break; } } if (!$matched) { return false; } } if (!empty($settings['targeting']['exclude_pages']) && in_array((int) $page_id, $settings['targeting']['exclude_pages'], true)) { return false; } if (!empty($settings['targeting']['exclude_post_types']) && in_array((string) $post_type, $settings['targeting']['exclude_post_types'], true)) { return false; } return true; } /** * Get default settings. * * @return array */ private function get_default_settings(): array { return [ 'enabled' => false, 'disable_on_mobile' => true, 'hide_original_cursor' => true, 'rendering_mode' => 'css', 'preset' => [ 'type' => 'dot', 'size' => 14, 'border_width' => 2, 'fill_color' => '#111111', 'border_color' => '#111111', 'opacity' => 0.9, 'blur' => 0, 'blend_mode' => 'normal', ], 'states' => [ 'normal' => [ 'scale' => 1, 'opacity' => 1, ], 'hover_link' => [ 'scale' => 1.25, 'color' => '#111111', 'border_color' => '#ffffff', 'label' => '', ], 'click' => [ 'ripple' => false, 'scale' => 0.9, ], ], 'targeting' => [ 'exclude_logged_in' => false, 'include_pages' => [], 'include_post_types' => [], 'include_urls' => [], 'exclude_pages' => [], 'exclude_post_types' => [], 'exclude_selectors' => '.elementor-editor-active,.no-cursor-zone', ], 'image' => [ 'url' => '', 'size' => 48, 'hotspot_x' => 0, 'hotspot_y' => 0, 'retina' => true, ], 'magnetic' => [ 'enabled' => false, 'strength' => 0.3, 'radius' => 140, 'selectors' => 'button,.ka-magnetic', ], 'movement' => [ 'follow_speed' => 0.18, 'tail' => [ 'points' => 0, 'decay' => 0.65, ], ], ]; } /** * Apply license limitations for free version. * * @param array $settings Settings. * * @return array */ private function apply_license_limitations(array $settings): array { if ($this->can_use_pro()) { return $settings; } // Free cursor types $allowed_presets = ['dot', 'ring', 'dot-ring', 'outline', 'crosshair', 'arrow']; if (!in_array($settings['preset']['type'], $allowed_presets, true)) { $settings['preset']['type'] = 'dot'; } $settings['preset']['blur'] = 0; $settings['preset']['blend_mode'] = 'normal'; $settings['states']['hover_link']['label'] = ''; $settings['states']['click']['ripple'] = false; $settings['image'] = $this->get_default_settings()['image']; $settings['magnetic']['enabled'] = false; $settings['magnetic']['selectors'] = 'button,.ka-magnetic'; $settings['movement']['follow_speed'] = 0.18; $settings['movement']['tail'] = [ 'points' => 0, 'decay' => 0.65, ]; $settings['targeting']['include_pages'] = []; $settings['targeting']['include_post_types'] = []; $settings['targeting']['include_urls'] = []; $settings['targeting']['exclude_pages'] = []; $settings['targeting']['exclude_post_types'] = []; $settings['targeting']['exclude_selectors'] = '.elementor-editor-active,.no-cursor-zone'; return $settings; } /** * Check license status. * * @return bool */ private function can_use_pro(): bool { return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only(); } /** * Normalize IDs input to integer array. * * @param mixed $value Raw ids. * * @return array */ private function normalize_ids($value): array { if (is_string($value)) { $value = explode(',', $value); } if (!is_array($value)) { return []; } $ids = []; foreach ($value as $id) { $id = (int) $id; if ($id > 0) { $ids[] = $id; } } return array_values(array_unique($ids)); } /** * Sanitize color string. * * @param string $value Raw value. * @param string $fallback Fallback. * * @return string */ private function sanitize_color(string $value, string $fallback): string { $value = trim($value); if ('' === $value) { return $fallback; } return sanitize_text_field($value); } /** * Sanitize float with bounds. * * @param mixed $value Raw value. * @param float $min Min value. * @param float $max Max value. * @param float $fallback Default. * * @return float */ private function sanitize_float($value, float $min, float $max, float $fallback): float { $float_val = (float) $value; if ($float_val < $min || $float_val > $max) { return $fallback; } return $float_val; } /** * Sanitize comma separated list or array. * * @param mixed $value Raw value. * * @return array */ private function sanitize_list($value): array { if (is_string($value)) { $value = explode(',', $value); } if (!is_array($value)) { return []; } $clean = []; foreach ($value as $item) { $item = sanitize_text_field($item); if ('' !== $item) { $clean[] = $item; } } return array_values(array_unique($clean)); } /** * Get current URL. * * @return string */ private function get_current_url(): string { $scheme = (!empty($_SERVER['HTTPS']) && 'off' !== $_SERVER['HTTPS']) ? 'https' : 'http'; $host = sanitize_text_field($_SERVER['HTTP_HOST'] ?? ''); $uri = sanitize_text_field($_SERVER['REQUEST_URI'] ?? ''); return $scheme . '://' . $host . $uri; } }