PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
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 / assets / script.js

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

788 lines 25.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * King Addons Custom Cursor
3 * Provides global custom cursor functionality with hover states,
4 * magnetic effects, and Elementor integration.
5 */
6 (() => {
7 'use strict';
8
9 const globalScope = window;
10 const DEFAULT_BODY_CLASS = 'ka-custom-cursor-enabled';
11
12 /**
13 * Check if device supports pointer (mouse).
14 * Falls back to checking for touch capability.
15 *
16 * @returns {boolean} True if device likely has a mouse pointer.
17 */
18 const hasPointerDevice = () => {
19 // Check for pointer:fine media query support
20 if (globalScope.matchMedia) {
21 const pointerFine = globalScope.matchMedia('(pointer:fine)');
22 if (pointerFine.matches) {
23 return true;
24 }
25 // Also check hover capability
26 const canHover = globalScope.matchMedia('(hover:hover)');
27 if (canHover.matches) {
28 return true;
29 }
30 }
31 // Fallback: assume pointer exists if not a pure touch device
32 // Check if maxTouchPoints is 0 (no touch) or device has mouse events
33 if ('ontouchstart' in globalScope && navigator.maxTouchPoints > 0) {
34 // Touch device - check if it also has mouse (like laptops with touchscreen)
35 if (globalScope.matchMedia && globalScope.matchMedia('(pointer:coarse)').matches) {
36 // Coarse pointer only = likely touch-only device
37 return false;
38 }
39 }
40 return true;
41 };
42
43 /**
44 * Custom Cursor API
45 */
46 const api = {
47 state: null,
48 config: null,
49 listeners: [],
50 raf: null,
51 cursor: null,
52 inner: null,
53 outer: null,
54 label: null,
55 tailContainer: null,
56 initialized: false,
57
58 isLivePreviewMatchedType(type) {
59 return ['dot', 'ring', 'dot-ring', 'outline', 'blend'].includes((type || '').toString());
60 },
61
62 /**
63 * Render core preset visuals to match the Live Preview logic.
64 * This ensures Dot/Ring/Dot+Ring/Outline look identical on frontend.
65 *
66 * @param {Object} options Render overrides.
67 * @param {string} [options.fill] Fill color.
68 * @param {string} [options.border] Border color.
69 * @param {number} [options.scale] Inner scale.
70 */
71 renderLikePreview(options = {}) {
72 if (!this.cursor || !this.inner || !this.outer || !this.config) {
73 return;
74 }
75
76 const { preset, states } = this.config;
77 const type = (preset.type || 'dot').toString();
78
79 if (!this.isLivePreviewMatchedType(type)) {
80 // For non-core types, keep the existing CSS-based rendering.
81 this.state.lastFill = (options.fill || preset.fill_color || '#111111').toString();
82 return;
83 }
84
85 const size = parseFloat(this.state.runtimeSize || preset.size || 14) || 14;
86 const borderWidth = parseFloat(preset.border_width || 2) || 2;
87 const fill = (options.fill || preset.fill_color || '#111111').toString();
88 const border = (options.border || preset.border_color || '#111111').toString();
89 const opacity = parseFloat(states?.normal?.opacity ?? 1) || 1;
90 const scale = parseFloat(options.scale ?? states?.normal?.scale ?? 1) || 1;
91 const blur = parseFloat(preset.blur || 0) || 0;
92
93 // Track last fill for ripple.
94 this.state.lastFill = fill;
95
96 // Reset styles
97 this.inner.style.display = 'none';
98 this.outer.style.display = 'none';
99
100 this.inner.style.boxShadow = 'none';
101 this.inner.style.background = fill;
102 this.inner.style.filter = 'none';
103 this.inner.style.borderRadius = '50%';
104 this.inner.style.border = '';
105
106 this.outer.style.background = 'transparent';
107 this.cursor.style.mixBlendMode = 'normal';
108
109 // IMPORTANT: base CSS uses `inset: 0` for both inner/outer.
110 // For preview-matched types we must opt out, otherwise width/height/top/left won't behave.
111 this.inner.style.inset = 'auto';
112 this.outer.style.inset = 'auto';
113 this.inner.style.right = 'auto';
114 this.inner.style.bottom = 'auto';
115 this.outer.style.right = 'auto';
116 this.outer.style.bottom = 'auto';
117 this.inner.style.top = '50%';
118 this.inner.style.left = '50%';
119 this.outer.style.top = '50%';
120 this.outer.style.left = '50%';
121
122 // Base inner
123 this.inner.style.width = `${size}px`;
124 this.inner.style.height = `${size}px`;
125 this.inner.style.opacity = `${opacity}`;
126 this.inner.style.transform = `translate(-50%, -50%) scale(${scale})`;
127
128 // Base outer (ring)
129 const outerSize = size + (borderWidth * 2) + 8;
130 this.outer.style.width = `${outerSize}px`;
131 this.outer.style.height = `${outerSize}px`;
132 this.outer.style.border = `${borderWidth}px solid ${border}`;
133 this.outer.style.marginTop = '0';
134 this.outer.style.marginLeft = '0';
135 this.outer.style.opacity = '1';
136 this.outer.style.transform = 'translate(-50%, -50%)';
137
138 // Type-specific
139 switch (type) {
140 case 'dot':
141 this.inner.style.display = 'block';
142 break;
143 case 'ring':
144 this.outer.style.display = 'block';
145 break;
146 case 'dot-ring':
147 this.inner.style.display = 'block';
148 this.outer.style.display = 'block';
149 break;
150 case 'outline':
151 this.inner.style.display = 'block';
152 this.inner.style.background = 'transparent';
153 this.inner.style.border = `${borderWidth}px solid ${fill}`;
154 this.inner.style.width = `${Math.max(0, size - borderWidth * 2)}px`;
155 this.inner.style.height = `${Math.max(0, size - borderWidth * 2)}px`;
156 break;
157 case 'blend':
158 // Keep existing CSS preset for advanced types; match blend-mode.
159 this.cursor.style.mixBlendMode = 'difference';
160 this.inner.style.display = 'block';
161 this.inner.style.background = '#ffffff';
162 this.inner.style.width = `${size * 1.5}px`;
163 this.inner.style.height = `${size * 1.5}px`;
164 break;
165 default:
166 // For all other types, fall back to existing CSS preset styles.
167 // But still apply blur if configured.
168 this.inner.style.display = 'block';
169 }
170
171 // Apply blur if set (mimic preview rule)
172 if (blur > 0 && !['soft-glow', 'blend'].includes(type)) {
173 this.inner.style.filter = `blur(${blur}px)`;
174 }
175 },
176
177 /**
178 * Initialize the custom cursor.
179 *
180 * @param {Object} config Configuration from PHP.
181 */
182 init(config = globalScope.KingAddonsCustomCursorData) {
183 // Prevent double initialization
184 if (this.initialized) {
185 return;
186 }
187
188 if (!config) {
189 // console.warn('[KingAddons CustomCursor] No configuration found.');
190 return;
191 }
192
193 if (!config.enabled) {
194 return;
195 }
196
197 // Wait for document body
198 if (!document.body) {
199 document.addEventListener('DOMContentLoaded', () => this.init(config), { once: true });
200 return;
201 }
202
203 // Check for pointer device (mouse)
204 if (!hasPointerDevice()) {
205 return;
206 }
207
208 // Clean up any previous instance
209 this.destroy();
210 this.config = config;
211 this.initialized = true;
212
213 // Find or create cursor element
214 this.cursor = document.getElementById('ka-custom-cursor');
215 if (!this.cursor) {
216 this.cursor = document.createElement('div');
217 this.cursor.id = 'ka-custom-cursor';
218 this.cursor.className = 'ka-custom-cursor';
219 this.cursor.setAttribute('aria-hidden', 'true');
220 this.cursor.innerHTML = `
221 <div class="ka-custom-cursor__outer"></div>
222 <div class="ka-custom-cursor__inner"></div>
223 <div class="ka-custom-cursor__label" data-ka-cursor-label></div>
224 <div class="ka-custom-cursor__tail" data-ka-cursor-tail></div>
225 `;
226 document.body.appendChild(this.cursor);
227 }
228
229 // Cache DOM references
230 this.inner = this.cursor.querySelector('.ka-custom-cursor__inner');
231 this.outer = this.cursor.querySelector('.ka-custom-cursor__outer');
232 this.label = this.cursor.querySelector('[data-ka-cursor-label]');
233 this.tailContainer = this.cursor.querySelector('[data-ka-cursor-tail]');
234
235 // Initialize state
236 this.state = {
237 x: globalScope.innerWidth / 2,
238 y: globalScope.innerHeight / 2,
239 targetX: globalScope.innerWidth / 2,
240 targetY: globalScope.innerHeight / 2,
241 visible: false,
242 multiplier: 1,
243 colorOverride: null,
244 borderOverride: null,
245 activeMagnet: null,
246 tailDots: [],
247 tailPositions: [],
248 currentState: 'normal',
249 runtimeSize: null,
250 };
251
252 // Apply configuration
253 this.applyPreset();
254 this.buildTail();
255 this.bindEvents();
256
257 // Add body class
258 document.body.classList.add(config.bodyClass || DEFAULT_BODY_CLASS);
259
260 // Sync hide-original cursor class (must also remove when disabled)
261 if (config.hideOriginalCursor) {
262 document.body.classList.add('ka-cursor-hide-original');
263 } else {
264 document.body.classList.remove('ka-cursor-hide-original');
265 }
266
267 // Add preset type class
268 this.cursor.classList.add(`ka-custom-cursor--type-${config.preset.type}`);
269
270 // Add blend mode class if needed
271 if (config.preset.blend_mode && config.preset.blend_mode !== 'normal') {
272 this.cursor.classList.add('ka-custom-cursor--blend');
273 }
274
275 // Set initial state and show cursor
276 this.setState('normal');
277 this.show();
278 this.renderLoop();
279 },
280
281 /**
282 * Destroy current instance and clean up.
283 */
284 destroy() {
285 // Remove event listeners
286 if (this.listeners.length) {
287 this.listeners.forEach(({ target, type, handler, options }) => {
288 target.removeEventListener(type, handler, options);
289 });
290 }
291 this.listeners = [];
292
293 // Cancel animation frame
294 if (this.raf) {
295 cancelAnimationFrame(this.raf);
296 this.raf = null;
297 }
298
299 // Reset cursor element
300 if (this.cursor) {
301 this.cursor.removeAttribute('data-ka-state');
302 this.cursor.classList.remove('ka-custom-cursor--hidden');
303 }
304
305 this.initialized = false;
306 },
307
308 /**
309 * Bind all event listeners.
310 */
311 bindEvents() {
312 const add = (target, type, handler, options) => {
313 target.addEventListener(type, handler, options);
314 this.listeners.push({ target, type, handler, options });
315 };
316
317 add(document, 'mousemove', this.handleMove.bind(this), { passive: true });
318 add(document, 'mouseenter', this.show.bind(this), { passive: true });
319 add(document, 'mouseleave', this.hide.bind(this), { passive: true });
320 add(document, 'mousedown', this.handleDown.bind(this), { passive: true });
321 add(document, 'mouseup', this.handleUp.bind(this), { passive: true });
322 add(document, 'mouseover', this.handleOver.bind(this), { passive: true });
323 add(document, 'mouseout', this.handleOut.bind(this), { passive: true });
324 add(document, 'scroll', this.handleScroll.bind(this), { passive: true });
325 },
326
327 /**
328 * Apply preset configuration to cursor element via CSS variables.
329 */
330 applyPreset() {
331 const { preset, states, image } = this.config;
332 const setVar = (key, value) => {
333 this.cursor.style.setProperty(key, value);
334 };
335
336 // Determine cursor size based on preset type
337 const hasImageSize = image && image.size;
338 const sizeValue = preset.type === 'image' && hasImageSize ? image.size : preset.size;
339 this.state.runtimeSize = sizeValue;
340
341 // Apply CSS variables
342 setVar('--ka-cursor-size', `${sizeValue}px`);
343 setVar('--ka-cursor-border-width', `${preset.border_width}px`);
344 setVar('--ka-cursor-fill', preset.fill_color);
345 setVar('--ka-cursor-border-color', preset.border_color);
346 if (this.isLivePreviewMatchedType(preset.type)) {
347 // Live Preview applies opacity/scale to inner only, not the whole cursor.
348 setVar('--ka-cursor-opacity', '1');
349 setVar('--ka-cursor-scale', '1');
350 } else {
351 setVar('--ka-cursor-opacity', states.normal.opacity);
352 setVar('--ka-cursor-scale', states.normal.scale);
353 }
354 setVar('--ka-cursor-blur', `${preset.blur}px`);
355 setVar('--ka-cursor-mix-blend', preset.blend_mode || 'normal');
356
357 // Apply image settings for image cursor type
358 if (preset.type === 'image' && image && image.url) {
359 setVar('--ka-cursor-image', `url(${image.url})`);
360 setVar('--ka-cursor-image-offset-x', `${image.hotspot_x || 0}px`);
361 setVar('--ka-cursor-image-offset-y', `${image.hotspot_y || 0}px`);
362 }
363
364 // Render visuals to match admin Live Preview.
365 this.renderLikePreview();
366 },
367
368 /**
369 * Build tail dots for trail effect.
370 */
371 buildTail() {
372 if (!this.tailContainer) {
373 return;
374 }
375 this.tailContainer.innerHTML = '';
376 this.state.tailDots = [];
377 this.state.tailPositions = [];
378
379 const points = this.config.movement && this.config.movement.tail
380 ? this.config.movement.tail.points || 0
381 : 0;
382
383 for (let i = 0; i < points; i++) {
384 const dot = document.createElement('span');
385 dot.className = 'ka-custom-cursor__tail-dot';
386 this.tailContainer.appendChild(dot);
387 this.state.tailDots.push(dot);
388 this.state.tailPositions.push({ x: this.state.x, y: this.state.y });
389 }
390 },
391
392 /**
393 * Main render loop using requestAnimationFrame.
394 */
395 renderLoop() {
396 // Calculate follow speed based on rendering mode
397 const followSpeed = this.config.mode === 'enhanced'
398 ? (this.config.movement?.follow_speed || 0.2)
399 : 1;
400
401 // Smooth interpolation to target position
402 const dx = this.state.targetX - this.state.x;
403 const dy = this.state.targetY - this.state.y;
404 this.state.x += dx * followSpeed;
405 this.state.y += dy * followSpeed;
406
407 // Update cursor position via CSS variables
408 const size = this.state.runtimeSize || this.config.preset.size;
409 this.cursor.style.setProperty('--ka-cursor-x', `${this.state.x - size / 2}px`);
410 this.cursor.style.setProperty('--ka-cursor-y', `${this.state.y - size / 2}px`);
411
412 // Update tail and magnetic effects
413 this.updateTail();
414 this.updateMagnetic();
415
416 // Continue loop
417 this.raf = requestAnimationFrame(this.renderLoop.bind(this));
418 },
419
420 /**
421 * Update tail dots positions.
422 */
423 updateTail() {
424 if (!this.state.tailDots.length) {
425 return;
426 }
427
428 // Add current position to the front
429 this.state.tailPositions.unshift({ x: this.state.x, y: this.state.y });
430
431 // Trim to max length
432 const maxPoints = this.state.tailDots.length;
433 this.state.tailPositions = this.state.tailPositions.slice(0, maxPoints);
434
435 // Update each tail dot
436 this.state.tailDots.forEach((dot, index) => {
437 const point = this.state.tailPositions[index] || { x: this.state.x, y: this.state.y };
438 const scale = Math.max(0.2, 1 - index / (maxPoints + 2));
439 const opacity = Math.max(0.15, 1 - index / (maxPoints + 1));
440 dot.style.transform = `translate3d(${point.x}px, ${point.y}px, 0) scale(${scale})`;
441 dot.style.opacity = `${opacity}`;
442 });
443 },
444
445 /**
446 * Update magnetic pull effect on active element.
447 */
448 updateMagnetic() {
449 if (!this.state.activeMagnet || !this.config.magnetic?.enabled) {
450 return;
451 }
452
453 const rect = this.state.activeMagnet.getBoundingClientRect();
454 const centerX = rect.left + rect.width / 2;
455 const centerY = rect.top + rect.height / 2;
456 const deltaX = this.state.x - centerX;
457 const deltaY = this.state.y - centerY;
458 const distance = Math.hypot(deltaX, deltaY);
459 const radius = this.config.magnetic.radius || 140;
460
461 // Reset transform if outside radius
462 if (distance > radius) {
463 this.state.activeMagnet.style.transform = '';
464 return;
465 }
466
467 // Get magnetic strength based on behavior attribute
468 const behavior = this.state.activeMagnet.getAttribute('data-ka-magnetic');
469 let strength = this.config.magnetic.strength || 0.2;
470
471 switch (behavior) {
472 case 'light':
473 strength = 0.2;
474 break;
475 case 'strong':
476 strength = 0.55;
477 break;
478 case 'follow':
479 strength = 0.75;
480 break;
481 }
482
483 // Calculate pull based on distance
484 const pull = (1 - Math.min(distance / radius, 1)) * strength;
485 const translateX = deltaX * pull;
486 const translateY = deltaY * pull;
487
488 this.state.activeMagnet.style.transform = `translate3d(${translateX}px, ${translateY}px, 0)`;
489 },
490
491 /**
492 * Handle mouse move event.
493 *
494 * @param {MouseEvent} event Mouse event.
495 */
496 handleMove(event) {
497 this.state.targetX = event.clientX;
498 this.state.targetY = event.clientY;
499 if (!this.state.visible) {
500 this.show();
501 }
502 },
503
504 /**
505 * Handle mouse down event.
506 */
507 handleDown(event) {
508 this.setState('click');
509 if (this.config.states.click.ripple) {
510 this.spawnClickRipple(event);
511 }
512 },
513
514 /**
515 * Spawn a click ripple that expands beyond cursor bounds.
516 */
517 spawnClickRipple(event) {
518 try {
519 const ripple = document.createElement('div');
520 ripple.className = 'ka-custom-cursor__click-ripple';
521 const x = event && typeof event.clientX === 'number' ? event.clientX : (parseFloat(this.state.targetX) || 0);
522 const y = event && typeof event.clientY === 'number' ? event.clientY : (parseFloat(this.state.targetY) || 0);
523 ripple.style.left = `${x - 30}px`;
524 ripple.style.top = `${y - 30}px`;
525 ripple.style.background = (this.state.lastFill || this.config?.preset?.fill_color || '#111111').toString();
526 document.body.appendChild(ripple);
527 globalScope.setTimeout(() => ripple.remove(), 650);
528 } catch (e) {
529 // noop
530 }
531 },
532
533 /**
534 * Handle mouse up event.
535 */
536 handleUp() {
537 this.setState('normal');
538 },
539
540 /**
541 * Handle mouse over event.
542 *
543 * @param {MouseEvent} event Mouse event.
544 */
545 handleOver(event) {
546 const target = event.target;
547 if (!target) {
548 return;
549 }
550
551 // Check if target should be excluded
552 if (this.isExcluded(target)) {
553 this.hide();
554 return;
555 }
556
557 this.show();
558
559 // Find cursor attribute on target or parent
560 const cursorAttr = target.closest(this.config.selectors.attribute);
561 const hoverAttr = cursorAttr ? cursorAttr.getAttribute('data-ka-cursor') : null;
562 const magneticAttr = target.closest(this.config.selectors.magnetic);
563
564 // Handle magnetic elements
565 if (magneticAttr && this.config.magnetic?.enabled) {
566 this.state.activeMagnet = magneticAttr;
567 }
568
569 // Get overrides from attributes
570 const colorOverride = cursorAttr?.getAttribute('data-ka-cursor-color') || target.getAttribute?.('data-ka-cursor-color');
571 const sizeOverride = cursorAttr?.getAttribute('data-ka-cursor-size') || target.getAttribute?.('data-ka-cursor-size');
572 const labelOverride = cursorAttr?.getAttribute('data-ka-cursor-label');
573
574 if (colorOverride) {
575 this.state.colorOverride = colorOverride;
576 }
577 if (sizeOverride) {
578 const parsed = parseFloat(sizeOverride);
579 if (!Number.isNaN(parsed) && parsed > 0) {
580 this.state.multiplier = parsed;
581 }
582 }
583
584 // Handle hide cursor state
585 if (hoverAttr === 'hide') {
586 this.hide();
587 return;
588 }
589
590 // Handle special hover states
591 if (['drag', 'zoom', 'hover'].includes(hoverAttr)) {
592 this.setState('hover', { state: hoverAttr, label: labelOverride });
593 return;
594 }
595
596 // Handle hover on links/buttons
597 if (target.closest(this.config.selectors.hover)) {
598 this.setState('hover', { label: labelOverride });
599 return;
600 }
601
602 this.setState('normal');
603 },
604
605 /**
606 * Handle mouse out event.
607 *
608 * @param {MouseEvent} event Mouse event.
609 */
610 handleOut(event) {
611 const related = event.relatedTarget;
612
613 // Reset magnetic element if mouse left it
614 if (this.state.activeMagnet && event.target === this.state.activeMagnet) {
615 if (!related || !event.target.contains(related)) {
616 event.target.style.transform = '';
617 this.state.activeMagnet = null;
618 }
619 }
620
621 // Reset state if mouse left the document or moved to excluded area
622 if (!related || this.isExcluded(related)) {
623 this.setState('normal');
624 this.state.multiplier = 1;
625 this.state.colorOverride = null;
626 this.state.borderOverride = null;
627 this.state.activeMagnet = null;
628 }
629 },
630
631 /**
632 * Handle scroll event.
633 */
634 handleScroll() {
635 if (!this.state.visible) {
636 return;
637 }
638 // Keep cursor position stable during scroll
639 this.state.targetX = this.state.x;
640 this.state.targetY = this.state.y;
641 },
642
643 /**
644 * Set cursor state (normal, hover, click).
645 *
646 * @param {string} name State name.
647 * @param {Object} options Optional options.
648 */
649 setState(name, options = {}) {
650 this.state.currentState = name;
651 this.cursor.dataset.kaState = name;
652
653 const baseScale = this.config.states.normal.scale || 1;
654 const hoverScale = this.config.states.hover_link.scale || 1.25;
655 const clickScale = this.config.states.click.scale || 0.9;
656 const multiplier = options.sizeMultiplier || this.state.multiplier || 1;
657
658 const isMatched = this.isLivePreviewMatchedType(this.config?.preset?.type);
659 const setVar = (key, value) => this.cursor.style.setProperty(key, value);
660
661 if (['hover', 'drag', 'zoom'].includes(name)) {
662 const nextFill = this.state.colorOverride || this.config.states.hover_link.color || this.config.preset.fill_color;
663 const nextBorder = this.state.borderOverride || this.config.states.hover_link.border_color || this.config.preset.border_color;
664 if (isMatched) {
665 this.renderLikePreview({ fill: nextFill, border: nextBorder, scale: hoverScale * multiplier });
666 } else {
667 setVar('--ka-cursor-scale', hoverScale * multiplier);
668 setVar('--ka-cursor-fill', nextFill);
669 setVar('--ka-cursor-border-color', nextBorder);
670 }
671 this.setLabel(options.label || this.config.states.hover_link.label);
672 return;
673 }
674
675 if (name === 'click') {
676 const nextFill = this.state.colorOverride || this.config.preset.fill_color;
677 const nextBorder = this.state.borderOverride || this.config.preset.border_color;
678 if (isMatched) {
679 this.renderLikePreview({ fill: nextFill, border: nextBorder, scale: clickScale * multiplier });
680 } else {
681 setVar('--ka-cursor-scale', clickScale * multiplier);
682 setVar('--ka-cursor-fill', nextFill);
683 setVar('--ka-cursor-border-color', nextBorder);
684 }
685 return;
686 }
687
688 // Normal state
689 if (isMatched) {
690 this.renderLikePreview({
691 fill: this.config.preset.fill_color,
692 border: this.config.preset.border_color,
693 scale: baseScale * multiplier,
694 });
695 } else {
696 setVar('--ka-cursor-scale', baseScale * multiplier);
697 setVar('--ka-cursor-fill', this.config.preset.fill_color);
698 setVar('--ka-cursor-border-color', this.config.preset.border_color);
699 }
700 this.state.multiplier = 1;
701 this.state.colorOverride = null;
702 this.state.borderOverride = null;
703 this.setLabel('');
704 },
705
706 /**
707 * Set cursor label text.
708 *
709 * @param {string} text Label text.
710 */
711 setLabel(text) {
712 if (!this.label) {
713 return;
714 }
715 if (text) {
716 this.label.textContent = text;
717 this.label.classList.add('is-visible');
718 } else {
719 this.label.textContent = '';
720 this.label.classList.remove('is-visible');
721 }
722 },
723
724 /**
725 * Check if target element is excluded.
726 *
727 * @param {Element} target Target element.
728 * @returns {boolean} True if excluded.
729 */
730 isExcluded(target) {
731 if (!target || !this.config.targeting?.excludeSelectors) {
732 return false;
733 }
734 const selectors = this.config.targeting.excludeSelectors
735 .split(',')
736 .map(item => item.trim())
737 .filter(Boolean);
738 return selectors.some(selector => target.closest(selector));
739 },
740
741 /**
742 * Show the cursor.
743 */
744 show() {
745 this.state.visible = true;
746 this.cursor.classList.remove('ka-custom-cursor--hidden');
747 },
748
749 /**
750 * Hide the cursor.
751 */
752 hide() {
753 this.state.visible = false;
754 this.cursor.classList.add('ka-custom-cursor--hidden');
755 },
756
757 /**
758 * Reinitialize cursor (useful for Elementor preview).
759 *
760 * @param {Object} config New configuration.
761 */
762 reinit(config) {
763 this.destroy();
764 this.initialized = false;
765 this.init(config);
766 },
767 };
768
769 // Expose API globally
770 globalScope.KingAddonsCustomCursor = api;
771
772 /**
773 * Initialize cursor when DOM is ready.
774 */
775 const initWhenReady = () => {
776 if (globalScope.KingAddonsCustomCursorData && globalScope.KingAddonsCustomCursorData.enabled) {
777 api.init(globalScope.KingAddonsCustomCursorData);
778 }
779 };
780
781 // Initialize on DOMContentLoaded or immediately if already loaded
782 if (document.readyState === 'loading') {
783 document.addEventListener('DOMContentLoaded', initWhenReady, { once: true });
784 } else {
785 initWhenReady();
786 }
787 })();
788