PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.74
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.74
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.74, at includes/extensions/Custom_Cursor/Custom_Cursor.php

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