PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Custom_Cursor / Custom_Cursor.php

Custom_Cursor.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.49, at includes/extensions/Custom_Cursor/Custom_Cursor.php

1,861 lines 86.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Custom Cursor feature.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Element_Base;
12 use Elementor\Plugin;
13
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 /**
19 * Provides global custom cursor with Elementor integration.
20 */
21 class Custom_Cursor
22 {
23 /**
24 * Option key for storing settings.
25 */
26 public const OPTION_KEY = 'king_addons_custom_cursor_settings';
27
28 /**
29 * Constructor.
30 */
31 public function __construct()
32 {
33 add_action('admin_init', [$this, 'register_settings']);
34 // Use priority 15 to ensure the parent menu exists before adding this submenu
35 add_action('admin_menu', [$this, 'register_settings_page'], 15);
36 add_action('wp_enqueue_scripts', [$this, 'maybe_enqueue_assets']);
37 add_action('elementor/preview/enqueue_scripts', [$this, 'enqueue_preview_assets']);
38 add_action('wp_footer', [$this, 'render_cursor_markup']);
39 add_filter('body_class', [$this, 'filter_body_classes']);
40
41 add_action('elementor/element/common/_section_style/after_section_end', [$this, 'register_elementor_controls'], 10, 2);
42 add_action('elementor/element/section/section_advanced/after_section_end', [$this, 'register_elementor_controls'], 10, 2);
43 add_action('elementor/element/container/section_layout/after_section_end', [$this, 'register_elementor_controls'], 10, 2);
44 add_action('elementor/element/column/section_advanced/after_section_end', [$this, 'register_elementor_controls'], 10, 2);
45 add_action('elementor/frontend/widget/before_render', [$this, 'apply_elementor_attributes']);
46 add_action('elementor/frontend/section/before_render', [$this, 'apply_elementor_attributes']);
47 add_action('elementor/frontend/container/before_render', [$this, 'apply_elementor_attributes']);
48 add_action('elementor/frontend/column/before_render', [$this, 'apply_elementor_attributes']);
49 }
50
51 /**
52 * Register settings in WordPress.
53 *
54 * @return void
55 */
56 public function register_settings(): void
57 {
58 register_setting(
59 'king_addons_custom_cursor',
60 self::OPTION_KEY,
61 [
62 'type' => 'array',
63 'sanitize_callback' => [$this, 'sanitize_settings'],
64 'default' => $this->get_default_settings(),
65 ]
66 );
67 }
68
69 /**
70 * Register settings page.
71 *
72 * @return void
73 */
74 public function register_settings_page(): void
75 {
76 add_submenu_page(
77 'king-addons',
78 esc_html__('Custom Cursor', 'king-addons'),
79 esc_html__('Custom Cursor', 'king-addons'),
80 'manage_options',
81 'king-addons-custom-cursor',
82 [$this, 'render_settings_page']
83 );
84 }
85
86 /**
87 * Render admin settings page.
88 *
89 * @return void
90 */
91 public function render_settings_page(): void
92 {
93 if (!current_user_can('manage_options')) {
94 return;
95 }
96
97 $settings = $this->get_settings();
98 $is_pro = $this->can_use_pro();
99
100 // Theme mode is per-user
101 $theme_mode = get_user_meta(get_current_user_id(), 'king_addons_theme_mode', true);
102 $allowed_theme_modes = ['dark', 'light', 'auto'];
103 if (!in_array($theme_mode, $allowed_theme_modes, true)) {
104 $theme_mode = 'dark';
105 }
106
107 // Enqueue shared V3 styles
108 wp_enqueue_style(
109 'king-addons-admin-v3',
110 KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css',
111 [],
112 KING_ADDONS_VERSION
113 );
114
115 ?>
116 <script>
117 (function() {
118 const mode = '<?php echo esc_js($theme_mode); ?>';
119 const mql = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
120 const isDark = mode === 'auto' ? !!(mql && mql.matches) : mode === 'dark';
121 document.documentElement.classList.toggle('ka-v3-dark', isDark);
122 document.body.classList.add('ka-admin-v3');
123 document.body.classList.toggle('ka-v3-dark', isDark);
124 })();
125 </script>
126 <style>
127 /* Custom Cursor V3 Additional Styles */
128 .ka-cc-v3 {
129 display: flex;
130 gap: 30px;
131 max-width: 1400px;
132 }
133 .ka-cc-v3 .ka-cc-main {
134 flex: 1;
135 min-width: 0;
136 }
137
138 /* Color picker */
139 .ka-cc-v3 .ka-color-wrap {
140 display: flex;
141 align-items: center;
142 gap: 12px;
143 }
144 .ka-cc-v3 .ka-color-preview {
145 width: 40px;
146 height: 40px;
147 border-radius: 10px;
148 border: 2px solid rgba(0, 0, 0, 0.1);
149 cursor: pointer;
150 transition: all 0.2s;
151 }
152 body.ka-v3-dark .ka-cc-v3 .ka-color-preview {
153 border-color: rgba(255, 255, 255, 0.1);
154 }
155 .ka-cc-v3 .ka-color-preview:hover {
156 transform: scale(1.05);
157 }
158 .ka-cc-v3 .ka-color-input {
159 max-width: 120px !important;
160 }
161
162 /* Status badge */
163 .ka-status-badge {
164 display: inline-flex;
165 align-items: center;
166 gap: 8px;
167 padding: 6px 14px;
168 border-radius: 980px;
169 font-size: 13px;
170 font-weight: 500;
171 }
172 .ka-status-badge-dot {
173 width: 8px;
174 height: 8px;
175 border-radius: 50%;
176 }
177 .ka-status-badge.enabled {
178 background: rgba(168, 85, 247, 0.1);
179 color: #a855f7;
180 }
181 .ka-status-badge.enabled .ka-status-badge-dot {
182 background: #a855f7;
183 }
184 .ka-status-badge.disabled {
185 background: rgba(239, 68, 68, 0.1);
186 color: #ef4444;
187 }
188 .ka-status-badge.disabled .ka-status-badge-dot {
189 background: #ef4444;
190 }
191
192 /* Toggle override for purple */
193 .ka-cc-v3 .ka-toggle input:checked + .ka-toggle-slider {
194 background: #a855f7 !important;
195 }
196
197 /* Save button purple */
198 .ka-cc-v3 .ka-card-footer {
199 padding: 20px 28px;
200 background: rgba(168, 85, 247, 0.04);
201 border: 1px solid rgba(168, 85, 247, 0.1);
202 }
203 body.ka-v3-dark .ka-cc-v3 .ka-card-footer {
204 background: rgba(168, 85, 247, 0.08);
205 border-color: rgba(168, 85, 247, 0.2);
206 }
207 .ka-cc-v3 .ka-save-btn {
208 display: inline-flex;
209 align-items: center;
210 gap: 10px;
211 background: #a855f7;
212 color: #fff;
213 border: none;
214 padding: 14px 28px;
215 border-radius: 980px;
216 font-size: 15px;
217 font-weight: 400;
218 cursor: pointer;
219 transition: all 0.3s;
220 }
221 .ka-cc-v3 .ka-save-btn:hover {
222 background: #9333ea;
223 transform: scale(1.02);
224 }
225 .ka-cc-v3 .ka-save-btn .dashicons {
226 font-size: 18px;
227 width: 18px;
228 height: 18px;
229 }
230
231 /* Opacity range slider */
232 .ka-cc-v3 input[type="range"] {
233 -webkit-appearance: none;
234 height: 6px;
235 background: rgba(0, 0, 0, 0.1);
236 border-radius: 3px;
237 outline: none;
238 }
239 body.ka-v3-dark .ka-cc-v3 input[type="range"] {
240 background: rgba(255, 255, 255, 0.1);
241 }
242 .ka-cc-v3 input[type="range"]::-webkit-slider-thumb {
243 -webkit-appearance: none;
244 width: 18px;
245 height: 18px;
246 background: #a855f7;
247 border-radius: 50%;
248 cursor: pointer;
249 }
250
251 /* Preview sidebar */
252 .ka-cc-v3 .ka-cc-sidebar {
253 width: 340px;
254 flex-shrink: 0;
255 }
256 .ka-cc-v3 .ka-preview-card {
257 background: #fff;
258 border-radius: 20px;
259 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
260 position: sticky;
261 top: 50px;
262 overflow: hidden;
263 }
264 body.ka-v3-dark .ka-cc-v3 .ka-preview-card {
265 background: #1c1c1e;
266 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
267 }
268 .ka-cc-v3 .ka-preview-header {
269 padding: 18px 22px;
270 border-bottom: 1px solid rgba(0, 0, 0, 0.04);
271 font-size: 15px;
272 font-weight: 600;
273 color: #1d1d1f;
274 display: flex;
275 align-items: center;
276 gap: 10px;
277 }
278 body.ka-v3-dark .ka-cc-v3 .ka-preview-header {
279 color: #f5f5f7;
280 border-bottom-color: rgba(255, 255, 255, 0.06);
281 }
282 .ka-cc-v3 .ka-preview-header .dashicons {
283 color: #a855f7;
284 }
285 .ka-cc-v3 .ka-preview-area {
286 height: 280px;
287 display: flex;
288 align-items: center;
289 justify-content: center;
290 background: linear-gradient(135deg, #f5f5f7 0%, #e8e8ed 100%);
291 position: relative;
292 overflow: hidden;
293 }
294 body.ka-v3-dark .ka-cc-v3 .ka-preview-area {
295 background: linear-gradient(135deg, #2c2c2e 0%, #1c1c1e 100%);
296 }
297 .ka-cc-v3 .ka-preview-area::before {
298 content: '';
299 position: absolute;
300 inset: 0;
301 background-image: radial-gradient(rgba(0,0,0,0.1) 1px, transparent 1px);
302 background-size: 20px 20px;
303 opacity: 0.5;
304 }
305 body.ka-v3-dark .ka-cc-v3 .ka-preview-area::before {
306 background-image: radial-gradient(rgba(255,255,255,0.1) 1px, transparent 1px);
307 }
308 .ka-cc-v3 .ka-preview-cursor {
309 position: relative;
310 z-index: 1;
311 transition: all 0.15s ease;
312 }
313 .ka-cc-v3 .ka-preview-cursor-inner {
314 border-radius: 50%;
315 transition: all 0.15s ease;
316 }
317 .ka-cc-v3 .ka-preview-cursor-outer {
318 position: absolute;
319 inset: -4px;
320 border-radius: 50%;
321 transition: all 0.15s ease;
322 }
323 .ka-cc-v3 .ka-preview-cursor-outer2 {
324 position: absolute;
325 border-radius: 50%;
326 transition: all 0.15s ease;
327 display: none;
328 }
329 .ka-cc-v3 .ka-preview-crosshair {
330 position: absolute;
331 pointer-events: none;
332 display: none;
333 }
334 .ka-cc-v3 .ka-preview-crosshair-h,
335 .ka-cc-v3 .ka-preview-crosshair-v {
336 position: absolute;
337 background: currentColor;
338 }
339 .ka-cc-v3 .ka-preview-crosshair-h {
340 width: 100%;
341 height: 2px;
342 top: 50%;
343 left: 0;
344 transform: translateY(-50%);
345 }
346 .ka-cc-v3 .ka-preview-crosshair-v {
347 width: 2px;
348 height: 100%;
349 left: 50%;
350 top: 0;
351 transform: translateX(-50%);
352 }
353 .ka-cc-v3 .ka-preview-arrow {
354 display: none;
355 width: 0;
356 height: 0;
357 border-style: solid;
358 }
359 .ka-cc-v3 .ka-preview-info {
360 padding: 18px 22px;
361 border-top: 1px solid rgba(0, 0, 0, 0.04);
362 }
363 body.ka-v3-dark .ka-cc-v3 .ka-preview-info {
364 border-top-color: rgba(255, 255, 255, 0.06);
365 }
366 .ka-cc-v3 .ka-preview-info-row {
367 display: flex;
368 justify-content: space-between;
369 font-size: 13px;
370 padding: 5px 0;
371 }
372 .ka-cc-v3 .ka-preview-info-label {
373 color: #86868b;
374 }
375 .ka-cc-v3 .ka-preview-info-value {
376 color: #1d1d1f;
377 font-weight: 500;
378 }
379 body.ka-v3-dark .ka-cc-v3 .ka-preview-info-value {
380 color: #f5f5f7;
381 }
382
383 /* Responsive */
384 @media (max-width: 1200px) {
385 .ka-cc-v3 { flex-direction: column; }
386 .ka-cc-v3 .ka-cc-sidebar {
387 width: 100%;
388 order: -1;
389 }
390 .ka-cc-v3 .ka-preview-card { position: static; }
391 }
392 </style>
393
394 <div class="ka-admin-wrap ka-cc-v3">
395 <div class="ka-cc-main">
396 <!-- Header -->
397 <div class="ka-admin-header">
398 <div class="ka-admin-header-left">
399 <div class="ka-admin-header-icon purple">
400 <span class="dashicons dashicons-admin-customizer"></span>
401 </div>
402 <div>
403 <h1 class="ka-admin-title"><?php esc_html_e('Custom Cursor', 'king-addons'); ?></h1>
404 <p class="ka-admin-subtitle"><?php esc_html_e('Create stunning custom cursors', 'king-addons'); ?></p>
405 </div>
406 </div>
407 <div class="ka-admin-header-actions">
408 <span class="ka-status-badge <?php echo !empty($settings['enabled']) ? 'enabled' : 'disabled'; ?>">
409 <span class="ka-status-badge-dot"></span>
410 <?php echo !empty($settings['enabled']) ? esc_html__('Enabled', 'king-addons') : esc_html__('Disabled', 'king-addons'); ?>
411 </span>
412 <div class="ka-v3-segmented" id="ka-v3-theme-segment" role="radiogroup" aria-label="Theme" data-active="<?php echo esc_attr($theme_mode); ?>">
413 <span class="ka-v3-segmented-indicator" aria-hidden="true"></span>
414 <button type="button" class="ka-v3-segmented-btn" data-theme="light" aria-pressed="<?php echo $theme_mode === 'light' ? 'true' : 'false'; ?>"><?php esc_html_e('Light', 'king-addons'); ?></button>
415 <button type="button" class="ka-v3-segmented-btn" data-theme="dark" aria-pressed="<?php echo $theme_mode === 'dark' ? 'true' : 'false'; ?>"><?php esc_html_e('Dark', 'king-addons'); ?></button>
416 <button type="button" class="ka-v3-segmented-btn" data-theme="auto" aria-pressed="<?php echo $theme_mode === 'auto' ? 'true' : 'false'; ?>"><?php esc_html_e('Auto', 'king-addons'); ?></button>
417 </div>
418 </div>
419 </div>
420
421 <form action="<?php echo esc_url(admin_url('options.php')); ?>" method="post" id="ka-cc-form">
422 <?php settings_fields('king_addons_custom_cursor'); ?>
423
424 <!-- General Settings -->
425 <div class="ka-card">
426 <div class="ka-card-header">
427 <span class="dashicons dashicons-admin-generic"></span>
428 <h2><?php esc_html_e('General Settings', 'king-addons'); ?></h2>
429 </div>
430 <div class="ka-card-body">
431 <div class="ka-row">
432 <div class="ka-row-label"><?php esc_html_e('Enable Cursor', 'king-addons'); ?></div>
433 <div class="ka-row-field">
434 <label class="ka-toggle">
435 <input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[enabled]" value="1" <?php checked(!empty($settings['enabled'])); ?> id="ka-cc-enabled" />
436 <span class="ka-toggle-slider"></span>
437 <span class="ka-toggle-label"><?php esc_html_e('Show custom cursor on your website', 'king-addons'); ?></span>
438 </label>
439 </div>
440 </div>
441 <div class="ka-row">
442 <div class="ka-row-label"><?php esc_html_e('Hide on Mobile', 'king-addons'); ?></div>
443 <div class="ka-row-field">
444 <label class="ka-toggle">
445 <input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[disable_on_mobile]" value="1" <?php checked(!empty($settings['disable_on_mobile'])); ?> />
446 <span class="ka-toggle-slider"></span>
447 <span class="ka-toggle-label"><?php esc_html_e('Disable on touch devices', 'king-addons'); ?></span>
448 </label>
449 </div>
450 </div>
451 <div class="ka-row">
452 <div class="ka-row-label"><?php echo esc_html__('Rendering Mode', 'king-addons'); ?></div>
453 <div class="ka-row-field">
454 <select name="<?php echo esc_attr(self::OPTION_KEY); ?>[rendering_mode]" id="ka-cc-mode">
455 <option value="css" <?php selected($settings['rendering_mode'], 'css'); ?>><?php echo esc_html__('CSS Only (Faster)', 'king-addons'); ?></option>
456 <option value="enhanced" <?php selected($settings['rendering_mode'], 'enhanced'); ?>><?php echo esc_html__('Enhanced JS (Smoother)', 'king-addons'); ?></option>
457 </select>
458 </div>
459 </div>
460 </div>
461 </div>
462
463 <!-- Appearance -->
464 <div class="ka-card">
465 <div class="ka-card-header">
466 <span class="dashicons dashicons-art"></span>
467 <h2><?php echo esc_html__('Appearance', 'king-addons'); ?></h2>
468 </div>
469 <div class="ka-card-body">
470 <div class="ka-row">
471 <div class="ka-row-label"><?php echo esc_html__('Cursor Type', 'king-addons'); ?></div>
472 <div class="ka-row-field">
473 <select name="<?php echo esc_attr(self::OPTION_KEY); ?>[preset][type]" id="ka-cc-type">
474 <optgroup label="<?php echo esc_attr__('Basic', 'king-addons'); ?>">
475 <option value="dot" <?php selected($settings['preset']['type'], 'dot'); ?>><?php echo esc_html__('Dot', 'king-addons'); ?></option>
476 <option value="ring" <?php selected($settings['preset']['type'], 'ring'); ?>><?php echo esc_html__('Ring', 'king-addons'); ?></option>
477 <option value="dot-ring" <?php selected($settings['preset']['type'], 'dot-ring'); ?>><?php echo esc_html__('Dot + Ring', 'king-addons'); ?></option>
478 </optgroup>
479 <optgroup label="<?php echo esc_attr__('Advanced', 'king-addons'); ?>">
480 <option value="outline" <?php selected($settings['preset']['type'], 'outline'); ?>><?php echo esc_html__('Outline', 'king-addons'); ?></option>
481 <option value="crosshair" <?php selected($settings['preset']['type'], 'crosshair'); ?>><?php echo esc_html__('Crosshair', 'king-addons'); ?></option>
482 <option value="arrow" <?php selected($settings['preset']['type'], 'arrow'); ?>><?php echo esc_html__('Arrow', 'king-addons'); ?></option>
483 </optgroup>
484 <optgroup label="<?php echo esc_attr__('Effects', 'king-addons'); ?> <?php echo $is_pro ? '' : '(Pro)'; ?>">
485 <option value="glow" <?php selected($settings['preset']['type'], 'glow'); ?> <?php disabled(!$is_pro); ?>><?php echo esc_html__('Glow', 'king-addons'); ?></option>
486 <option value="soft-glow" <?php selected($settings['preset']['type'], 'soft-glow'); ?> <?php disabled(!$is_pro); ?>><?php echo esc_html__('Soft Glow', 'king-addons'); ?></option>
487 <option value="neon" <?php selected($settings['preset']['type'], 'neon'); ?> <?php disabled(!$is_pro); ?>><?php echo esc_html__('Neon', 'king-addons'); ?></option>
488 <option value="blend" <?php selected($settings['preset']['type'], 'blend'); ?> <?php disabled(!$is_pro); ?>><?php echo esc_html__('Blend (Invert)', 'king-addons'); ?></option>
489 <option value="blob" <?php selected($settings['preset']['type'], 'blob'); ?> <?php disabled(!$is_pro); ?>><?php echo esc_html__('Blob', 'king-addons'); ?></option>
490 <option value="dual" <?php selected($settings['preset']['type'], 'dual'); ?> <?php disabled(!$is_pro); ?>><?php echo esc_html__('Dual Circle', 'king-addons'); ?></option>
491 <option value="gradient" <?php selected($settings['preset']['type'], 'gradient'); ?> <?php disabled(!$is_pro); ?>><?php echo esc_html__('Gradient', 'king-addons'); ?></option>
492 </optgroup>
493 <optgroup label="<?php echo esc_attr__('Custom', 'king-addons'); ?> <?php echo $is_pro ? '' : '(Pro)'; ?>">
494 <option value="image" <?php selected($settings['preset']['type'], 'image'); ?> <?php disabled(!$is_pro); ?>><?php echo esc_html__('Image', 'king-addons'); ?></option>
495 </optgroup>
496 </select>
497 </div>
498 </div>
499 <div class="ka-row">
500 <div class="ka-row-label"><?php echo esc_html__('Size', 'king-addons'); ?></div>
501 <div class="ka-row-field">
502 <input type="number" name="<?php echo esc_attr(self::OPTION_KEY); ?>[preset][size]" value="<?php echo esc_attr((int) $settings['preset']['size']); ?>" min="4" max="200" id="ka-cc-size" /> <span style="color:#64748b">px</span>
503 </div>
504 </div>
505 <div class="ka-row">
506 <div class="ka-row-label"><?php echo esc_html__('Fill Color', 'king-addons'); ?></div>
507 <div class="ka-row-field">
508 <div class="ka-color-wrap">
509 <input type="color" class="ka-color-preview" value="<?php echo esc_attr($settings['preset']['fill_color']); ?>" id="ka-cc-fill-picker" />
510 <input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[preset][fill_color]" value="<?php echo esc_attr($settings['preset']['fill_color']); ?>" class="ka-color-input" id="ka-cc-fill" />
511 </div>
512 </div>
513 </div>
514 <div class="ka-row">
515 <div class="ka-row-label"><?php echo esc_html__('Border Width', 'king-addons'); ?></div>
516 <div class="ka-row-field">
517 <input type="number" name="<?php echo esc_attr(self::OPTION_KEY); ?>[preset][border_width]" value="<?php echo esc_attr((float) $settings['preset']['border_width']); ?>" min="0" max="20" step="0.5" id="ka-cc-border-width" /> <span style="color:#64748b">px</span>
518 </div>
519 </div>
520 <div class="ka-row">
521 <div class="ka-row-label"><?php echo esc_html__('Border Color', 'king-addons'); ?></div>
522 <div class="ka-row-field">
523 <div class="ka-color-wrap">
524 <input type="color" class="ka-color-preview" value="<?php echo esc_attr($settings['preset']['border_color']); ?>" id="ka-cc-border-picker" />
525 <input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[preset][border_color]" value="<?php echo esc_attr($settings['preset']['border_color']); ?>" class="ka-color-input" id="ka-cc-border" />
526 </div>
527 </div>
528 </div>
529 <div class="ka-row">
530 <div class="ka-row-label"><?php echo esc_html__('Opacity', 'king-addons'); ?></div>
531 <div class="ka-row-field">
532 <input type="range" min="0" max="1" step="0.05" value="<?php echo esc_attr((float) $settings['preset']['opacity']); ?>" id="ka-cc-opacity-range" style="width:150px;vertical-align:middle" />
533 <input type="number" name="<?php echo esc_attr(self::OPTION_KEY); ?>[preset][opacity]" value="<?php echo esc_attr((float) $settings['preset']['opacity']); ?>" min="0" max="1" step="0.05" id="ka-cc-opacity" style="width:60px;margin-left:10px" />
534 </div>
535 </div>
536 <div class="ka-row">
537 <div class="ka-row-label"><?php echo esc_html__('Blur', 'king-addons'); ?><?php if (!$is_pro): ?><span class="ka-pro-badge">PRO</span><?php endif; ?></div>
538 <div class="ka-row-field">
539 <input type="number" name="<?php echo esc_attr(self::OPTION_KEY); ?>[preset][blur]" value="<?php echo esc_attr((float) $settings['preset']['blur']); ?>" min="0" max="40" <?php disabled(!$is_pro); ?> id="ka-cc-blur" /> <span style="color:#64748b">px</span>
540 </div>
541 </div>
542 </div>
543 </div>
544
545 <!-- States -->
546 <div class="ka-card">
547 <div class="ka-card-header">
548 <span class="dashicons dashicons-superhero"></span>
549 <h2><?php echo esc_html__('Cursor States', 'king-addons'); ?></h2>
550 </div>
551 <div class="ka-card-body">
552 <div class="ka-row">
553 <div class="ka-row-label"><?php echo esc_html__('Normal Scale', 'king-addons'); ?></div>
554 <div class="ka-row-field">
555 <input type="number" name="<?php echo esc_attr(self::OPTION_KEY); ?>[states][normal][scale]" value="<?php echo esc_attr((float) $settings['states']['normal']['scale']); ?>" min="0.1" max="5" step="0.05" id="ka-cc-scale" />
556 </div>
557 </div>
558 <div class="ka-row">
559 <div class="ka-row-label"><?php echo esc_html__('Normal Opacity', 'king-addons'); ?></div>
560 <div class="ka-row-field">
561 <input type="number" name="<?php echo esc_attr(self::OPTION_KEY); ?>[states][normal][opacity]" value="<?php echo esc_attr((float) $settings['states']['normal']['opacity']); ?>" min="0" max="1" step="0.05" />
562 </div>
563 </div>
564 <div class="ka-row">
565 <div class="ka-row-label"><?php echo esc_html__('Hover Scale', 'king-addons'); ?></div>
566 <div class="ka-row-field">
567 <input type="number" name="<?php echo esc_attr(self::OPTION_KEY); ?>[states][hover_link][scale]" value="<?php echo esc_attr((float) $settings['states']['hover_link']['scale']); ?>" min="0.5" max="5" step="0.05" id="ka-cc-hover-scale" />
568 </div>
569 </div>
570 <div class="ka-row">
571 <div class="ka-row-label"><?php echo esc_html__('Hover Fill Color', 'king-addons'); ?></div>
572 <div class="ka-row-field">
573 <div class="ka-color-wrap">
574 <input type="color" class="ka-color-preview" value="<?php echo esc_attr($settings['states']['hover_link']['color']); ?>" id="ka-cc-hover-fill-picker" />
575 <input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[states][hover_link][color]" value="<?php echo esc_attr($settings['states']['hover_link']['color']); ?>" class="ka-color-input" id="ka-cc-hover-fill" />
576 </div>
577 </div>
578 </div>
579 <div class="ka-row">
580 <div class="ka-row-label"><?php echo esc_html__('Hover Border Color', 'king-addons'); ?></div>
581 <div class="ka-row-field">
582 <div class="ka-color-wrap">
583 <input type="color" class="ka-color-preview" value="<?php echo esc_attr($settings['states']['hover_link']['border_color']); ?>" id="ka-cc-hover-border-picker" />
584 <input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[states][hover_link][border_color]" value="<?php echo esc_attr($settings['states']['hover_link']['border_color']); ?>" class="ka-color-input" id="ka-cc-hover-border" />
585 </div>
586 </div>
587 </div>
588 <div class="ka-row">
589 <div class="ka-row-label"><?php echo esc_html__('Click Scale', 'king-addons'); ?></div>
590 <div class="ka-row-field">
591 <input type="number" name="<?php echo esc_attr(self::OPTION_KEY); ?>[states][click][scale]" value="<?php echo esc_attr((float) $settings['states']['click']['scale']); ?>" min="0.5" max="2" step="0.05" />
592 </div>
593 </div>
594 <div class="ka-row">
595 <div class="ka-row-label"><?php echo esc_html__('Hover Label', 'king-addons'); ?><?php if (!$is_pro): ?><span class="ka-pro-badge">PRO</span><?php endif; ?></div>
596 <div class="ka-row-field">
597 <input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[states][hover_link][label]" value="<?php echo esc_attr($settings['states']['hover_link']['label']); ?>" placeholder="<?php echo esc_attr__('View', 'king-addons'); ?>" <?php disabled(!$is_pro); ?> />
598 </div>
599 </div>
600 <div class="ka-row">
601 <div class="ka-row-label"><?php echo esc_html__('Click Ripple', 'king-addons'); ?><?php if (!$is_pro): ?><span class="ka-pro-badge">PRO</span><?php endif; ?></div>
602 <div class="ka-row-field">
603 <label class="ka-toggle">
604 <input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[states][click][ripple]" value="1" <?php checked(!empty($settings['states']['click']['ripple'])); ?> <?php disabled(!$is_pro); ?> />
605 <span class="ka-toggle-slider"></span>
606 <span class="ka-toggle-label"><?php echo esc_html__('Enable ripple animation on click', 'king-addons'); ?></span>
607 </label>
608 </div>
609 </div>
610 </div>
611 </div>
612
613 <!-- Targeting -->
614 <div class="ka-card">
615 <div class="ka-card-header">
616 <span class="dashicons dashicons-visibility"></span>
617 <h2><?php echo esc_html__('Targeting', 'king-addons'); ?></h2>
618 </div>
619 <div class="ka-card-body">
620 <div class="ka-row">
621 <div class="ka-row-label"><?php echo esc_html__('Hide for Logged-in', 'king-addons'); ?></div>
622 <div class="ka-row-field">
623 <label class="ka-toggle">
624 <input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[targeting][exclude_logged_in]" value="1" <?php checked(!empty($settings['targeting']['exclude_logged_in'])); ?> />
625 <span class="ka-toggle-slider"></span>
626 <span class="ka-toggle-label"><?php echo esc_html__('Hide cursor for logged-in users', 'king-addons'); ?></span>
627 </label>
628 </div>
629 </div>
630 <div class="ka-row">
631 <div class="ka-row-label"><?php echo esc_html__('Include Pages', 'king-addons'); ?><?php if (!$is_pro): ?><span class="ka-pro-badge">PRO</span><?php endif; ?></div>
632 <div class="ka-row-field">
633 <input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[targeting][include_pages]" value="<?php echo esc_attr(implode(',', $settings['targeting']['include_pages'])); ?>" placeholder="12, 45, 99" style="max-width:300px" <?php disabled(!$is_pro); ?> />
634 </div>
635 </div>
636 <div class="ka-row">
637 <div class="ka-row-label"><?php echo esc_html__('Exclude Selectors', 'king-addons'); ?><?php if (!$is_pro): ?><span class="ka-pro-badge">PRO</span><?php endif; ?></div>
638 <div class="ka-row-field">
639 <input type="text" name="<?php echo esc_attr(self::OPTION_KEY); ?>[targeting][exclude_selectors]" value="<?php echo esc_attr($settings['targeting']['exclude_selectors']); ?>" placeholder=".no-cursor, .modal" style="max-width:300px" <?php disabled(!$is_pro); ?> />
640 </div>
641 </div>
642 </div>
643 </div>
644
645 <!-- Hidden fields for other settings -->
646 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[preset][blend_mode]" value="<?php echo esc_attr($settings['preset']['blend_mode']); ?>" />
647 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[targeting][include_post_types]" value="<?php echo esc_attr(implode(',', $settings['targeting']['include_post_types'])); ?>" />
648 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[targeting][include_urls]" value="<?php echo esc_attr(implode(',', $settings['targeting']['include_urls'])); ?>" />
649 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[targeting][exclude_pages]" value="<?php echo esc_attr(implode(',', $settings['targeting']['exclude_pages'])); ?>" />
650 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[targeting][exclude_post_types]" value="<?php echo esc_attr(implode(',', $settings['targeting']['exclude_post_types'])); ?>" />
651 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[image][url]" value="<?php echo esc_attr($settings['image']['url']); ?>" />
652 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[image][size]" value="<?php echo esc_attr($settings['image']['size']); ?>" />
653 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[image][hotspot_x]" value="<?php echo esc_attr($settings['image']['hotspot_x']); ?>" />
654 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[image][hotspot_y]" value="<?php echo esc_attr($settings['image']['hotspot_y']); ?>" />
655 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[image][retina]" value="<?php echo !empty($settings['image']['retina']) ? '1' : ''; ?>" />
656 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[magnetic][enabled]" value="<?php echo !empty($settings['magnetic']['enabled']) ? '1' : ''; ?>" />
657 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[magnetic][strength]" value="<?php echo esc_attr($settings['magnetic']['strength']); ?>" />
658 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[magnetic][radius]" value="<?php echo esc_attr($settings['magnetic']['radius']); ?>" />
659 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[magnetic][selectors]" value="<?php echo esc_attr($settings['magnetic']['selectors']); ?>" />
660 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[movement][follow_speed]" value="<?php echo esc_attr($settings['movement']['follow_speed']); ?>" />
661 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[movement][tail][points]" value="<?php echo esc_attr($settings['movement']['tail']['points']); ?>" />
662 <input type="hidden" name="<?php echo esc_attr(self::OPTION_KEY); ?>[movement][tail][decay]" value="<?php echo esc_attr($settings['movement']['tail']['decay']); ?>" />
663
664 <div class="ka-card ka-card-footer">
665 <button type="submit" class="ka-save-btn">
666 <span class="dashicons dashicons-saved"></span>
667 <?php esc_html_e('Save Changes', 'king-addons'); ?>
668 </button>
669 </div>
670 </form>
671 </div>
672
673 <!-- Sidebar with Preview -->
674 <div class="ka-cc-sidebar">
675 <div class="ka-preview-card">
676 <div class="ka-preview-header">
677 <span class="dashicons dashicons-visibility"></span>
678 <?php esc_html_e('Live Preview', 'king-addons'); ?>
679 </div>
680 <div class="ka-preview-area" id="ka-cc-preview-area">
681 <div class="ka-preview-cursor" id="ka-cc-preview-cursor">
682 <div class="ka-preview-cursor-inner" id="ka-cc-preview-inner"></div>
683 <div class="ka-preview-cursor-outer" id="ka-cc-preview-outer"></div>
684 <div class="ka-preview-cursor-outer2" id="ka-cc-preview-outer2"></div>
685 <div class="ka-preview-crosshair" id="ka-cc-preview-crosshair">
686 <div class="ka-preview-crosshair-h"></div>
687 <div class="ka-preview-crosshair-v"></div>
688 </div>
689 <div class="ka-preview-arrow" id="ka-cc-preview-arrow"></div>
690 </div>
691 </div>
692 <div class="ka-preview-info">
693 <div class="ka-preview-info-row">
694 <span class="ka-preview-info-label"><?php esc_html_e('Type', 'king-addons'); ?></span>
695 <span class="ka-preview-info-value" id="ka-cc-info-type"><?php echo esc_html(ucfirst($settings['preset']['type'])); ?></span>
696 </div>
697 <div class="ka-preview-info-row">
698 <span class="ka-preview-info-label"><?php esc_html_e('Size', 'king-addons'); ?></span>
699 <span class="ka-preview-info-value" id="ka-cc-info-size"><?php echo esc_html($settings['preset']['size']); ?>px</span>
700 </div>
701 <div class="ka-preview-info-row">
702 <span class="ka-preview-info-label"><?php esc_html_e('Colors', 'king-addons'); ?></span>
703 <span class="ka-preview-info-value">
704 <span style="display:inline-block;width:14px;height:14px;border-radius:6px;background:<?php echo esc_attr($settings['preset']['fill_color']); ?>;vertical-align:middle;margin-right:4px;border:1px solid rgba(0,0,0,0.1)" id="ka-cc-info-fill"></span>
705 <span style="display:inline-block;width:14px;height:14px;border-radius:6px;background:<?php echo esc_attr($settings['preset']['border_color']); ?>;vertical-align:middle;border:1px solid rgba(0,0,0,0.1)" id="ka-cc-info-border"></span>
706 </span>
707 </div>
708 </div>
709 </div>
710 </div>
711 </div>
712
713 <script>
714 (function() {
715 const ajaxUrl = '<?php echo esc_url(admin_url('admin-ajax.php')); ?>';
716 const nonce = '<?php echo esc_js(wp_create_nonce('king_addons_dashboard_ui')); ?>';
717 const segment = document.getElementById('ka-v3-theme-segment');
718 const mql = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
719 let mode = (segment && segment.getAttribute('data-active') ? segment.getAttribute('data-active') : 'dark').toString();
720 let mqlHandler = null;
721
722 function saveUISetting(key, value) {
723 const body = new URLSearchParams();
724 body.set('action', 'king_addons_save_dashboard_ui');
725 body.set('nonce', nonce);
726 body.set('key', key);
727 body.set('value', value);
728 fetch(ajaxUrl, {
729 method: 'POST',
730 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
731 body: body.toString()
732 });
733 }
734
735 function updateSegment(activeMode) {
736 if (!segment) {
737 return;
738 }
739 segment.setAttribute('data-active', activeMode);
740 segment.querySelectorAll('.ka-v3-segmented-btn').forEach((btn) => {
741 const theme = btn.getAttribute('data-theme');
742 btn.setAttribute('aria-pressed', theme === activeMode ? 'true' : 'false');
743 });
744 }
745
746 function applyTheme(isDark) {
747 document.body.classList.toggle('ka-v3-dark', isDark);
748 document.documentElement.classList.toggle('ka-v3-dark', isDark);
749 }
750
751 function setThemeMode(nextMode, save) {
752 mode = nextMode;
753 updateSegment(nextMode);
754
755 if (mqlHandler && mql) {
756 if (mql.removeEventListener) {
757 mql.removeEventListener('change', mqlHandler);
758 } else if (mql.removeListener) {
759 mql.removeListener(mqlHandler);
760 }
761 mqlHandler = null;
762 }
763
764 if (nextMode === 'auto') {
765 applyTheme(!!(mql && mql.matches));
766 mqlHandler = (e) => {
767 if (mode !== 'auto') {
768 return;
769 }
770 applyTheme(!!e.matches);
771 };
772 if (mql) {
773 if (mql.addEventListener) {
774 mql.addEventListener('change', mqlHandler);
775 } else if (mql.addListener) {
776 mql.addListener(mqlHandler);
777 }
778 }
779 } else {
780 applyTheme(nextMode === 'dark');
781 }
782
783 if (save) {
784 saveUISetting('theme_mode', nextMode);
785 }
786 }
787
788 window.kaV3ToggleDark = function() {
789 const isDark = document.body.classList.contains('ka-v3-dark');
790 setThemeMode(isDark ? 'light' : 'dark', true);
791 };
792
793 if (segment) {
794 segment.addEventListener('click', function(e) {
795 const btn = e.target.closest('.ka-v3-segmented-btn');
796 if (!btn) return;
797 e.preventDefault();
798 setThemeMode((btn.getAttribute('data-theme') || 'dark').toString(), true);
799 });
800 }
801
802 if (segment) {
803 setThemeMode(mode, false);
804 }
805 })();
806 </script>
807
808 <script>
809 (function() {
810 'use strict';
811
812 const inner = document.getElementById('ka-cc-preview-inner');
813 const outer = document.getElementById('ka-cc-preview-outer');
814 const outer2 = document.getElementById('ka-cc-preview-outer2');
815 const crosshair = document.getElementById('ka-cc-preview-crosshair');
816 const arrow = document.getElementById('ka-cc-preview-arrow');
817 const previewArea = document.getElementById('ka-cc-preview-area');
818 const cursor = document.getElementById('ka-cc-preview-cursor');
819
820 // Input elements
821 const sizeInput = document.getElementById('ka-cc-size');
822 const fillInput = document.getElementById('ka-cc-fill');
823 const fillPicker = document.getElementById('ka-cc-fill-picker');
824 const borderInput = document.getElementById('ka-cc-border');
825 const borderPicker = document.getElementById('ka-cc-border-picker');
826 const borderWidthInput = document.getElementById('ka-cc-border-width');
827 const opacityInput = document.getElementById('ka-cc-opacity');
828 const opacityRange = document.getElementById('ka-cc-opacity-range');
829 const typeInput = document.getElementById('ka-cc-type');
830 const scaleInput = document.getElementById('ka-cc-scale');
831 const blurInput = document.getElementById('ka-cc-blur');
832
833 // Info elements
834 const infoType = document.getElementById('ka-cc-info-type');
835 const infoSize = document.getElementById('ka-cc-info-size');
836 const infoFill = document.getElementById('ka-cc-info-fill');
837 const infoBorder = document.getElementById('ka-cc-info-border');
838
839 // Type name mapping for display
840 const typeNames = {
841 'dot': 'Dot',
842 'ring': 'Ring',
843 'dot-ring': 'Dot + Ring',
844 'outline': 'Outline',
845 'crosshair': 'Crosshair',
846 'arrow': 'Arrow',
847 'glow': 'Glow',
848 'soft-glow': 'Soft Glow',
849 'neon': 'Neon',
850 'blend': 'Blend (Invert)',
851 'blob': 'Blob',
852 'dual': 'Dual Circle',
853 'gradient': 'Gradient',
854 'image': 'Image'
855 };
856
857 function updatePreview() {
858 const size = parseInt(sizeInput.value) || 14;
859 const fill = fillInput.value || '#111111';
860 const border = borderInput.value || '#111111';
861 const borderWidth = parseFloat(borderWidthInput.value) || 2;
862 const opacity = parseFloat(opacityInput.value) || 1;
863 const type = typeInput.value || 'dot';
864 const scale = parseFloat(scaleInput.value) || 1;
865 const blur = parseFloat(blurInput.value) || 0;
866
867 // Reset all elements
868 inner.style.display = 'none';
869 outer.style.display = 'none';
870 outer2.style.display = 'none';
871 crosshair.style.display = 'none';
872 arrow.style.display = 'none';
873 inner.style.boxShadow = 'none';
874 inner.style.background = fill;
875 inner.style.filter = 'none';
876 inner.style.borderRadius = '50%';
877 outer.style.background = 'none';
878 // Don't override CSS background with inline style - let CSS handle dark mode
879 previewArea.style.background = '';
880 cursor.style.mixBlendMode = 'normal';
881
882 // Base inner size
883 inner.style.width = size + 'px';
884 inner.style.height = size + 'px';
885 inner.style.opacity = opacity;
886 inner.style.transform = 'scale(' + scale + ')';
887
888 // Base outer ring
889 const outerSize = size + (borderWidth * 2) + 8;
890 outer.style.width = outerSize + 'px';
891 outer.style.height = outerSize + 'px';
892 outer.style.border = borderWidth + 'px solid ' + border;
893 outer.style.marginTop = '-' + (outerSize / 2) + 'px';
894 outer.style.marginLeft = '-' + (outerSize / 2) + 'px';
895 outer.style.top = '50%';
896 outer.style.left = '50%';
897 outer.style.opacity = '1';
898 outer.style.transform = 'none';
899
900 // Type-specific rendering
901 switch (type) {
902 case 'dot':
903 inner.style.display = 'block';
904 break;
905
906 case 'ring':
907 outer.style.display = 'block';
908 outer.style.background = 'transparent';
909 break;
910
911 case 'dot-ring':
912 inner.style.display = 'block';
913 outer.style.display = 'block';
914 break;
915
916 case 'outline':
917 inner.style.display = 'block';
918 inner.style.background = 'transparent';
919 inner.style.border = borderWidth + 'px solid ' + fill;
920 inner.style.width = (size - borderWidth * 2) + 'px';
921 inner.style.height = (size - borderWidth * 2) + 'px';
922 break;
923
924 case 'crosshair':
925 crosshair.style.display = 'block';
926 crosshair.style.width = size + 'px';
927 crosshair.style.height = size + 'px';
928 crosshair.style.color = fill;
929 crosshair.style.opacity = opacity;
930 // Small center dot
931 inner.style.display = 'block';
932 inner.style.width = '4px';
933 inner.style.height = '4px';
934 break;
935
936 case 'arrow':
937 arrow.style.display = 'block';
938 const arrowSize = size * 0.6;
939 arrow.style.borderWidth = arrowSize + 'px ' + (arrowSize * 0.6) + 'px 0 ' + (arrowSize * 0.6) + 'px';
940 arrow.style.borderColor = fill + ' transparent transparent transparent';
941 arrow.style.opacity = opacity;
942 arrow.style.transform = 'rotate(-45deg)';
943 break;
944
945 case 'glow':
946 inner.style.display = 'block';
947 inner.style.boxShadow = '0 0 ' + (size * 0.8) + 'px ' + (size * 0.3) + 'px ' + fill;
948 break;
949
950 case 'soft-glow':
951 inner.style.display = 'block';
952 inner.style.filter = 'blur(' + (size * 0.15) + 'px)';
953 inner.style.boxShadow = '0 0 ' + (size * 1.2) + 'px ' + (size * 0.5) + 'px ' + fill;
954 break;
955
956 case 'neon':
957 inner.style.display = 'block';
958 inner.style.background = '#fff';
959 inner.style.boxShadow =
960 '0 0 ' + (size * 0.3) + 'px ' + fill + ', ' +
961 '0 0 ' + (size * 0.6) + 'px ' + fill + ', ' +
962 '0 0 ' + (size * 1) + 'px ' + fill + ', ' +
963 '0 0 ' + (size * 1.5) + 'px ' + fill;
964 break;
965
966 case 'blend':
967 inner.style.display = 'block';
968 inner.style.background = '#ffffff';
969 inner.style.width = (size * 1.5) + 'px';
970 inner.style.height = (size * 1.5) + 'px';
971 cursor.style.mixBlendMode = 'difference';
972 previewArea.style.background = 'linear-gradient(135deg, #1e293b 0%, #334155 50%, #f1f5f9 50%, #e2e8f0 100%)';
973 break;
974
975 case 'blob':
976 inner.style.display = 'block';
977 inner.style.borderRadius = '60% 40% 30% 70% / 60% 30% 70% 40%';
978 inner.style.width = (size * 1.3) + 'px';
979 inner.style.height = (size * 1.3) + 'px';
980 break;
981
982 case 'dual':
983 inner.style.display = 'block';
984 outer.style.display = 'block';
985 outer2.style.display = 'block';
986 // Outer ring 1
987 outer.style.opacity = '0.6';
988 outer.style.transform = 'scale(1.3)';
989 // Outer ring 2
990 const outer2Size = outerSize * 1.6;
991 outer2.style.width = outer2Size + 'px';
992 outer2.style.height = outer2Size + 'px';
993 outer2.style.border = (borderWidth * 0.5) + 'px solid ' + border;
994 outer2.style.marginTop = '-' + (outer2Size / 2) + 'px';
995 outer2.style.marginLeft = '-' + (outer2Size / 2) + 'px';
996 outer2.style.top = '50%';
997 outer2.style.left = '50%';
998 outer2.style.opacity = '0.3';
999 break;
1000
1001 case 'gradient':
1002 inner.style.display = 'block';
1003 inner.style.background = 'linear-gradient(135deg, ' + fill + ' 0%, ' + border + ' 100%)';
1004 inner.style.width = (size * 1.2) + 'px';
1005 inner.style.height = (size * 1.2) + 'px';
1006 break;
1007
1008 case 'image':
1009 inner.style.display = 'block';
1010 inner.style.background = '#ddd';
1011 inner.style.border = '2px dashed #999';
1012 inner.style.borderRadius = '4px';
1013 break;
1014
1015 default:
1016 inner.style.display = 'block';
1017 }
1018
1019 // Apply blur for Pro types if set
1020 if (blur > 0 && !['soft-glow', 'blend'].includes(type)) {
1021 inner.style.filter = 'blur(' + blur + 'px)';
1022 }
1023
1024 // Update info
1025 infoType.textContent = typeNames[type] || type;
1026 infoSize.textContent = size + 'px';
1027 infoFill.style.background = fill;
1028 infoBorder.style.background = border;
1029 }
1030
1031 // Color picker sync
1032 fillPicker.addEventListener('input', function() {
1033 fillInput.value = this.value;
1034 updatePreview();
1035 });
1036 fillInput.addEventListener('input', function() {
1037 try { fillPicker.value = this.value; } catch(e) {}
1038 updatePreview();
1039 });
1040 borderPicker.addEventListener('input', function() {
1041 borderInput.value = this.value;
1042 updatePreview();
1043 });
1044 borderInput.addEventListener('input', function() {
1045 try { borderPicker.value = this.value; } catch(e) {}
1046 updatePreview();
1047 });
1048
1049 // Opacity range sync
1050 opacityRange.addEventListener('input', function() {
1051 opacityInput.value = this.value;
1052 updatePreview();
1053 });
1054 opacityInput.addEventListener('input', function() {
1055 opacityRange.value = this.value;
1056 updatePreview();
1057 });
1058
1059 // Hover color pickers
1060 const hoverFillPicker = document.getElementById('ka-cc-hover-fill-picker');
1061 const hoverFillInput = document.getElementById('ka-cc-hover-fill');
1062 const hoverBorderPicker = document.getElementById('ka-cc-hover-border-picker');
1063 const hoverBorderInput = document.getElementById('ka-cc-hover-border');
1064
1065 if (hoverFillPicker && hoverFillInput) {
1066 hoverFillPicker.addEventListener('input', function() {
1067 hoverFillInput.value = this.value;
1068 });
1069 hoverFillInput.addEventListener('input', function() {
1070 try { hoverFillPicker.value = this.value; } catch(e) {}
1071 });
1072 }
1073 if (hoverBorderPicker && hoverBorderInput) {
1074 hoverBorderPicker.addEventListener('input', function() {
1075 hoverBorderInput.value = this.value;
1076 });
1077 hoverBorderInput.addEventListener('input', function() {
1078 try { hoverBorderPicker.value = this.value; } catch(e) {}
1079 });
1080 }
1081
1082 // Bind all relevant inputs
1083 [sizeInput, borderWidthInput, typeInput, scaleInput, blurInput].forEach(function(el) {
1084 if (el) {
1085 el.addEventListener('input', updatePreview);
1086 el.addEventListener('change', updatePreview);
1087 }
1088 });
1089
1090 // Interactive preview - follow mouse
1091 let isHovering = false;
1092 let cursorX = 0, cursorY = 0;
1093 let targetX = 0, targetY = 0;
1094
1095 previewArea.addEventListener('mouseenter', function() {
1096 isHovering = true;
1097 });
1098 previewArea.addEventListener('mouseleave', function() {
1099 isHovering = false;
1100 cursor.style.transform = '';
1101 });
1102 previewArea.addEventListener('mousemove', function(e) {
1103 if (!isHovering) return;
1104 const rect = previewArea.getBoundingClientRect();
1105 targetX = e.clientX - rect.left - 20;
1106 targetY = e.clientY - rect.top - 20;
1107 });
1108
1109 // Smooth animation
1110 function animateCursor() {
1111 if (isHovering) {
1112 cursorX += (targetX - cursorX) * 0.15;
1113 cursorY += (targetY - cursorY) * 0.15;
1114 cursor.style.transform = 'translate(' + cursorX + 'px, ' + cursorY + 'px)';
1115 }
1116 requestAnimationFrame(animateCursor);
1117 }
1118 animateCursor();
1119
1120 // Initial update
1121 updatePreview();
1122 })();
1123 </script>
1124 <?php
1125 }
1126
1127 /**
1128 * Enqueue frontend assets if enabled.
1129 *
1130 * @return void
1131 */
1132 public function maybe_enqueue_assets(): void
1133 {
1134 $settings = $this->get_settings();
1135 if (!$this->should_activate(false, $settings)) {
1136 return;
1137 }
1138
1139 $style_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-style';
1140 $script_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-script';
1141
1142 wp_enqueue_style(
1143 $style_handle,
1144 KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/style.css',
1145 [],
1146 KING_ADDONS_VERSION
1147 );
1148
1149 // Add inline CSS for cursor customization
1150 $inline_css = $this->generate_inline_css($settings);
1151 if ($inline_css) {
1152 wp_add_inline_style($style_handle, $inline_css);
1153 }
1154
1155 wp_enqueue_script(
1156 $script_handle,
1157 KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/script.js',
1158 [],
1159 KING_ADDONS_VERSION,
1160 true
1161 );
1162
1163 wp_localize_script(
1164 $script_handle,
1165 'KingAddonsCustomCursorData',
1166 $this->build_frontend_settings($settings, false)
1167 );
1168 }
1169
1170 /**
1171 * Generate inline CSS for cursor customization.
1172 *
1173 * @param array<string,mixed> $settings Settings array.
1174 *
1175 * @return string CSS string.
1176 */
1177 private function generate_inline_css(array $settings): string
1178 {
1179 $preset = $settings['preset'];
1180 $states = $settings['states'];
1181
1182 $css = ':root {';
1183 $css .= '--ka-cursor-size: ' . esc_attr($preset['size']) . 'px;';
1184 $css .= '--ka-cursor-border-width: ' . esc_attr($preset['border_width']) . 'px;';
1185 $css .= '--ka-cursor-fill: ' . esc_attr($preset['fill_color']) . ';';
1186 $css .= '--ka-cursor-border-color: ' . esc_attr($preset['border_color']) . ';';
1187 $css .= '--ka-cursor-opacity: ' . esc_attr($states['normal']['opacity']) . ';';
1188 $css .= '--ka-cursor-scale: ' . esc_attr($states['normal']['scale']) . ';';
1189 $css .= '--ka-cursor-blur: ' . esc_attr($preset['blur']) . 'px;';
1190 $css .= '--ka-cursor-mix-blend: ' . esc_attr($preset['blend_mode'] ?: 'normal') . ';';
1191 $css .= '}';
1192
1193 return $css;
1194 }
1195
1196 /**
1197 * Enqueue assets in Elementor preview.
1198 *
1199 * @return void
1200 */
1201 public function enqueue_preview_assets(): void
1202 {
1203 $settings = $this->get_settings();
1204
1205 $style_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-style';
1206 $script_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-script';
1207 $preview_handle = KING_ADDONS_ASSETS_UNIQUE_KEY . '-custom-cursor-preview';
1208
1209 wp_enqueue_style(
1210 $style_handle,
1211 KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/style.css',
1212 [],
1213 KING_ADDONS_VERSION
1214 );
1215
1216 wp_enqueue_script(
1217 $script_handle,
1218 KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/script.js',
1219 [],
1220 KING_ADDONS_VERSION,
1221 true
1222 );
1223
1224 wp_enqueue_script(
1225 $preview_handle,
1226 KING_ADDONS_URL . 'includes/extensions/Custom_Cursor/assets/preview-handler.js',
1227 ['elementor-frontend', $script_handle],
1228 KING_ADDONS_VERSION,
1229 true
1230 );
1231
1232 $localized = $this->build_frontend_settings($settings, true);
1233
1234 wp_localize_script(
1235 $script_handle,
1236 'KingAddonsCustomCursorData',
1237 $localized
1238 );
1239
1240 wp_localize_script(
1241 $preview_handle,
1242 'KingAddonsCustomCursorPreview',
1243 [
1244 'enabled' => $localized['enabled'],
1245 ]
1246 );
1247 }
1248
1249 /**
1250 * Render cursor container.
1251 *
1252 * @return void
1253 */
1254 public function render_cursor_markup(): void
1255 {
1256 $settings = $this->get_settings();
1257 if (!$this->should_activate(false, $settings)) {
1258 return;
1259 }
1260
1261 ?>
1262 <div id="ka-custom-cursor" class="ka-custom-cursor" aria-hidden="true">
1263 <div class="ka-custom-cursor__outer"></div>
1264 <div class="ka-custom-cursor__inner"></div>
1265 <div class="ka-custom-cursor__label" data-ka-cursor-label></div>
1266 <div class="ka-custom-cursor__tail" data-ka-cursor-tail></div>
1267 </div>
1268 <?php
1269 }
1270
1271 /**
1272 * Add body class when enabled.
1273 *
1274 * @param array<int,string> $classes Body classes.
1275 *
1276 * @return array<int,string>
1277 */
1278 public function filter_body_classes(array $classes): array
1279 {
1280 $settings = $this->get_settings();
1281 if ($this->should_activate(false, $settings)) {
1282 $classes[] = 'ka-custom-cursor-enabled';
1283 }
1284 return $classes;
1285 }
1286
1287 /**
1288 * Register Elementor controls.
1289 *
1290 * @param Element_Base $element Elementor element.
1291 * @param array<mixed> $args Args.
1292 *
1293 * @return void
1294 */
1295 public function register_elementor_controls(Element_Base $element, $args): void
1296 {
1297 $element->start_controls_section(
1298 'king_addons_custom_cursor_section',
1299 [
1300 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Custom Cursor', 'king-addons'),
1301 'tab' => Controls_Manager::TAB_ADVANCED,
1302 ]
1303 );
1304
1305 $element->add_control(
1306 'kng_cursor_interaction',
1307 [
1308 'label' => esc_html__('Cursor Interaction', 'king-addons'),
1309 'type' => Controls_Manager::SELECT,
1310 'options' => [
1311 'default' => esc_html__('Default', 'king-addons'),
1312 'hover' => esc_html__('Hover', 'king-addons'),
1313 'drag' => esc_html__('Drag', 'king-addons'),
1314 'zoom' => esc_html__('Zoom', 'king-addons'),
1315 'hide' => esc_html__('Hide Cursor', 'king-addons'),
1316 ],
1317 'default' => 'default',
1318 'frontend_available' => true,
1319 ]
1320 );
1321
1322 $element->add_control(
1323 'kng_cursor_magnetic',
1324 [
1325 'label' => esc_html__('Magnetic Behavior', 'king-addons'),
1326 'type' => Controls_Manager::SELECT,
1327 'options' => [
1328 'none' => esc_html__('None', 'king-addons'),
1329 'light' => esc_html__('Light', 'king-addons'),
1330 'strong' => sprintf(__('Strong %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
1331 'follow' => sprintf(__('Follow Center %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
1332 ],
1333 'default' => 'none',
1334 'frontend_available' => true,
1335 ]
1336 );
1337
1338 $element->add_control(
1339 'kng_cursor_color_override',
1340 [
1341 'label' => esc_html__('Cursor Color', 'king-addons'),
1342 'type' => Controls_Manager::COLOR,
1343 'frontend_available' => true,
1344 'condition' => [
1345 'kng_cursor_interaction!' => 'hide',
1346 ],
1347 ]
1348 );
1349
1350 $element->add_control(
1351 'kng_cursor_size_multiplier',
1352 [
1353 'label' => esc_html__('Cursor Size Multiplier', 'king-addons'),
1354 'type' => Controls_Manager::SLIDER,
1355 'size_units' => ['px'],
1356 'range' => [
1357 'px' => [
1358 'min' => 0.5,
1359 'max' => 3,
1360 'step' => 0.1,
1361 ],
1362 ],
1363 'default' => [
1364 'size' => 1,
1365 ],
1366 'frontend_available' => true,
1367 'condition' => [
1368 'kng_cursor_interaction!' => 'hide',
1369 ],
1370 ]
1371 );
1372
1373 if (!$this->can_use_pro()) {
1374 $element->add_control(
1375 'kng_cursor_pro_notice',
1376 [
1377 'type' => Controls_Manager::RAW_HTML,
1378 'raw' => wp_kses_post('<div class="king-addons-pro-notice">' . esc_html__('Upgrade to Pro to unlock magnetic strength variations and advanced overrides.', 'king-addons') . '</div>'),
1379 ]
1380 );
1381 }
1382
1383 $element->end_controls_section();
1384 }
1385
1386 /**
1387 * Apply Elementor attributes for runtime.
1388 *
1389 * @param Element_Base $element Elementor element.
1390 *
1391 * @return void
1392 */
1393 public function apply_elementor_attributes(Element_Base $element): void
1394 {
1395 $settings = $element->get_settings_for_display();
1396
1397 $interaction = $settings['kng_cursor_interaction'] ?? 'default';
1398 if ($interaction && 'default' !== $interaction) {
1399 $element->add_render_attribute('_wrapper', 'data-ka-cursor', esc_attr($interaction));
1400 }
1401
1402 $magnetic = $settings['kng_cursor_magnetic'] ?? 'none';
1403 if (!$this->can_use_pro() && in_array($magnetic, ['strong', 'follow'], true)) {
1404 $magnetic = 'none';
1405 }
1406 if ('none' !== $magnetic) {
1407 $element->add_render_attribute('_wrapper', 'data-ka-magnetic', esc_attr($magnetic));
1408 }
1409
1410 if (!empty($settings['kng_cursor_color_override'])) {
1411 $element->add_render_attribute('_wrapper', 'data-ka-cursor-color', esc_attr($settings['kng_cursor_color_override']));
1412 }
1413
1414 if (!empty($settings['kng_cursor_size_multiplier']['size'])) {
1415 $multiplier = (float) $settings['kng_cursor_size_multiplier']['size'];
1416 if ($multiplier > 0) {
1417 $element->add_render_attribute('_wrapper', 'data-ka-cursor-size', esc_attr((string) $multiplier));
1418 }
1419 }
1420 }
1421
1422 /**
1423 * Sanitize settings.
1424 *
1425 * @param array<string,mixed> $input Raw input.
1426 *
1427 * @return array<string,mixed>
1428 */
1429 public function sanitize_settings($input): array
1430 {
1431 $defaults = $this->get_default_settings();
1432 $output = $defaults;
1433
1434 $output['enabled'] = !empty($input['enabled']);
1435 $output['disable_on_mobile'] = !empty($input['disable_on_mobile']);
1436 $output['rendering_mode'] = in_array($input['rendering_mode'] ?? 'css', ['css', 'enhanced'], true) ? $input['rendering_mode'] : 'css';
1437
1438 $preset = $input['preset'] ?? [];
1439 $output['preset'] = [
1440 'type' => sanitize_text_field($preset['type'] ?? $defaults['preset']['type']),
1441 'size' => $this->sanitize_float($preset['size'] ?? $defaults['preset']['size'], 4, 200, (float) $defaults['preset']['size']),
1442 'border_width' => $this->sanitize_float($preset['border_width'] ?? $defaults['preset']['border_width'], 0, 20, (float) $defaults['preset']['border_width']),
1443 'fill_color' => $this->sanitize_color($preset['fill_color'] ?? $defaults['preset']['fill_color'], $defaults['preset']['fill_color']),
1444 'border_color' => $this->sanitize_color($preset['border_color'] ?? $defaults['preset']['border_color'], $defaults['preset']['border_color']),
1445 'opacity' => $this->sanitize_float($preset['opacity'] ?? $defaults['preset']['opacity'], 0, 1, (float) $defaults['preset']['opacity']),
1446 'blur' => $this->sanitize_float($preset['blur'] ?? $defaults['preset']['blur'], 0, 80, (float) $defaults['preset']['blur']),
1447 'blend_mode' => sanitize_text_field($preset['blend_mode'] ?? $defaults['preset']['blend_mode']),
1448 ];
1449
1450 $states = $input['states'] ?? [];
1451 $output['states'] = [
1452 'normal' => [
1453 'scale' => $this->sanitize_float($states['normal']['scale'] ?? $defaults['states']['normal']['scale'], 0.1, 5, (float) $defaults['states']['normal']['scale']),
1454 'opacity' => $this->sanitize_float($states['normal']['opacity'] ?? $defaults['states']['normal']['opacity'], 0, 1, (float) $defaults['states']['normal']['opacity']),
1455 ],
1456 'hover_link' => [
1457 'scale' => $this->sanitize_float($states['hover_link']['scale'] ?? $defaults['states']['hover_link']['scale'], 0.5, 5, (float) $defaults['states']['hover_link']['scale']),
1458 'color' => $this->sanitize_color($states['hover_link']['color'] ?? $defaults['states']['hover_link']['color'], $defaults['states']['hover_link']['color']),
1459 'border_color' => $this->sanitize_color($states['hover_link']['border_color'] ?? $defaults['states']['hover_link']['border_color'], $defaults['states']['hover_link']['border_color']),
1460 'label' => sanitize_text_field($states['hover_link']['label'] ?? $defaults['states']['hover_link']['label']),
1461 ],
1462 'click' => [
1463 'ripple' => !empty($states['click']['ripple']),
1464 'scale' => $this->sanitize_float($states['click']['scale'] ?? $defaults['states']['click']['scale'], 0.4, 2, (float) $defaults['states']['click']['scale']),
1465 ],
1466 ];
1467
1468 $targeting = $input['targeting'] ?? [];
1469 $output['targeting'] = [
1470 'exclude_logged_in' => !empty($targeting['exclude_logged_in']),
1471 'include_pages' => $this->normalize_ids($targeting['include_pages'] ?? []),
1472 'include_post_types' => $this->sanitize_list($targeting['include_post_types'] ?? []),
1473 'include_urls' => $this->sanitize_list($targeting['include_urls'] ?? []),
1474 'exclude_pages' => $this->normalize_ids($targeting['exclude_pages'] ?? []),
1475 'exclude_post_types' => $this->sanitize_list($targeting['exclude_post_types'] ?? []),
1476 'exclude_selectors' => sanitize_text_field($targeting['exclude_selectors'] ?? $defaults['targeting']['exclude_selectors']),
1477 ];
1478
1479 $image = $input['image'] ?? [];
1480 $output['image'] = [
1481 'url' => esc_url_raw($image['url'] ?? $defaults['image']['url']),
1482 'size' => $this->sanitize_float($image['size'] ?? $defaults['image']['size'], 8, 256, (float) $defaults['image']['size']),
1483 'hotspot_x' => $this->sanitize_float($image['hotspot_x'] ?? $defaults['image']['hotspot_x'], -400, 400, (float) $defaults['image']['hotspot_x']),
1484 'hotspot_y' => $this->sanitize_float($image['hotspot_y'] ?? $defaults['image']['hotspot_y'], -400, 400, (float) $defaults['image']['hotspot_y']),
1485 'retina' => !empty($image['retina']),
1486 ];
1487
1488 $magnetic = $input['magnetic'] ?? [];
1489 $output['magnetic'] = [
1490 'enabled' => !empty($magnetic['enabled']),
1491 'strength' => $this->sanitize_float($magnetic['strength'] ?? $defaults['magnetic']['strength'], 0, 1, (float) $defaults['magnetic']['strength']),
1492 'radius' => (int) $this->sanitize_float($magnetic['radius'] ?? $defaults['magnetic']['radius'], 20, 600, (float) $defaults['magnetic']['radius']),
1493 'selectors' => sanitize_text_field($magnetic['selectors'] ?? $defaults['magnetic']['selectors']),
1494 ];
1495
1496 $movement = $input['movement'] ?? [];
1497 $tail = $movement['tail'] ?? [];
1498 $output['movement'] = [
1499 'follow_speed' => $this->sanitize_float($movement['follow_speed'] ?? $defaults['movement']['follow_speed'], 0.01, 1, (float) $defaults['movement']['follow_speed']),
1500 'tail' => [
1501 'points' => (int) $this->sanitize_float($tail['points'] ?? $defaults['movement']['tail']['points'], 0, 16, (float) $defaults['movement']['tail']['points']),
1502 'decay' => $this->sanitize_float($tail['decay'] ?? $defaults['movement']['tail']['decay'], 0.1, 0.99, (float) $defaults['movement']['tail']['decay']),
1503 ],
1504 ];
1505
1506 return $this->apply_license_limitations($output);
1507 }
1508
1509 /**
1510 * Get settings merged with defaults and license limitations.
1511 *
1512 * @return array<string,mixed>
1513 */
1514 public function get_settings(): array
1515 {
1516 $saved = get_option(self::OPTION_KEY, []);
1517 if (!is_array($saved)) {
1518 $saved = [];
1519 }
1520 $merged = wp_parse_args($saved, $this->get_default_settings());
1521 return $this->apply_license_limitations($merged);
1522 }
1523
1524 /**
1525 * Build settings passed to frontend.
1526 *
1527 * @param array<string,mixed> $settings Settings.
1528 * @param bool $is_preview Preview mode.
1529 *
1530 * @return array<string,mixed>
1531 */
1532 private function build_frontend_settings(array $settings, bool $is_preview): array
1533 {
1534 $enabled = $this->should_activate($is_preview, $settings);
1535
1536 return [
1537 'enabled' => $enabled,
1538 'mode' => $settings['rendering_mode'],
1539 'preset' => $settings['preset'],
1540 'states' => $settings['states'],
1541 'image' => $settings['image'],
1542 'magnetic' => $settings['magnetic'],
1543 'movement' => $settings['movement'],
1544 'targeting' => [
1545 'excludeSelectors' => $settings['targeting']['exclude_selectors'],
1546 ],
1547 'selectors' => [
1548 'hover' => 'a,button,.ka-hoverable',
1549 'attribute' => '[data-ka-cursor]',
1550 'magnetic' => '[data-ka-magnetic]',
1551 ],
1552 'bodyClass' => 'ka-custom-cursor-enabled',
1553 'isPro' => $this->can_use_pro(),
1554 'isPreview' => $is_preview,
1555 ];
1556 }
1557
1558 /**
1559 * Determine if cursor should activate on current request.
1560 *
1561 * @param bool $is_preview Preview flag.
1562 * @param array<string,mixed>|null $settings Settings.
1563 *
1564 * @return bool
1565 */
1566 private function should_activate(bool $is_preview = false, ?array $settings = null): bool
1567 {
1568 $settings = $settings ?? $this->get_settings();
1569
1570 if (empty($settings['enabled'])) {
1571 return false;
1572 }
1573
1574 if (!$is_preview && is_admin()) {
1575 return false;
1576 }
1577
1578 if (!$is_preview && !empty($settings['disable_on_mobile']) && wp_is_mobile()) {
1579 return false;
1580 }
1581
1582 if (!$is_preview && !empty($settings['targeting']['exclude_logged_in']) && is_user_logged_in()) {
1583 return false;
1584 }
1585
1586 if (!$is_preview && class_exists(Plugin::class) && Plugin::instance()->editor->is_edit_mode()) {
1587 return false;
1588 }
1589
1590 if (!$this->can_use_pro()) {
1591 return true;
1592 }
1593
1594 $page_id = get_queried_object_id();
1595 if (!empty($settings['targeting']['include_pages']) && !in_array((int) $page_id, $settings['targeting']['include_pages'], true)) {
1596 return false;
1597 }
1598
1599 $post_type = get_post_type($page_id);
1600 if (!empty($settings['targeting']['include_post_types']) && !in_array((string) $post_type, $settings['targeting']['include_post_types'], true)) {
1601 return false;
1602 }
1603
1604 if (!empty($settings['targeting']['include_urls'])) {
1605 $current_url = $this->get_current_url();
1606 $matched = false;
1607 foreach ($settings['targeting']['include_urls'] as $fragment) {
1608 if ('' !== $fragment && false !== stripos($current_url, $fragment)) {
1609 $matched = true;
1610 break;
1611 }
1612 }
1613 if (!$matched) {
1614 return false;
1615 }
1616 }
1617
1618 if (!empty($settings['targeting']['exclude_pages']) && in_array((int) $page_id, $settings['targeting']['exclude_pages'], true)) {
1619 return false;
1620 }
1621
1622 if (!empty($settings['targeting']['exclude_post_types']) && in_array((string) $post_type, $settings['targeting']['exclude_post_types'], true)) {
1623 return false;
1624 }
1625
1626 return true;
1627 }
1628
1629 /**
1630 * Get default settings.
1631 *
1632 * @return array<string,mixed>
1633 */
1634 private function get_default_settings(): array
1635 {
1636 return [
1637 'enabled' => false,
1638 'disable_on_mobile' => true,
1639 'rendering_mode' => 'css',
1640 'preset' => [
1641 'type' => 'dot',
1642 'size' => 14,
1643 'border_width' => 2,
1644 'fill_color' => '#111111',
1645 'border_color' => '#111111',
1646 'opacity' => 0.9,
1647 'blur' => 0,
1648 'blend_mode' => 'normal',
1649 ],
1650 'states' => [
1651 'normal' => [
1652 'scale' => 1,
1653 'opacity' => 1,
1654 ],
1655 'hover_link' => [
1656 'scale' => 1.25,
1657 'color' => '#111111',
1658 'border_color' => '#ffffff',
1659 'label' => '',
1660 ],
1661 'click' => [
1662 'ripple' => false,
1663 'scale' => 0.9,
1664 ],
1665 ],
1666 'targeting' => [
1667 'exclude_logged_in' => false,
1668 'include_pages' => [],
1669 'include_post_types' => [],
1670 'include_urls' => [],
1671 'exclude_pages' => [],
1672 'exclude_post_types' => [],
1673 'exclude_selectors' => '.elementor-editor-active,.no-cursor-zone',
1674 ],
1675 'image' => [
1676 'url' => '',
1677 'size' => 48,
1678 'hotspot_x' => 0,
1679 'hotspot_y' => 0,
1680 'retina' => true,
1681 ],
1682 'magnetic' => [
1683 'enabled' => false,
1684 'strength' => 0.3,
1685 'radius' => 140,
1686 'selectors' => 'button,.ka-magnetic',
1687 ],
1688 'movement' => [
1689 'follow_speed' => 0.18,
1690 'tail' => [
1691 'points' => 0,
1692 'decay' => 0.65,
1693 ],
1694 ],
1695 ];
1696 }
1697
1698 /**
1699 * Apply license limitations for free version.
1700 *
1701 * @param array<string,mixed> $settings Settings.
1702 *
1703 * @return array<string,mixed>
1704 */
1705 private function apply_license_limitations(array $settings): array
1706 {
1707 if ($this->can_use_pro()) {
1708 return $settings;
1709 }
1710
1711 // Free cursor types
1712 $allowed_presets = ['dot', 'ring', 'dot-ring', 'outline', 'crosshair', 'arrow'];
1713 if (!in_array($settings['preset']['type'], $allowed_presets, true)) {
1714 $settings['preset']['type'] = 'dot';
1715 }
1716
1717 $settings['preset']['blur'] = 0;
1718 $settings['preset']['blend_mode'] = 'normal';
1719
1720 $settings['states']['hover_link']['label'] = '';
1721 $settings['states']['click']['ripple'] = false;
1722
1723 $settings['image'] = $this->get_default_settings()['image'];
1724 $settings['magnetic']['enabled'] = false;
1725 $settings['magnetic']['selectors'] = 'button,.ka-magnetic';
1726 $settings['movement']['follow_speed'] = 0.18;
1727 $settings['movement']['tail'] = [
1728 'points' => 0,
1729 'decay' => 0.65,
1730 ];
1731
1732 $settings['targeting']['include_pages'] = [];
1733 $settings['targeting']['include_post_types'] = [];
1734 $settings['targeting']['include_urls'] = [];
1735 $settings['targeting']['exclude_pages'] = [];
1736 $settings['targeting']['exclude_post_types'] = [];
1737 $settings['targeting']['exclude_selectors'] = '.elementor-editor-active,.no-cursor-zone';
1738
1739 return $settings;
1740 }
1741
1742 /**
1743 * Check license status.
1744 *
1745 * @return bool
1746 */
1747 private function can_use_pro(): bool
1748 {
1749 return function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
1750 }
1751
1752 /**
1753 * Normalize IDs input to integer array.
1754 *
1755 * @param mixed $value Raw ids.
1756 *
1757 * @return array<int,int>
1758 */
1759 private function normalize_ids($value): array
1760 {
1761 if (is_string($value)) {
1762 $value = explode(',', $value);
1763 }
1764
1765 if (!is_array($value)) {
1766 return [];
1767 }
1768
1769 $ids = [];
1770 foreach ($value as $id) {
1771 $id = (int) $id;
1772 if ($id > 0) {
1773 $ids[] = $id;
1774 }
1775 }
1776 return array_values(array_unique($ids));
1777 }
1778
1779 /**
1780 * Sanitize color string.
1781 *
1782 * @param string $value Raw value.
1783 * @param string $fallback Fallback.
1784 *
1785 * @return string
1786 */
1787 private function sanitize_color(string $value, string $fallback): string
1788 {
1789 $value = trim($value);
1790 if ('' === $value) {
1791 return $fallback;
1792 }
1793 return sanitize_text_field($value);
1794 }
1795
1796 /**
1797 * Sanitize float with bounds.
1798 *
1799 * @param mixed $value Raw value.
1800 * @param float $min Min value.
1801 * @param float $max Max value.
1802 * @param float $fallback Default.
1803 *
1804 * @return float
1805 */
1806 private function sanitize_float($value, float $min, float $max, float $fallback): float
1807 {
1808 $float_val = (float) $value;
1809 if ($float_val < $min || $float_val > $max) {
1810 return $fallback;
1811 }
1812 return $float_val;
1813 }
1814
1815 /**
1816 * Sanitize comma separated list or array.
1817 *
1818 * @param mixed $value Raw value.
1819 *
1820 * @return array<int,string>
1821 */
1822 private function sanitize_list($value): array
1823 {
1824 if (is_string($value)) {
1825 $value = explode(',', $value);
1826 }
1827
1828 if (!is_array($value)) {
1829 return [];
1830 }
1831
1832 $clean = [];
1833 foreach ($value as $item) {
1834 $item = sanitize_text_field($item);
1835 if ('' !== $item) {
1836 $clean[] = $item;
1837 }
1838 }
1839
1840 return array_values(array_unique($clean));
1841 }
1842
1843 /**
1844 * Get current URL.
1845 *
1846 * @return string
1847 */
1848 private function get_current_url(): string
1849 {
1850 $scheme = (!empty($_SERVER['HTTPS']) && 'off' !== $_SERVER['HTTPS']) ? 'https' : 'http';
1851 $host = sanitize_text_field($_SERVER['HTTP_HOST'] ?? '');
1852 $uri = sanitize_text_field($_SERVER['REQUEST_URI'] ?? '');
1853 return $scheme . '://' . $host . $uri;
1854 }
1855 }
1856
1857
1858
1859
1860
1861