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

591 lines 17.5 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 parseJSON = (value) => {
19 if (!value) {
20 return {};
21 }
22
23 try {
24 return JSON.parse(value);
25 } catch (e) {
26 return {};
27 }
28 };
29
30 const getClosestIndex = (slides, offsetRatio) => {
31 const targetY = window.innerHeight * offsetRatio;
32 let closestIndex = 0;
33 let closestDistance = Number.POSITIVE_INFINITY;
34
35 slides.forEach((slide, index) => {
36 const rect = slide.getBoundingClientRect();
37 const distance = Math.abs(rect.top - targetY);
38 if (distance < closestDistance) {
39 closestDistance = distance;
40 closestIndex = index;
41 }
42 });
43
44 return closestIndex;
45 };
46
47 const getLottieLibrary = () => {
48 if (typeof lottie !== "undefined") {
49 return lottie;
50 }
51
52 if (typeof window.lottie !== "undefined") {
53 return window.lottie;
54 }
55
56 if (typeof window.Lottie !== "undefined") {
57 return window.Lottie;
58 }
59
60 return null;
61 };
62
63 const getScrollMarginPx = (root) => {
64 if (!root) {
65 return 0;
66 }
67
68 const rawValue = window
69 .getComputedStyle(root)
70 .getPropertyValue("--kng-sly-scroll-margin")
71 .trim();
72
73 if (!rawValue) {
74 return 0;
75 }
76
77 const match = rawValue.match(/^(-?[\d.]+)(px|vh|vw|rem|em)?$/);
78 if (!match) {
79 return 0;
80 }
81
82 const value = Number(match[1]);
83 if (!Number.isFinite(value)) {
84 return 0;
85 }
86
87 const unit = match[2] || "px";
88 if (unit === "vh") {
89 return (window.innerHeight * value) / 100;
90 }
91
92 if (unit === "vw") {
93 return (window.innerWidth * value) / 100;
94 }
95
96 if (unit === "rem") {
97 const base = Number(
98 window.getComputedStyle(document.documentElement).fontSize || 16
99 );
100 return base * value;
101 }
102
103 if (unit === "em") {
104 const base = Number(window.getComputedStyle(root).fontSize || 16);
105 return base * value;
106 }
107
108 return value;
109 };
110
111 const initScrollytelling = (root) => {
112 if (!root || root.dataset.scrollyInit === "yes") {
113 return;
114 }
115
116 const slides = Array.from(root.querySelectorAll(".king-addons-scrollytelling__slide"));
117 if (!slides.length) {
118 return;
119 }
120
121 root.dataset.scrollyInit = "yes";
122
123 const dots = slides.map((slide) => slide.querySelector(".king-addons-scrollytelling__dot"));
124 const options = parseOptions(root);
125 const offset = clamp(Number(options.offset) || 40, 10, 80);
126 const offsetRatio = offset / 100;
127 const clickableDots = !!options.clickableDots;
128 const updateHash = !!options.updateHash;
129 const readHash = !!options.readHash;
130 const sticky = !!options.sticky;
131 const snapMode = options.snap || "off";
132 const snapDuration = clamp(Number(options.snapDuration) || 420, 150, 2000);
133 const lottieActiveOnly = options.lottieActiveOnly !== false;
134 const prefersReduced =
135 window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
136 const isEditor =
137 window.elementorFrontend &&
138 elementorFrontend.isEditMode &&
139 elementorFrontend.isEditMode();
140 const mediaPanel = root.querySelector(".king-addons-scrollytelling__media-sticky");
141 const shouldLazy = !isEditor;
142
143 let activeIndex = -1;
144 let ticking = false;
145 let scrollMarginPx = getScrollMarginPx(root);
146 let snapTimeout = null;
147 let snapFrame = null;
148 let lottieLib = getLottieLibrary();
149
150 const lottieInstances = new Map();
151 let allLottiesInitialized = false;
152
153 const parseNumber = (value) => {
154 if (value === "" || value === null || value === undefined) {
155 return null;
156 }
157
158 const numberValue = Number(value);
159 return Number.isFinite(numberValue) ? numberValue : null;
160 };
161
162 const isWidgetInView = () => {
163 const rect = root.getBoundingClientRect();
164 return rect.bottom > 0 && rect.top < window.innerHeight;
165 };
166
167 const updateScrollMargin = () => {
168 scrollMarginPx = getScrollMarginPx(root);
169 };
170
171 const getSlideScrollY = (slide) => {
172 const rect = slide.getBoundingClientRect();
173 return window.pageYOffset + rect.top - scrollMarginPx;
174 };
175
176 const scrollToPosition = (targetY, duration) => {
177 const maxScroll = Math.max(
178 0,
179 document.documentElement.scrollHeight - window.innerHeight
180 );
181 const target = clamp(targetY, 0, maxScroll);
182
183 if (snapFrame) {
184 window.cancelAnimationFrame(snapFrame);
185 snapFrame = null;
186 }
187
188 if (duration <= 0 || prefersReduced) {
189 window.scrollTo(0, target);
190 return;
191 }
192
193 const startY = window.pageYOffset;
194 const distance = target - startY;
195 const startTime = window.performance.now();
196
197 const animate = (now) => {
198 const progress = clamp((now - startTime) / duration, 0, 1);
199 const eased = 1 - Math.pow(1 - progress, 3);
200 window.scrollTo(0, startY + distance * eased);
201
202 if (progress < 1) {
203 snapFrame = window.requestAnimationFrame(animate);
204 }
205 };
206
207 snapFrame = window.requestAnimationFrame(animate);
208 };
209
210 const scrollToSlide = (slide, duration) => {
211 if (!slide) {
212 return;
213 }
214
215 scrollToPosition(getSlideScrollY(slide), duration);
216 };
217
218 const loadIframesInElement = (element) => {
219 if (!element) {
220 return;
221 }
222
223 element.querySelectorAll("iframe[data-src]").forEach((iframe) => {
224 if (iframe.dataset.loaded === "yes") {
225 return;
226 }
227
228 const src = iframe.getAttribute("data-src");
229 if (!src) {
230 return;
231 }
232
233 iframe.setAttribute("src", src);
234 iframe.dataset.loaded = "yes";
235 });
236 };
237
238 const preloadIframesAround = (index) => {
239 if (!shouldLazy) {
240 slides.forEach((slide) => loadIframesInElement(slide));
241 loadIframesInElement(mediaPanel);
242 return;
243 }
244
245 const start = Math.max(0, index - 1);
246 const end = Math.min(slides.length - 1, index + 1);
247 for (let i = start; i <= end; i += 1) {
248 loadIframesInElement(slides[i]);
249 }
250
251 loadIframesInElement(mediaPanel);
252 };
253
254 const playLottieInstance = (entry) => {
255 const start = parseNumber(entry.settings.segmentStart);
256 const end = parseNumber(entry.settings.segmentEnd);
257
258 if (start !== null && end !== null && end > start) {
259 entry.animation.playSegments([start, end], true);
260 } else {
261 entry.animation.play();
262 }
263 };
264
265 const initLottieContainer = (container, index) => {
266 if (!container || container.dataset.lottieInit === "yes" || !lottieLib) {
267 return;
268 }
269
270 const jsonUrl = container.dataset.jsonUrl || "";
271 if (!jsonUrl) {
272 return;
273 }
274
275 const settings = parseJSON(container.dataset.settings || "{}");
276 const loop = settings.loop === true || settings.loop === "yes";
277 const speed = Number(settings.speed) > 0 ? Number(settings.speed) : 1;
278
279 const animation = lottieLib.loadAnimation({
280 container,
281 path: jsonUrl,
282 renderer: settings.renderer || "svg",
283 loop,
284 autoplay: !lottieActiveOnly && !prefersReduced,
285 });
286
287 animation.setSpeed(speed);
288 container.dataset.lottieInit = "yes";
289 lottieInstances.set(container, {
290 animation,
291 settings,
292 index,
293 isActive: false,
294 });
295
296 if (prefersReduced || lottieActiveOnly) {
297 animation.pause();
298 }
299 };
300
301 const initLottieInElement = (element, index) => {
302 if (!element) {
303 return;
304 }
305
306 const container = element.querySelector(".king-addons-scrollytelling__lottie");
307 if (!container) {
308 return;
309 }
310
311 initLottieContainer(container, index);
312 };
313
314 const initNearbyLotties = (index) => {
315 if (!lottieLib) {
316 return;
317 }
318
319 if (!lottieActiveOnly) {
320 if (!allLottiesInitialized) {
321 slides.forEach((slide, slideIndex) => initLottieInElement(slide, slideIndex));
322 allLottiesInitialized = true;
323 }
324 initLottieInElement(mediaPanel, index);
325 return;
326 }
327
328 const start = Math.max(0, index - 1);
329 const end = Math.min(slides.length - 1, index + 1);
330 for (let i = start; i <= end; i += 1) {
331 initLottieInElement(slides[i], i);
332 }
333
334 initLottieInElement(mediaPanel, index);
335 };
336
337 const updateLottiePlayback = (index) => {
338 if (!lottieLib) {
339 return;
340 }
341
342 lottieInstances.forEach((entry) => {
343 const shouldBeActive = entry.index === index;
344
345 if (prefersReduced) {
346 entry.animation.pause();
347 entry.isActive = shouldBeActive;
348 return;
349 }
350
351 if (lottieActiveOnly) {
352 if (entry.isActive === shouldBeActive) {
353 return;
354 }
355
356 entry.isActive = shouldBeActive;
357 if (shouldBeActive) {
358 playLottieInstance(entry);
359 } else {
360 entry.animation.pause();
361 }
362 return;
363 }
364
365 if (!entry.isActive) {
366 entry.isActive = true;
367 }
368
369 if (entry.settings.autoplay !== "no") {
370 entry.animation.play();
371 }
372 });
373 };
374
375 const handleMediaUpdate = (index) => {
376 preloadIframesAround(index);
377 initNearbyLotties(index);
378 updateLottiePlayback(index);
379 };
380
381 const getMediaMarkup = (index) => {
382 if (!slides[index]) {
383 return "";
384 }
385
386 const media = slides[index].querySelector(".king-addons-scrollytelling__media");
387 if (!media) {
388 return "";
389 }
390
391 return media.innerHTML.trim();
392 };
393
394 const updateStickyMedia = (index) => {
395 if (!sticky || !mediaPanel) {
396 return;
397 }
398
399 const markup = getMediaMarkup(index);
400 if (!markup || mediaPanel.dataset.index === String(index)) {
401 return;
402 }
403
404 mediaPanel.innerHTML = markup;
405 mediaPanel.dataset.index = String(index);
406 loadIframesInElement(mediaPanel);
407 initLottieInElement(mediaPanel, index);
408 };
409
410 const setActive = (nextIndex, shouldUpdateHash = true) => {
411 const safeIndex = clamp(nextIndex, 0, slides.length - 1);
412 if (safeIndex === activeIndex) {
413 return;
414 }
415
416 activeIndex = safeIndex;
417
418 slides.forEach((slide, index) => {
419 slide.classList.toggle("is-active", index === activeIndex);
420 slide.classList.toggle("is-completed", index < activeIndex);
421 slide.classList.toggle("is-upcoming", index > activeIndex);
422
423 const dot = dots[index];
424 if (!dot) {
425 return;
426 }
427
428 if (index === activeIndex) {
429 dot.setAttribute("aria-current", "step");
430 } else {
431 dot.removeAttribute("aria-current");
432 }
433 });
434
435 updateStickyMedia(activeIndex);
436 handleMediaUpdate(activeIndex);
437
438 if (!updateHash || isEditor || !shouldUpdateHash || !isWidgetInView()) {
439 return;
440 }
441
442 const anchor = slides[activeIndex].dataset.anchor || "";
443 if (!anchor) {
444 return;
445 }
446
447 const newHash = `#${encodeURIComponent(anchor)}`;
448 if (window.location.hash !== newHash) {
449 window.history.replaceState(null, "", newHash);
450 }
451 };
452
453 const updateFromScroll = () => {
454 const nextIndex = getClosestIndex(slides, offsetRatio);
455 setActive(nextIndex);
456 };
457
458 const scheduleUpdate = () => {
459 if (ticking) {
460 return;
461 }
462
463 ticking = true;
464 window.requestAnimationFrame(() => {
465 updateFromScroll();
466 ticking = false;
467 });
468 };
469
470 const scheduleSnap = () => {
471 if (snapMode === "off" || prefersReduced || isEditor) {
472 return;
473 }
474
475 if (!isWidgetInView()) {
476 return;
477 }
478
479 if (snapTimeout) {
480 window.clearTimeout(snapTimeout);
481 }
482
483 snapTimeout = window.setTimeout(() => {
484 if (!isWidgetInView()) {
485 return;
486 }
487
488 const nextIndex = getClosestIndex(slides, offsetRatio);
489 const target = slides[nextIndex];
490 if (!target) {
491 return;
492 }
493
494 const targetY = getSlideScrollY(target);
495 const distance = Math.abs(window.pageYOffset - targetY);
496 if (snapMode === "soft" && distance > window.innerHeight * 0.35) {
497 return;
498 }
499
500 scrollToPosition(targetY, snapDuration);
501 }, 120);
502 };
503
504 const onScroll = () => {
505 scheduleUpdate();
506 scheduleSnap();
507 };
508
509 window.addEventListener("scroll", onScroll, { passive: true });
510 window.addEventListener("resize", () => {
511 updateScrollMargin();
512 scheduleUpdate();
513 });
514
515 updateScrollMargin();
516 scheduleUpdate();
517
518 if ("IntersectionObserver" in window) {
519 const observer = new IntersectionObserver(scheduleUpdate, {
520 root: null,
521 rootMargin: "0px",
522 threshold: 0,
523 });
524
525 slides.forEach((slide) => observer.observe(slide));
526 }
527
528 if (clickableDots) {
529 dots.forEach((dot, index) => {
530 if (!dot) {
531 return;
532 }
533
534 dot.addEventListener("click", (event) => {
535 event.preventDefault();
536 const target = slides[index];
537 if (!target) {
538 return;
539 }
540
541 scrollToSlide(target, snapDuration);
542 });
543 });
544 }
545
546 if (readHash && !isEditor && window.location.hash) {
547 const hash = decodeURIComponent(window.location.hash.replace("#", ""));
548 const targetIndex = slides.findIndex(
549 (slide) => (slide.dataset.anchor || "") === hash
550 );
551
552 if (targetIndex >= 0) {
553 window.setTimeout(() => {
554 scrollToSlide(slides[targetIndex], snapDuration);
555 setActive(targetIndex, false);
556 }, 100);
557 }
558 }
559
560 if (root.querySelector(".king-addons-scrollytelling__lottie") && !lottieLib) {
561 let attempts = 0;
562 const timer = window.setInterval(() => {
563 attempts += 1;
564 lottieLib = getLottieLibrary();
565 if (lottieLib || attempts >= 20) {
566 window.clearInterval(timer);
567 if (lottieLib) {
568 initNearbyLotties(activeIndex >= 0 ? activeIndex : 0);
569 updateLottiePlayback(activeIndex >= 0 ? activeIndex : 0);
570 }
571 }
572 }, 200);
573 }
574 };
575
576 const initScrollytellingWidgets = ($scope) => {
577 $scope.find(".king-addons-scrollytelling").each(function () {
578 initScrollytelling(this);
579 });
580 };
581
582 $(window).on("elementor/frontend/init", function () {
583 elementorFrontend.hooks.addAction(
584 "frontend/element_ready/king-addons-scrollytelling-slides.default",
585 function ($scope) {
586 initScrollytellingWidgets($scope);
587 }
588 );
589 });
590 })(jQuery);
591