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

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

616 lines 18.7 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 /**
59 * Initialize the custom cursor.
60 *
61 * @param {Object} config Configuration from PHP.
62 */
63 init(config = globalScope.KingAddonsCustomCursorData) {
64 // Prevent double initialization
65 if (this.initialized) {
66 return;
67 }
68
69 if (!config) {
70 console.warn('[KingAddons CustomCursor] No configuration found.');
71 return;
72 }
73
74 if (!config.enabled) {
75 return;
76 }
77
78 // Wait for document body
79 if (!document.body) {
80 document.addEventListener('DOMContentLoaded', () => this.init(config), { once: true });
81 return;
82 }
83
84 // Check for pointer device (mouse)
85 if (!hasPointerDevice()) {
86 return;
87 }
88
89 // Clean up any previous instance
90 this.destroy();
91 this.config = config;
92 this.initialized = true;
93
94 // Find or create cursor element
95 this.cursor = document.getElementById('ka-custom-cursor');
96 if (!this.cursor) {
97 this.cursor = document.createElement('div');
98 this.cursor.id = 'ka-custom-cursor';
99 this.cursor.className = 'ka-custom-cursor';
100 this.cursor.setAttribute('aria-hidden', 'true');
101 this.cursor.innerHTML = `
102 <div class="ka-custom-cursor__outer"></div>
103 <div class="ka-custom-cursor__inner"></div>
104 <div class="ka-custom-cursor__label" data-ka-cursor-label></div>
105 <div class="ka-custom-cursor__tail" data-ka-cursor-tail></div>
106 `;
107 document.body.appendChild(this.cursor);
108 }
109
110 // Cache DOM references
111 this.inner = this.cursor.querySelector('.ka-custom-cursor__inner');
112 this.outer = this.cursor.querySelector('.ka-custom-cursor__outer');
113 this.label = this.cursor.querySelector('[data-ka-cursor-label]');
114 this.tailContainer = this.cursor.querySelector('[data-ka-cursor-tail]');
115
116 // Initialize state
117 this.state = {
118 x: globalScope.innerWidth / 2,
119 y: globalScope.innerHeight / 2,
120 targetX: globalScope.innerWidth / 2,
121 targetY: globalScope.innerHeight / 2,
122 visible: false,
123 multiplier: 1,
124 colorOverride: null,
125 borderOverride: null,
126 activeMagnet: null,
127 tailDots: [],
128 tailPositions: [],
129 currentState: 'normal',
130 runtimeSize: null,
131 };
132
133 // Apply configuration
134 this.applyPreset();
135 this.buildTail();
136 this.bindEvents();
137
138 // Add body class
139 document.body.classList.add(config.bodyClass || DEFAULT_BODY_CLASS);
140
141 // Add preset type class
142 this.cursor.classList.add(`ka-custom-cursor--type-${config.preset.type}`);
143
144 // Add blend mode class if needed
145 if (config.preset.blend_mode && config.preset.blend_mode !== 'normal') {
146 this.cursor.classList.add('ka-custom-cursor--blend');
147 }
148
149 // Set initial state and show cursor
150 this.setState('normal');
151 this.show();
152 this.renderLoop();
153 },
154
155 /**
156 * Destroy current instance and clean up.
157 */
158 destroy() {
159 // Remove event listeners
160 if (this.listeners.length) {
161 this.listeners.forEach(({ target, type, handler, options }) => {
162 target.removeEventListener(type, handler, options);
163 });
164 }
165 this.listeners = [];
166
167 // Cancel animation frame
168 if (this.raf) {
169 cancelAnimationFrame(this.raf);
170 this.raf = null;
171 }
172
173 // Reset cursor element
174 if (this.cursor) {
175 this.cursor.removeAttribute('data-ka-state');
176 this.cursor.classList.remove('ka-custom-cursor--hidden');
177 }
178
179 this.initialized = false;
180 },
181
182 /**
183 * Bind all event listeners.
184 */
185 bindEvents() {
186 const add = (target, type, handler, options) => {
187 target.addEventListener(type, handler, options);
188 this.listeners.push({ target, type, handler, options });
189 };
190
191 add(document, 'mousemove', this.handleMove.bind(this), { passive: true });
192 add(document, 'mouseenter', this.show.bind(this), { passive: true });
193 add(document, 'mouseleave', this.hide.bind(this), { passive: true });
194 add(document, 'mousedown', this.handleDown.bind(this), { passive: true });
195 add(document, 'mouseup', this.handleUp.bind(this), { passive: true });
196 add(document, 'mouseover', this.handleOver.bind(this), { passive: true });
197 add(document, 'mouseout', this.handleOut.bind(this), { passive: true });
198 add(document, 'scroll', this.handleScroll.bind(this), { passive: true });
199 },
200
201 /**
202 * Apply preset configuration to cursor element via CSS variables.
203 */
204 applyPreset() {
205 const { preset, states, image } = this.config;
206 const setVar = (key, value) => {
207 this.cursor.style.setProperty(key, value);
208 };
209
210 // Determine cursor size based on preset type
211 const hasImageSize = image && image.size;
212 const sizeValue = preset.type === 'image' && hasImageSize ? image.size : preset.size;
213 this.state.runtimeSize = sizeValue;
214
215 // Apply CSS variables
216 setVar('--ka-cursor-size', `${sizeValue}px`);
217 setVar('--ka-cursor-border-width', `${preset.border_width}px`);
218 setVar('--ka-cursor-fill', preset.fill_color);
219 setVar('--ka-cursor-border-color', preset.border_color);
220 setVar('--ka-cursor-opacity', states.normal.opacity);
221 setVar('--ka-cursor-scale', states.normal.scale);
222 setVar('--ka-cursor-blur', `${preset.blur}px`);
223 setVar('--ka-cursor-mix-blend', preset.blend_mode || 'normal');
224
225 // Apply image settings for image cursor type
226 if (preset.type === 'image' && image && image.url) {
227 setVar('--ka-cursor-image', `url(${image.url})`);
228 setVar('--ka-cursor-image-offset-x', `${image.hotspot_x || 0}px`);
229 setVar('--ka-cursor-image-offset-y', `${image.hotspot_y || 0}px`);
230 }
231 },
232
233 /**
234 * Build tail dots for trail effect.
235 */
236 buildTail() {
237 if (!this.tailContainer) {
238 return;
239 }
240 this.tailContainer.innerHTML = '';
241 this.state.tailDots = [];
242 this.state.tailPositions = [];
243
244 const points = this.config.movement && this.config.movement.tail
245 ? this.config.movement.tail.points || 0
246 : 0;
247
248 for (let i = 0; i < points; i++) {
249 const dot = document.createElement('span');
250 dot.className = 'ka-custom-cursor__tail-dot';
251 this.tailContainer.appendChild(dot);
252 this.state.tailDots.push(dot);
253 this.state.tailPositions.push({ x: this.state.x, y: this.state.y });
254 }
255 },
256
257 /**
258 * Main render loop using requestAnimationFrame.
259 */
260 renderLoop() {
261 // Calculate follow speed based on rendering mode
262 const followSpeed = this.config.mode === 'enhanced'
263 ? (this.config.movement?.follow_speed || 0.2)
264 : 1;
265
266 // Smooth interpolation to target position
267 const dx = this.state.targetX - this.state.x;
268 const dy = this.state.targetY - this.state.y;
269 this.state.x += dx * followSpeed;
270 this.state.y += dy * followSpeed;
271
272 // Update cursor position via CSS variables
273 const size = this.state.runtimeSize || this.config.preset.size;
274 this.cursor.style.setProperty('--ka-cursor-x', `${this.state.x - size / 2}px`);
275 this.cursor.style.setProperty('--ka-cursor-y', `${this.state.y - size / 2}px`);
276
277 // Update tail and magnetic effects
278 this.updateTail();
279 this.updateMagnetic();
280
281 // Continue loop
282 this.raf = requestAnimationFrame(this.renderLoop.bind(this));
283 },
284
285 /**
286 * Update tail dots positions.
287 */
288 updateTail() {
289 if (!this.state.tailDots.length) {
290 return;
291 }
292
293 // Add current position to the front
294 this.state.tailPositions.unshift({ x: this.state.x, y: this.state.y });
295
296 // Trim to max length
297 const maxPoints = this.state.tailDots.length;
298 this.state.tailPositions = this.state.tailPositions.slice(0, maxPoints);
299
300 // Update each tail dot
301 this.state.tailDots.forEach((dot, index) => {
302 const point = this.state.tailPositions[index] || { x: this.state.x, y: this.state.y };
303 const scale = Math.max(0.2, 1 - index / (maxPoints + 2));
304 const opacity = Math.max(0.15, 1 - index / (maxPoints + 1));
305 dot.style.transform = `translate3d(${point.x}px, ${point.y}px, 0) scale(${scale})`;
306 dot.style.opacity = `${opacity}`;
307 });
308 },
309
310 /**
311 * Update magnetic pull effect on active element.
312 */
313 updateMagnetic() {
314 if (!this.state.activeMagnet || !this.config.magnetic?.enabled) {
315 return;
316 }
317
318 const rect = this.state.activeMagnet.getBoundingClientRect();
319 const centerX = rect.left + rect.width / 2;
320 const centerY = rect.top + rect.height / 2;
321 const deltaX = this.state.x - centerX;
322 const deltaY = this.state.y - centerY;
323 const distance = Math.hypot(deltaX, deltaY);
324 const radius = this.config.magnetic.radius || 140;
325
326 // Reset transform if outside radius
327 if (distance > radius) {
328 this.state.activeMagnet.style.transform = '';
329 return;
330 }
331
332 // Get magnetic strength based on behavior attribute
333 const behavior = this.state.activeMagnet.getAttribute('data-ka-magnetic');
334 let strength = this.config.magnetic.strength || 0.2;
335
336 switch (behavior) {
337 case 'light':
338 strength = 0.2;
339 break;
340 case 'strong':
341 strength = 0.55;
342 break;
343 case 'follow':
344 strength = 0.75;
345 break;
346 }
347
348 // Calculate pull based on distance
349 const pull = (1 - Math.min(distance / radius, 1)) * strength;
350 const translateX = deltaX * pull;
351 const translateY = deltaY * pull;
352
353 this.state.activeMagnet.style.transform = `translate3d(${translateX}px, ${translateY}px, 0)`;
354 },
355
356 /**
357 * Handle mouse move event.
358 *
359 * @param {MouseEvent} event Mouse event.
360 */
361 handleMove(event) {
362 this.state.targetX = event.clientX;
363 this.state.targetY = event.clientY;
364 if (!this.state.visible) {
365 this.show();
366 }
367 },
368
369 /**
370 * Handle mouse down event.
371 */
372 handleDown() {
373 this.setState('click');
374 if (this.config.states.click.ripple) {
375 this.cursor.classList.add('ka-custom-cursor--ripple');
376 globalScope.setTimeout(() => {
377 this.cursor.classList.remove('ka-custom-cursor--ripple');
378 }, 300);
379 }
380 },
381
382 /**
383 * Handle mouse up event.
384 */
385 handleUp() {
386 this.setState('normal');
387 },
388
389 /**
390 * Handle mouse over event.
391 *
392 * @param {MouseEvent} event Mouse event.
393 */
394 handleOver(event) {
395 const target = event.target;
396 if (!target) {
397 return;
398 }
399
400 // Check if target should be excluded
401 if (this.isExcluded(target)) {
402 this.hide();
403 return;
404 }
405
406 this.show();
407
408 // Find cursor attribute on target or parent
409 const cursorAttr = target.closest(this.config.selectors.attribute);
410 const hoverAttr = cursorAttr ? cursorAttr.getAttribute('data-ka-cursor') : null;
411 const magneticAttr = target.closest(this.config.selectors.magnetic);
412
413 // Handle magnetic elements
414 if (magneticAttr && this.config.magnetic?.enabled) {
415 this.state.activeMagnet = magneticAttr;
416 }
417
418 // Get overrides from attributes
419 const colorOverride = cursorAttr?.getAttribute('data-ka-cursor-color') || target.getAttribute?.('data-ka-cursor-color');
420 const sizeOverride = cursorAttr?.getAttribute('data-ka-cursor-size') || target.getAttribute?.('data-ka-cursor-size');
421 const labelOverride = cursorAttr?.getAttribute('data-ka-cursor-label');
422
423 if (colorOverride) {
424 this.state.colorOverride = colorOverride;
425 }
426 if (sizeOverride) {
427 const parsed = parseFloat(sizeOverride);
428 if (!Number.isNaN(parsed) && parsed > 0) {
429 this.state.multiplier = parsed;
430 }
431 }
432
433 // Handle hide cursor state
434 if (hoverAttr === 'hide') {
435 this.hide();
436 return;
437 }
438
439 // Handle special hover states
440 if (['drag', 'zoom', 'hover'].includes(hoverAttr)) {
441 this.setState('hover', { state: hoverAttr, label: labelOverride });
442 return;
443 }
444
445 // Handle hover on links/buttons
446 if (target.closest(this.config.selectors.hover)) {
447 this.setState('hover', { label: labelOverride });
448 return;
449 }
450
451 this.setState('normal');
452 },
453
454 /**
455 * Handle mouse out event.
456 *
457 * @param {MouseEvent} event Mouse event.
458 */
459 handleOut(event) {
460 const related = event.relatedTarget;
461
462 // Reset magnetic element if mouse left it
463 if (this.state.activeMagnet && event.target === this.state.activeMagnet) {
464 if (!related || !event.target.contains(related)) {
465 event.target.style.transform = '';
466 this.state.activeMagnet = null;
467 }
468 }
469
470 // Reset state if mouse left the document or moved to excluded area
471 if (!related || this.isExcluded(related)) {
472 this.setState('normal');
473 this.state.multiplier = 1;
474 this.state.colorOverride = null;
475 this.state.borderOverride = null;
476 this.state.activeMagnet = null;
477 }
478 },
479
480 /**
481 * Handle scroll event.
482 */
483 handleScroll() {
484 if (!this.state.visible) {
485 return;
486 }
487 // Keep cursor position stable during scroll
488 this.state.targetX = this.state.x;
489 this.state.targetY = this.state.y;
490 },
491
492 /**
493 * Set cursor state (normal, hover, click).
494 *
495 * @param {string} name State name.
496 * @param {Object} options Optional options.
497 */
498 setState(name, options = {}) {
499 this.state.currentState = name;
500 this.cursor.dataset.kaState = name;
501
502 const baseScale = this.config.states.normal.scale || 1;
503 const hoverScale = this.config.states.hover_link.scale || 1.25;
504 const clickScale = this.config.states.click.scale || 0.9;
505 const multiplier = options.sizeMultiplier || this.state.multiplier || 1;
506
507 const setVar = (key, value) => this.cursor.style.setProperty(key, value);
508
509 if (['hover', 'drag', 'zoom'].includes(name)) {
510 setVar('--ka-cursor-scale', hoverScale * multiplier);
511 setVar('--ka-cursor-fill', this.state.colorOverride || this.config.states.hover_link.color || this.config.preset.fill_color);
512 setVar('--ka-cursor-border-color', this.state.borderOverride || this.config.states.hover_link.border_color || this.config.preset.border_color);
513 this.setLabel(options.label || this.config.states.hover_link.label);
514 return;
515 }
516
517 if (name === 'click') {
518 setVar('--ka-cursor-scale', clickScale * multiplier);
519 setVar('--ka-cursor-fill', this.state.colorOverride || this.config.preset.fill_color);
520 setVar('--ka-cursor-border-color', this.state.borderOverride || this.config.preset.border_color);
521 return;
522 }
523
524 // Normal state
525 setVar('--ka-cursor-scale', baseScale * multiplier);
526 setVar('--ka-cursor-fill', this.config.preset.fill_color);
527 setVar('--ka-cursor-border-color', this.config.preset.border_color);
528 this.state.multiplier = 1;
529 this.state.colorOverride = null;
530 this.state.borderOverride = null;
531 this.setLabel('');
532 },
533
534 /**
535 * Set cursor label text.
536 *
537 * @param {string} text Label text.
538 */
539 setLabel(text) {
540 if (!this.label) {
541 return;
542 }
543 if (text) {
544 this.label.textContent = text;
545 this.label.classList.add('is-visible');
546 } else {
547 this.label.textContent = '';
548 this.label.classList.remove('is-visible');
549 }
550 },
551
552 /**
553 * Check if target element is excluded.
554 *
555 * @param {Element} target Target element.
556 * @returns {boolean} True if excluded.
557 */
558 isExcluded(target) {
559 if (!target || !this.config.targeting?.excludeSelectors) {
560 return false;
561 }
562 const selectors = this.config.targeting.excludeSelectors
563 .split(',')
564 .map(item => item.trim())
565 .filter(Boolean);
566 return selectors.some(selector => target.closest(selector));
567 },
568
569 /**
570 * Show the cursor.
571 */
572 show() {
573 this.state.visible = true;
574 this.cursor.classList.remove('ka-custom-cursor--hidden');
575 },
576
577 /**
578 * Hide the cursor.
579 */
580 hide() {
581 this.state.visible = false;
582 this.cursor.classList.add('ka-custom-cursor--hidden');
583 },
584
585 /**
586 * Reinitialize cursor (useful for Elementor preview).
587 *
588 * @param {Object} config New configuration.
589 */
590 reinit(config) {
591 this.destroy();
592 this.initialized = false;
593 this.init(config);
594 },
595 };
596
597 // Expose API globally
598 globalScope.KingAddonsCustomCursor = api;
599
600 /**
601 * Initialize cursor when DOM is ready.
602 */
603 const initWhenReady = () => {
604 if (globalScope.KingAddonsCustomCursorData && globalScope.KingAddonsCustomCursorData.enabled) {
605 api.init(globalScope.KingAddonsCustomCursorData);
606 }
607 };
608
609 // Initialize on DOMContentLoaded or immediately if already loaded
610 if (document.readyState === 'loading') {
611 document.addEventListener('DOMContentLoaded', initWhenReady, { once: true });
612 } else {
613 initWhenReady();
614 }
615 })();
616