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 / widgets / Magnetic_Buttons / 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/widgets/Magnetic_Buttons/script.js

575 lines 19.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 "use strict";
2
3 (function ($) {
4 const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
5
6 const parseOptions = (root) => {
7 if (!root || !root.dataset) {
8 return {};
9 }
10
11 try {
12 return JSON.parse(root.dataset.options || "{}");
13 } catch (e) {
14 return {};
15 }
16 };
17
18 const getCurve = (preset) => {
19 switch (preset) {
20 case "linear":
21 return (t) => t;
22 case "ease-out":
23 return (t) => 1 - Math.pow(1 - t, 3);
24 case "gaussian":
25 return (t) => {
26 const raw = Math.exp(-Math.pow(1 - t, 2) * 4);
27 const min = Math.exp(-4);
28 return (raw - min) / (1 - min);
29 };
30 case "snappy":
31 return (t) => 1 - Math.pow(1 - t, 3);
32 case "calm":
33 return (t) => t * t;
34 case "smooth":
35 default:
36 return (t) => 1 - Math.pow(1 - t, 2);
37 }
38 };
39
40 const isEnabled = (value, fallback = false) => {
41 if (value === undefined || value === null || value === "") {
42 return fallback;
43 }
44 if (value === true || value === "yes" || value === "1" || value === 1) {
45 return true;
46 }
47 if (value === false || value === "no" || value === "0" || value === 0) {
48 return false;
49 }
50 return fallback;
51 };
52
53 const normalizeOptions = (raw = {}) => {
54 return {
55 enabled: isEnabled(raw.enabled, true),
56 strength: clamp(Number(raw.strength) || 0, 0, 100),
57 radius: Math.max(Number(raw.radius) || 0, 0),
58 maxOffset: Math.max(Number(raw.maxOffset) || 0, 0),
59 returnSpeed: clamp(Number(raw.returnSpeed) || 0, 0, 100),
60 curve: raw.curve || raw.easing || "smooth",
61 axisX: isEnabled(raw.axisX, true),
62 axisY: isEnabled(raw.axisY, true),
63 damping: clamp(Number(raw.damping) || 0, 0, 1),
64 triggerMode: raw.triggerMode || "bounds",
65 triggerPadding: Math.max(Number(raw.triggerPadding) || 0, 0),
66 boundary: raw.boundary || "none",
67 boundaryWidth: Math.max(Number(raw.boundaryWidth) || 0, 0),
68 boundaryHeight: Math.max(Number(raw.boundaryHeight) || 0, 0),
69 edgeResistance: clamp(Number(raw.edgeResistance) || 0, 0, 100),
70 overshoot: isEnabled(raw.overshoot, false),
71 innerEnabled: isEnabled(raw.innerEnabled, false),
72 innerStrength: clamp(Number(raw.innerStrength) || 0, 0, 100),
73 innerMaxOffset: Math.max(Number(raw.innerMaxOffset) || 0, 0),
74 reducedMotionMode: raw.reducedMotionMode || "respect",
75 touchBehavior: raw.touchBehavior || "off",
76 editorPreview: isEnabled(raw.editorPreview, true),
77 pauseOffscreen: isEnabled(raw.pauseOffscreen, false),
78 selector: typeof raw.selector === "string" ? raw.selector.trim() : "",
79 innerSelector: typeof raw.innerSelector === "string" ? raw.innerSelector.trim() : "",
80 };
81 };
82
83 const resolveTargets = (root, options) => {
84 const targetType = root.dataset.target || "button";
85 const selectorMap = {
86 button: ".king-addons-magnetic-buttons__button",
87 card: ".king-addons-magnetic-buttons__card",
88 icon: ".king-addons-magnetic-buttons__icon-target",
89 };
90
91 if (targetType === "selector") {
92 const selector = options.selector || root.dataset.selector || "";
93 if (!selector) {
94 return [];
95 }
96
97 let targets = Array.from(root.querySelectorAll(selector));
98 if (!targets.length) {
99 targets = Array.from(document.querySelectorAll(selector));
100 }
101 return targets;
102 }
103
104 const selector = selectorMap[targetType] || selectorMap.button;
105 return Array.from(root.querySelectorAll(selector));
106 };
107
108 const initMagneticTarget = (root, target, targetType, options) => {
109 if (!target) {
110 return;
111 }
112
113 if (target.__kaMagneticInstance && typeof target.__kaMagneticInstance.destroy === "function") {
114 target.__kaMagneticInstance.destroy();
115 }
116
117 if (!options.enabled) {
118 return;
119 }
120
121 const isEditor =
122 window.elementorFrontend &&
123 elementorFrontend.isEditMode &&
124 elementorFrontend.isEditMode();
125 if (isEditor && !options.editorPreview) {
126 return;
127 }
128
129 const prefersReduced =
130 window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
131 if (options.reducedMotionMode === "disable") {
132 return;
133 }
134
135 let motionFactor = 1;
136 if (prefersReduced) {
137 if (options.reducedMotionMode === "respect") {
138 return;
139 }
140 if (options.reducedMotionMode === "minimal") {
141 motionFactor *= 0.4;
142 }
143 }
144
145 const isCoarsePointer =
146 window.matchMedia && window.matchMedia("(hover: none), (pointer: coarse)").matches;
147 const allowTouch = options.touchBehavior !== "off";
148 const isTouchReduced = options.touchBehavior === "reduced";
149 if (isCoarsePointer && !allowTouch) {
150 return;
151 }
152 if (isCoarsePointer && isTouchReduced) {
153 motionFactor *= 0.6;
154 }
155
156 const strength = clamp((options.strength / 100) * motionFactor, 0, 1);
157 const radius = Math.max(options.radius, 0);
158 const maxOffset = Math.max(options.maxOffset * motionFactor, 0);
159 const returnSpeed = clamp(options.returnSpeed / 100, 0, 1);
160
161 if (!strength || !radius || !maxOffset) {
162 return;
163 }
164
165 target.dataset.magneticInit = "yes";
166
167 const curve = getCurve(options.curve);
168 const edgeResistance = clamp(options.edgeResistance / 100, 0, 1);
169 const damping = clamp(options.damping, 0, 1);
170 const speed = clamp(0.06 + returnSpeed * 0.24, 0.05, 0.35);
171 const lerpSpeed = clamp(speed * (1 - damping * 0.6) + 0.02, 0.02, 0.4);
172 const springStiffness = clamp(0.04 + returnSpeed * 0.2, 0.04, 0.3);
173 const springDamping = clamp(0.12 + damping * 0.5, 0.12, 0.8);
174
175 const innerSelectors = {
176 button: ".king-addons-magnetic-buttons__content",
177 card: ".king-addons-magnetic-buttons__card-inner",
178 icon: ".king-addons-magnetic-buttons__icon-inner",
179 };
180
181 const innerSelector = options.innerSelector || innerSelectors[targetType] || "";
182 const innerTarget = options.innerEnabled && innerSelector ? target.querySelector(innerSelector) : null;
183 const innerStrength = clamp(options.innerStrength / 100, 0, 1);
184 const innerMaxOffset = Math.max(options.innerMaxOffset, 0);
185
186 const frameInterval = isEditor ? 1000 / 30 : 0;
187
188 let currentX = 0;
189 let currentY = 0;
190 let targetX = 0;
191 let targetY = 0;
192 let innerX = 0;
193 let innerY = 0;
194 let targetInnerX = 0;
195 let targetInnerY = 0;
196 let velocityX = 0;
197 let velocityY = 0;
198 let innerVelocityX = 0;
199 let innerVelocityY = 0;
200 let rafId = 0;
201 let lastFrame = 0;
202 let isHovering = false;
203 let isPressed = false;
204 let isVisible = true;
205
206 const applyTransform = () => {
207 target.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
208 if (innerTarget) {
209 innerTarget.style.transform = `translate3d(${innerX}px, ${innerY}px, 0)`;
210 }
211 };
212
213 const resetTargets = () => {
214 targetX = 0;
215 targetY = 0;
216 targetInnerX = 0;
217 targetInnerY = 0;
218 };
219
220 const getBoundaryRect = (rect) => {
221 if (options.boundary === "parent") {
222 const parent = target.parentElement || root;
223 return parent ? parent.getBoundingClientRect() : null;
224 }
225
226 if (options.boundary === "custom") {
227 const width = options.boundaryWidth;
228 const height = options.boundaryHeight;
229 if (!width || !height) {
230 return null;
231 }
232
233 const centerX = rect.left + rect.width / 2;
234 const centerY = rect.top + rect.height / 2;
235 return {
236 left: centerX - width / 2,
237 right: centerX + width / 2,
238 top: centerY - height / 2,
239 bottom: centerY + height / 2,
240 };
241 }
242
243 return null;
244 };
245
246 const applyBoundaryClamp = (rect) => {
247 if (options.boundary === "none") {
248 return;
249 }
250
251 const boundary = getBoundaryRect(rect);
252 if (!boundary) {
253 return;
254 }
255
256 const minX = boundary.left - rect.left;
257 const maxX = boundary.right - rect.right;
258 const minY = boundary.top - rect.top;
259 const maxY = boundary.bottom - rect.bottom;
260
261 targetX = clamp(targetX, Math.min(minX, maxX), Math.max(minX, maxX));
262 targetY = clamp(targetY, Math.min(minY, maxY), Math.max(minY, maxY));
263 };
264
265 const updateTarget = (clientX, clientY) => {
266 const rect = target.getBoundingClientRect();
267 const centerX = rect.left + rect.width / 2;
268 const centerY = rect.top + rect.height / 2;
269 let dx = clientX - centerX;
270 let dy = clientY - centerY;
271
272 if (!options.axisX) {
273 dx = 0;
274 }
275 if (!options.axisY) {
276 dy = 0;
277 }
278
279 const distance = Math.sqrt(dx * dx + dy * dy);
280 if (!radius || distance > radius) {
281 resetTargets();
282 return;
283 }
284
285 const closeness = 1 - clamp(distance / radius, 0, 1);
286 const edgeFactor = 1 - edgeResistance * (1 - closeness);
287 const pull = curve(closeness) * strength * clamp(edgeFactor, 0, 1);
288 const limitedOffset = Math.min(maxOffset, radius);
289
290 targetX = clamp(dx * pull, -limitedOffset, limitedOffset);
291 targetY = clamp(dy * pull, -limitedOffset, limitedOffset);
292
293 applyBoundaryClamp(rect);
294
295 if (innerTarget && innerStrength && innerMaxOffset) {
296 const innerPull = curve(closeness) * innerStrength;
297 const innerLimited = Math.min(innerMaxOffset, radius);
298 targetInnerX = clamp(dx * innerPull, -innerLimited, innerLimited);
299 targetInnerY = clamp(dy * innerPull, -innerLimited, innerLimited);
300 }
301 };
302
303 const isWithinTriggerArea = (clientX, clientY) => {
304 const rect = target.getBoundingClientRect();
305
306 if (options.triggerMode === "radius") {
307 const centerX = rect.left + rect.width / 2;
308 const centerY = rect.top + rect.height / 2;
309 const dx = clientX - centerX;
310 const dy = clientY - centerY;
311 return Math.sqrt(dx * dx + dy * dy) <= radius;
312 }
313
314 if (options.triggerMode === "padding") {
315 const padding = options.triggerPadding;
316 return (
317 clientX >= rect.left - padding &&
318 clientX <= rect.right + padding &&
319 clientY >= rect.top - padding &&
320 clientY <= rect.bottom + padding
321 );
322 }
323
324 return (
325 clientX >= rect.left &&
326 clientX <= rect.right &&
327 clientY >= rect.top &&
328 clientY <= rect.bottom
329 );
330 };
331
332 const animate = (timestamp) => {
333 if (frameInterval && timestamp - lastFrame < frameInterval) {
334 rafId = window.requestAnimationFrame(animate);
335 return;
336 }
337
338 lastFrame = timestamp;
339
340 if (options.overshoot) {
341 velocityX += (targetX - currentX) * springStiffness;
342 velocityY += (targetY - currentY) * springStiffness;
343 velocityX *= 1 - springDamping;
344 velocityY *= 1 - springDamping;
345 currentX += velocityX;
346 currentY += velocityY;
347 } else {
348 currentX += (targetX - currentX) * lerpSpeed;
349 currentY += (targetY - currentY) * lerpSpeed;
350 }
351
352 if (innerTarget) {
353 if (options.overshoot) {
354 innerVelocityX += (targetInnerX - innerX) * springStiffness;
355 innerVelocityY += (targetInnerY - innerY) * springStiffness;
356 innerVelocityX *= 1 - springDamping;
357 innerVelocityY *= 1 - springDamping;
358 innerX += innerVelocityX;
359 innerY += innerVelocityY;
360 } else {
361 innerX += (targetInnerX - innerX) * lerpSpeed;
362 innerY += (targetInnerY - innerY) * lerpSpeed;
363 }
364 }
365
366 if (Math.abs(targetX - currentX) < 0.05) {
367 currentX = targetX;
368 }
369 if (Math.abs(targetY - currentY) < 0.05) {
370 currentY = targetY;
371 }
372
373 if (innerTarget) {
374 if (Math.abs(targetInnerX - innerX) < 0.05) {
375 innerX = targetInnerX;
376 }
377 if (Math.abs(targetInnerY - innerY) < 0.05) {
378 innerY = targetInnerY;
379 }
380 }
381
382 applyTransform();
383
384 const isSettled =
385 !isHovering &&
386 !isPressed &&
387 currentX === 0 &&
388 currentY === 0 &&
389 (!innerTarget || (innerX === 0 && innerY === 0));
390
391 if (isSettled) {
392 rafId = 0;
393 return;
394 }
395
396 rafId = window.requestAnimationFrame(animate);
397 };
398
399 const start = () => {
400 if (!rafId) {
401 rafId = window.requestAnimationFrame(animate);
402 }
403 };
404
405 const onPointerEnter = (event) => {
406 if (!isVisible) {
407 return;
408 }
409 if (event.pointerType === "touch" && !allowTouch) {
410 return;
411 }
412 isHovering = true;
413 updateTarget(event.clientX, event.clientY);
414 start();
415 };
416
417 const onPointerMove = (event) => {
418 if (!isVisible || isPressed) {
419 return;
420 }
421 if (event.pointerType === "touch" && !allowTouch) {
422 return;
423 }
424 updateTarget(event.clientX, event.clientY);
425 start();
426 };
427
428 const onPointerLeave = () => {
429 isHovering = false;
430 resetTargets();
431 start();
432 };
433
434 const onPointerMoveWindow = (event) => {
435 if (!isVisible || isPressed) {
436 return;
437 }
438 if (event.pointerType === "touch" && !allowTouch) {
439 return;
440 }
441
442 const inside = isWithinTriggerArea(event.clientX, event.clientY);
443 if (inside) {
444 isHovering = true;
445 updateTarget(event.clientX, event.clientY);
446 start();
447 return;
448 }
449
450 if (isHovering) {
451 isHovering = false;
452 resetTargets();
453 start();
454 }
455 };
456
457 const onPointerDown = (event) => {
458 if (event.pointerType === "touch") {
459 return;
460 }
461 isPressed = true;
462 resetTargets();
463 start();
464 };
465
466 const onPointerUp = () => {
467 if (!isPressed) {
468 return;
469 }
470 isPressed = false;
471 resetTargets();
472 start();
473 };
474
475 const onFocus = () => {
476 isHovering = false;
477 isPressed = false;
478 currentX = 0;
479 currentY = 0;
480 innerX = 0;
481 innerY = 0;
482 resetTargets();
483 applyTransform();
484 };
485
486 if (options.triggerMode === "bounds") {
487 target.addEventListener("pointerenter", onPointerEnter);
488 target.addEventListener("pointermove", onPointerMove);
489 target.addEventListener("pointerleave", onPointerLeave);
490 } else {
491 window.addEventListener("pointermove", onPointerMoveWindow);
492 }
493
494 target.addEventListener("pointerdown", onPointerDown);
495 window.addEventListener("pointerup", onPointerUp);
496 target.addEventListener("focus", onFocus);
497 target.addEventListener("blur", onPointerLeave);
498
499 let observer = null;
500 if (options.pauseOffscreen && "IntersectionObserver" in window) {
501 observer = new IntersectionObserver((entries) => {
502 const entry = entries[0];
503 isVisible = !!(entry && entry.isIntersecting);
504
505 if (!isVisible) {
506 isHovering = false;
507 resetTargets();
508 start();
509 }
510 });
511
512 observer.observe(target);
513 }
514
515 const destroy = () => {
516 if (options.triggerMode === "bounds") {
517 target.removeEventListener("pointerenter", onPointerEnter);
518 target.removeEventListener("pointermove", onPointerMove);
519 target.removeEventListener("pointerleave", onPointerLeave);
520 } else {
521 window.removeEventListener("pointermove", onPointerMoveWindow);
522 }
523
524 target.removeEventListener("pointerdown", onPointerDown);
525 window.removeEventListener("pointerup", onPointerUp);
526 target.removeEventListener("focus", onFocus);
527 target.removeEventListener("blur", onPointerLeave);
528
529 if (observer) {
530 observer.disconnect();
531 observer = null;
532 }
533
534 if (rafId) {
535 window.cancelAnimationFrame(rafId);
536 rafId = 0;
537 }
538
539 target.style.transform = "";
540 if (innerTarget) {
541 innerTarget.style.transform = "";
542 }
543
544 delete target.dataset.magneticInit;
545 target.removeAttribute("data-magnetic-init");
546 delete target.__kaMagneticInstance;
547 };
548
549 target.__kaMagneticInstance = { destroy };
550 };
551
552 const initMagneticButtons = ($scope) => {
553 $scope.find(".king-addons-magnetic-buttons").each(function () {
554 const root = this;
555 const rawOptions = parseOptions(root);
556 const options = normalizeOptions(rawOptions);
557 const targetType = root.dataset.target || "button";
558 const targets = resolveTargets(root, options);
559
560 targets.forEach((target) => {
561 initMagneticTarget(root, target, targetType, options);
562 });
563 });
564 };
565
566 $(window).on("elementor/frontend/init", function () {
567 elementorFrontend.hooks.addAction(
568 "frontend/element_ready/king-addons-magnetic-buttons.default",
569 function ($scope) {
570 initMagneticButtons($scope);
571 }
572 );
573 });
574 })(jQuery);
575