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

script.js in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.83, at includes/widgets/Spotlight_Reveal/script.js

333 lines 10.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 const globalPointerHandlers = new Set();
6 let globalPointerActive = false;
7
8 const onGlobalPointerMove = (event) => {
9 globalPointerHandlers.forEach((handler) => handler(event));
10 };
11
12 const registerGlobalPointer = (handler) => {
13 globalPointerHandlers.add(handler);
14 if (!globalPointerActive) {
15 window.addEventListener("pointermove", onGlobalPointerMove);
16 globalPointerActive = true;
17 }
18 };
19
20 const parseOptions = (root) => {
21 if (!root || !root.dataset) {
22 return {};
23 }
24
25 try {
26 return JSON.parse(root.dataset.options || "{}");
27 } catch (e) {
28 return {};
29 }
30 };
31
32 const getUnit = (value) => {
33 if (!value) {
34 return "";
35 }
36 const match = String(value).trim().match(/[a-z%]+$/i);
37 return match ? match[0] : "";
38 };
39
40 const toPixels = (value, root, rect) => {
41 if (!value) {
42 return 0;
43 }
44
45 const number = parseFloat(value);
46 if (Number.isNaN(number)) {
47 return 0;
48 }
49
50 const unit = getUnit(value);
51 if (unit === "vw") {
52 return (window.innerWidth * number) / 100;
53 }
54
55 if (unit === "vh") {
56 return (window.innerHeight * number) / 100;
57 }
58
59 if (unit === "%") {
60 const basis = rect ? Math.min(rect.width, rect.height) : 0;
61 return (basis * number) / 100;
62 }
63
64 if (unit === "rem") {
65 const rootSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
66 return number * rootSize;
67 }
68
69 if (unit === "em") {
70 const rootSize = parseFloat(getComputedStyle(root).fontSize) || 16;
71 return number * rootSize;
72 }
73
74 return number;
75 };
76
77 const getMaskRadius = (root, rect) => {
78 const style = window.getComputedStyle(root);
79 const sizeValue = style.getPropertyValue("--ka-spotlight-size").trim();
80 const sizePx = toPixels(sizeValue, root, rect);
81 return sizePx / 2;
82 };
83
84 const initSpotlightReveal = (root) => {
85 if (!root || root.dataset.spotlightInit === "yes") {
86 return;
87 }
88
89 root.dataset.spotlightInit = "yes";
90
91 const options = parseOptions(root);
92 const trigger = options.trigger === "scroll" ? "scroll" : "cursor";
93 const hoverOnly = !(options.hoverOnly === false || options.hoverOnly === "no" || options.hoverOnly === 0);
94 const constrain = !(options.constrain === false || options.constrain === "no" || options.constrain === 0);
95 const smoothingInput = clamp(Number(options.smoothing) || 0, 0, 1);
96 const speed = 1 - smoothingInput * 0.9;
97 const startPercentX = clamp(Number(options.startX) || 50, 0, 100);
98 const startPercentY = clamp(Number(options.startY) || 50, 0, 100);
99 const scrollStart = clamp(Number(options.scrollStart) || 0, 0, 100);
100 const scrollEnd = clamp(Number(options.scrollEnd) || 0, 0, 100);
101
102 const isEditor =
103 window.elementorFrontend &&
104 elementorFrontend.isEditMode &&
105 elementorFrontend.isEditMode();
106 const activeTrigger = isEditor && trigger === "scroll" ? "cursor" : trigger;
107
108 let bounds = root.getBoundingClientRect();
109 let maskRadius = getMaskRadius(root, bounds);
110 let startPosition = {
111 x: (bounds.width * startPercentX) / 100,
112 y: (bounds.height * startPercentY) / 100,
113 };
114
115 let currentX = startPosition.x;
116 let currentY = startPosition.y;
117 let targetX = startPosition.x;
118 let targetY = startPosition.y;
119 let rafId = 0;
120 let lastFrame = 0;
121 let isHovering = false;
122 let inView = true;
123 const frameInterval = isEditor ? 1000 / 30 : 0;
124
125 const prefersReduced =
126 window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
127
128 const applyPosition = () => {
129 root.style.setProperty("--ka-spotlight-x", `${currentX}px`);
130 root.style.setProperty("--ka-spotlight-y", `${currentY}px`);
131 };
132
133 const refreshMetrics = () => {
134 bounds = root.getBoundingClientRect();
135 maskRadius = getMaskRadius(root, bounds);
136 startPosition = {
137 x: (bounds.width * startPercentX) / 100,
138 y: (bounds.height * startPercentY) / 100,
139 };
140 };
141
142 const clampPosition = (x, y) => {
143 if (!constrain) {
144 return { x, y };
145 }
146
147 let clampedX = x;
148 let clampedY = y;
149 const maxX = bounds.width - maskRadius;
150 const maxY = bounds.height - maskRadius;
151
152 if (maxX <= maskRadius) {
153 clampedX = bounds.width / 2;
154 } else {
155 clampedX = clamp(x, maskRadius, maxX);
156 }
157
158 if (maxY <= maskRadius) {
159 clampedY = bounds.height / 2;
160 } else {
161 clampedY = clamp(y, maskRadius, maxY);
162 }
163
164 return { x: clampedX, y: clampedY };
165 };
166
167 const updateTarget = (x, y) => {
168 const clamped = clampPosition(x, y);
169 targetX = clamped.x;
170 targetY = clamped.y;
171 };
172
173 const animate = (timestamp) => {
174 if (frameInterval && timestamp - lastFrame < frameInterval) {
175 rafId = window.requestAnimationFrame(animate);
176 return;
177 }
178
179 lastFrame = timestamp;
180
181 currentX += (targetX - currentX) * speed;
182 currentY += (targetY - currentY) * speed;
183
184 if (Math.abs(targetX - currentX) < 0.2) {
185 currentX = targetX;
186 }
187
188 if (Math.abs(targetY - currentY) < 0.2) {
189 currentY = targetY;
190 }
191
192 applyPosition();
193
194 if (currentX === targetX && currentY === targetY) {
195 rafId = 0;
196 return;
197 }
198
199 rafId = window.requestAnimationFrame(animate);
200 };
201
202 const startAnimation = () => {
203 if (!rafId) {
204 rafId = window.requestAnimationFrame(animate);
205 }
206 };
207
208 const resetToStart = () => {
209 updateTarget(startPosition.x, startPosition.y);
210 startAnimation();
211 };
212
213 const updateTargetFromPointer = (clientX, clientY) => {
214 const x = clientX - bounds.left;
215 const y = clientY - bounds.top;
216 updateTarget(x, y);
217 startAnimation();
218 };
219
220 const updateTargetFromScroll = () => {
221 if (!inView) {
222 return;
223 }
224
225 const rect = root.getBoundingClientRect();
226 bounds = rect;
227 const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
228 const startOffset = (viewportHeight * scrollStart) / 100;
229 const endOffset = (viewportHeight * scrollEnd) / 100;
230 const total = rect.height + viewportHeight - startOffset - endOffset;
231 const progress = total > 0 ? (viewportHeight - rect.top - startOffset) / total : 0;
232 const clamped = clamp(progress, 0, 1);
233
234 updateTarget(rect.width * 0.5, rect.height * clamped);
235 startAnimation();
236 };
237
238 const onResize = () => {
239 refreshMetrics();
240 if (activeTrigger === "scroll") {
241 updateTargetFromScroll();
242 } else if (!isHovering) {
243 resetToStart();
244 }
245 };
246
247 refreshMetrics();
248 applyPosition();
249
250 if (prefersReduced) {
251 root.classList.add("king-addons-spotlight-reveal--active");
252 return;
253 }
254
255 if (activeTrigger === "cursor") {
256 const onPointerEnter = (event) => {
257 if (event.pointerType === "touch") {
258 return;
259 }
260 isHovering = true;
261 refreshMetrics();
262 updateTargetFromPointer(event.clientX, event.clientY);
263 };
264
265 const onPointerMove = (event) => {
266 if (event.pointerType === "touch") {
267 return;
268 }
269 if (hoverOnly && !isHovering) {
270 return;
271 }
272 updateTargetFromPointer(event.clientX, event.clientY);
273 };
274
275 const onPointerLeave = () => {
276 isHovering = false;
277 if (hoverOnly) {
278 resetToStart();
279 }
280 };
281
282 root.addEventListener("pointerenter", onPointerEnter);
283 if (hoverOnly) {
284 root.addEventListener("pointermove", onPointerMove);
285 root.addEventListener("pointerleave", onPointerLeave);
286 } else {
287 registerGlobalPointer(onPointerMove);
288 }
289 }
290
291 if (activeTrigger === "scroll") {
292 if (typeof IntersectionObserver !== "undefined") {
293 const observer = new IntersectionObserver((entries) => {
294 entries.forEach((entry) => {
295 if (entry.target !== root) {
296 return;
297 }
298 inView = entry.isIntersecting;
299 if (inView) {
300 updateTargetFromScroll();
301 }
302 });
303 });
304
305 observer.observe(root);
306 } else {
307 inView = true;
308 }
309
310 window.addEventListener("scroll", updateTargetFromScroll, { passive: true });
311 updateTargetFromScroll();
312 }
313
314 window.addEventListener("resize", onResize, { passive: true });
315 root.classList.add("king-addons-spotlight-reveal--active");
316 };
317
318 const initSpotlightReveals = ($scope) => {
319 $scope.find(".king-addons-spotlight-reveal").each(function () {
320 initSpotlightReveal(this);
321 });
322 };
323
324 $(window).on("elementor/frontend/init", function () {
325 elementorFrontend.hooks.addAction(
326 "frontend/element_ready/king-addons-spotlight-reveal.default",
327 function ($scope) {
328 initSpotlightReveals($scope);
329 }
330 );
331 });
332 })(jQuery);
333