# king-addons/51.1.78/includes/widgets/Scrollytelling_Slides/script.js

King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder, version 51.1.78. 591 lines.

- Page: https://pluginprobe.com/plugins/king-addons/51.1.78/code/includes/widgets/Scrollytelling_Slides/script.js
- Raw: https://pluginprobe.com/plugins/king-addons/51.1.78/raw/includes/widgets/Scrollytelling_Slides/script.js
- Modified: 2026-08-25T03:27:10+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/king-addons/51.1.78/code/includes/widgets/Scrollytelling_Slides/script.js#L10-L20`.

```javascript
"use strict";

(function ($) {
    const clamp = (value, min, max) => Math.min(Math.max(value, min), max);

    const parseOptions = (root) => {
        if (!root || !root.dataset) {
            return {};
        }

        try {
            return JSON.parse(root.dataset.options || "{}");
        } catch (e) {
            return {};
        }
    };

    const parseJSON = (value) => {
        if (!value) {
            return {};
        }

        try {
            return JSON.parse(value);
        } catch (e) {
            return {};
        }
    };

    const getClosestIndex = (slides, offsetRatio) => {
        const targetY = window.innerHeight * offsetRatio;
        let closestIndex = 0;
        let closestDistance = Number.POSITIVE_INFINITY;

        slides.forEach((slide, index) => {
            const rect = slide.getBoundingClientRect();
            const distance = Math.abs(rect.top - targetY);
            if (distance < closestDistance) {
                closestDistance = distance;
                closestIndex = index;
            }
        });

        return closestIndex;
    };

    const getLottieLibrary = () => {
        if (typeof lottie !== "undefined") {
            return lottie;
        }

        if (typeof window.lottie !== "undefined") {
            return window.lottie;
        }

        if (typeof window.Lottie !== "undefined") {
            return window.Lottie;
        }

        return null;
    };

    const getScrollMarginPx = (root) => {
        if (!root) {
            return 0;
        }

        const rawValue = window
            .getComputedStyle(root)
            .getPropertyValue("--kng-sly-scroll-margin")
            .trim();

        if (!rawValue) {
            return 0;
        }

        const match = rawValue.match(/^(-?[\d.]+)(px|vh|vw|rem|em)?$/);
        if (!match) {
            return 0;
        }

        const value = Number(match[1]);
        if (!Number.isFinite(value)) {
            return 0;
        }

        const unit = match[2] || "px";
        if (unit === "vh") {
            return (window.innerHeight * value) / 100;
        }

        if (unit === "vw") {
            return (window.innerWidth * value) / 100;
        }

        if (unit === "rem") {
            const base = Number(
                window.getComputedStyle(document.documentElement).fontSize || 16
            );
            return base * value;
        }

        if (unit === "em") {
            const base = Number(window.getComputedStyle(root).fontSize || 16);
            return base * value;
        }

        return value;
    };

    const initScrollytelling = (root) => {
        if (!root || root.dataset.scrollyInit === "yes") {
            return;
        }

        const slides = Array.from(root.querySelectorAll(".king-addons-scrollytelling__slide"));
        if (!slides.length) {
            return;
        }

        root.dataset.scrollyInit = "yes";

        const dots = slides.map((slide) => slide.querySelector(".king-addons-scrollytelling__dot"));
        const options = parseOptions(root);
        const offset = clamp(Number(options.offset) || 40, 10, 80);
        const offsetRatio = offset / 100;
        const clickableDots = !!options.clickableDots;
        const updateHash = !!options.updateHash;
        const readHash = !!options.readHash;
        const sticky = !!options.sticky;
        const snapMode = options.snap || "off";
        const snapDuration = clamp(Number(options.snapDuration) || 420, 150, 2000);
        const lottieActiveOnly = options.lottieActiveOnly !== false;
        const prefersReduced =
            window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
        const isEditor =
            window.elementorFrontend &&
            elementorFrontend.isEditMode &&
            elementorFrontend.isEditMode();
        const mediaPanel = root.querySelector(".king-addons-scrollytelling__media-sticky");
        const shouldLazy = !isEditor;

        let activeIndex = -1;
        let ticking = false;
        let scrollMarginPx = getScrollMarginPx(root);
        let snapTimeout = null;
        let snapFrame = null;
        let lottieLib = getLottieLibrary();

        const lottieInstances = new Map();
        let allLottiesInitialized = false;

        const parseNumber = (value) => {
            if (value === "" || value === null || value === undefined) {
                return null;
            }

            const numberValue = Number(value);
            return Number.isFinite(numberValue) ? numberValue : null;
        };

        const isWidgetInView = () => {
            const rect = root.getBoundingClientRect();
            return rect.bottom > 0 && rect.top < window.innerHeight;
        };

        const updateScrollMargin = () => {
            scrollMarginPx = getScrollMarginPx(root);
        };

        const getSlideScrollY = (slide) => {
            const rect = slide.getBoundingClientRect();
            return window.pageYOffset + rect.top - scrollMarginPx;
        };

        const scrollToPosition = (targetY, duration) => {
            const maxScroll = Math.max(
                0,
                document.documentElement.scrollHeight - window.innerHeight
            );
            const target = clamp(targetY, 0, maxScroll);

            if (snapFrame) {
                window.cancelAnimationFrame(snapFrame);
                snapFrame = null;
            }

            if (duration <= 0 || prefersReduced) {
                window.scrollTo(0, target);
                return;
            }

            const startY = window.pageYOffset;
            const distance = target - startY;
            const startTime = window.performance.now();

            const animate = (now) => {
                const progress = clamp((now - startTime) / duration, 0, 1);
                const eased = 1 - Math.pow(1 - progress, 3);
                window.scrollTo(0, startY + distance * eased);

                if (progress < 1) {
                    snapFrame = window.requestAnimationFrame(animate);
                }
            };

            snapFrame = window.requestAnimationFrame(animate);
        };

        const scrollToSlide = (slide, duration) => {
            if (!slide) {
                return;
            }

            scrollToPosition(getSlideScrollY(slide), duration);
        };

        const loadIframesInElement = (element) => {
            if (!element) {
                return;
            }

            element.querySelectorAll("iframe[data-src]").forEach((iframe) => {
                if (iframe.dataset.loaded === "yes") {
                    return;
                }

                const src = iframe.getAttribute("data-src");
                if (!src) {
                    return;
                }

                iframe.setAttribute("src", src);
                iframe.dataset.loaded = "yes";
            });
        };

        const preloadIframesAround = (index) => {
            if (!shouldLazy) {
                slides.forEach((slide) => loadIframesInElement(slide));
                loadIframesInElement(mediaPanel);
                return;
            }

            const start = Math.max(0, index - 1);
            const end = Math.min(slides.length - 1, index + 1);
            for (let i = start; i <= end; i += 1) {
                loadIframesInElement(slides[i]);
            }

            loadIframesInElement(mediaPanel);
        };

        const playLottieInstance = (entry) => {
            const start = parseNumber(entry.settings.segmentStart);
            const end = parseNumber(entry.settings.segmentEnd);

            if (start !== null && end !== null && end > start) {
                entry.animation.playSegments([start, end], true);
            } else {
                entry.animation.play();
            }
        };

        const initLottieContainer = (container, index) => {
            if (!container || container.dataset.lottieInit === "yes" || !lottieLib) {
                return;
            }

            const jsonUrl = container.dataset.jsonUrl || "";
            if (!jsonUrl) {
                return;
            }

            const settings = parseJSON(container.dataset.settings || "{}");
            const loop = settings.loop === true || settings.loop === "yes";
            const speed = Number(settings.speed) > 0 ? Number(settings.speed) : 1;

            const animation = lottieLib.loadAnimation({
                container,
                path: jsonUrl,
                renderer: settings.renderer || "svg",
                loop,
                autoplay: !lottieActiveOnly && !prefersReduced,
            });

            animation.setSpeed(speed);
            container.dataset.lottieInit = "yes";
            lottieInstances.set(container, {
                animation,
                settings,
                index,
                isActive: false,
            });

            if (prefersReduced || lottieActiveOnly) {
                animation.pause();
            }
        };

        const initLottieInElement = (element, index) => {
            if (!element) {
                return;
            }

            const container = element.querySelector(".king-addons-scrollytelling__lottie");
            if (!container) {
                return;
            }

            initLottieContainer(container, index);
        };

        const initNearbyLotties = (index) => {
            if (!lottieLib) {
                return;
            }

            if (!lottieActiveOnly) {
                if (!allLottiesInitialized) {
                    slides.forEach((slide, slideIndex) => initLottieInElement(slide, slideIndex));
                    allLottiesInitialized = true;
                }
                initLottieInElement(mediaPanel, index);
                return;
            }

            const start = Math.max(0, index - 1);
            const end = Math.min(slides.length - 1, index + 1);
            for (let i = start; i <= end; i += 1) {
                initLottieInElement(slides[i], i);
            }

            initLottieInElement(mediaPanel, index);
        };

        const updateLottiePlayback = (index) => {
            if (!lottieLib) {
                return;
            }

            lottieInstances.forEach((entry) => {
                const shouldBeActive = entry.index === index;

                if (prefersReduced) {
                    entry.animation.pause();
                    entry.isActive = shouldBeActive;
                    return;
                }

                if (lottieActiveOnly) {
                    if (entry.isActive === shouldBeActive) {
                        return;
                    }

                    entry.isActive = shouldBeActive;
                    if (shouldBeActive) {
                        playLottieInstance(entry);
                    } else {
                        entry.animation.pause();
                    }
                    return;
                }

                if (!entry.isActive) {
                    entry.isActive = true;
                }

                if (entry.settings.autoplay !== "no") {
                    entry.animation.play();
                }
            });
        };

        const handleMediaUpdate = (index) => {
            preloadIframesAround(index);
            initNearbyLotties(index);
            updateLottiePlayback(index);
        };

        const getMediaMarkup = (index) => {
            if (!slides[index]) {
                return "";
            }

            const media = slides[index].querySelector(".king-addons-scrollytelling__media");
            if (!media) {
                return "";
            }

            return media.innerHTML.trim();
        };

        const updateStickyMedia = (index) => {
            if (!sticky || !mediaPanel) {
                return;
            }

            const markup = getMediaMarkup(index);
            if (!markup || mediaPanel.dataset.index === String(index)) {
                return;
            }

            mediaPanel.innerHTML = markup;
            mediaPanel.dataset.index = String(index);
            loadIframesInElement(mediaPanel);
            initLottieInElement(mediaPanel, index);
        };

        const setActive = (nextIndex, shouldUpdateHash = true) => {
            const safeIndex = clamp(nextIndex, 0, slides.length - 1);
            if (safeIndex === activeIndex) {
                return;
            }

            activeIndex = safeIndex;

            slides.forEach((slide, index) => {
                slide.classList.toggle("is-active", index === activeIndex);
                slide.classList.toggle("is-completed", index < activeIndex);
                slide.classList.toggle("is-upcoming", index > activeIndex);

                const dot = dots[index];
                if (!dot) {
                    return;
                }

                if (index === activeIndex) {
                    dot.setAttribute("aria-current", "step");
                } else {
                    dot.removeAttribute("aria-current");
                }
            });

            updateStickyMedia(activeIndex);
            handleMediaUpdate(activeIndex);

            if (!updateHash || isEditor || !shouldUpdateHash || !isWidgetInView()) {
                return;
            }

            const anchor = slides[activeIndex].dataset.anchor || "";
            if (!anchor) {
                return;
            }

            const newHash = `#${encodeURIComponent(anchor)}`;
            if (window.location.hash !== newHash) {
                window.history.replaceState(null, "", newHash);
            }
        };

        const updateFromScroll = () => {
            const nextIndex = getClosestIndex(slides, offsetRatio);
            setActive(nextIndex);
        };

        const scheduleUpdate = () => {
            if (ticking) {
                return;
            }

            ticking = true;
            window.requestAnimationFrame(() => {
                updateFromScroll();
                ticking = false;
            });
        };

        const scheduleSnap = () => {
            if (snapMode === "off" || prefersReduced || isEditor) {
                return;
            }

            if (!isWidgetInView()) {
                return;
            }

            if (snapTimeout) {
                window.clearTimeout(snapTimeout);
            }

            snapTimeout = window.setTimeout(() => {
                if (!isWidgetInView()) {
                    return;
                }

                const nextIndex = getClosestIndex(slides, offsetRatio);
                const target = slides[nextIndex];
                if (!target) {
                    return;
                }

                const targetY = getSlideScrollY(target);
                const distance = Math.abs(window.pageYOffset - targetY);
                if (snapMode === "soft" && distance > window.innerHeight * 0.35) {
                    return;
                }

                scrollToPosition(targetY, snapDuration);
            }, 120);
        };

        const onScroll = () => {
            scheduleUpdate();
            scheduleSnap();
        };

        window.addEventListener("scroll", onScroll, { passive: true });
        window.addEventListener("resize", () => {
            updateScrollMargin();
            scheduleUpdate();
        });

        updateScrollMargin();
        scheduleUpdate();

        if ("IntersectionObserver" in window) {
            const observer = new IntersectionObserver(scheduleUpdate, {
                root: null,
                rootMargin: "0px",
                threshold: 0,
            });

            slides.forEach((slide) => observer.observe(slide));
        }

        if (clickableDots) {
            dots.forEach((dot, index) => {
                if (!dot) {
                    return;
                }

                dot.addEventListener("click", (event) => {
                    event.preventDefault();
                    const target = slides[index];
                    if (!target) {
                        return;
                    }

                    scrollToSlide(target, snapDuration);
                });
            });
        }

        if (readHash && !isEditor && window.location.hash) {
            const hash = decodeURIComponent(window.location.hash.replace("#", ""));
            const targetIndex = slides.findIndex(
                (slide) => (slide.dataset.anchor || "") === hash
            );

            if (targetIndex >= 0) {
                window.setTimeout(() => {
                    scrollToSlide(slides[targetIndex], snapDuration);
                    setActive(targetIndex, false);
                }, 100);
            }
        }

        if (root.querySelector(".king-addons-scrollytelling__lottie") && !lottieLib) {
            let attempts = 0;
            const timer = window.setInterval(() => {
                attempts += 1;
                lottieLib = getLottieLibrary();
                if (lottieLib || attempts >= 20) {
                    window.clearInterval(timer);
                    if (lottieLib) {
                        initNearbyLotties(activeIndex >= 0 ? activeIndex : 0);
                        updateLottiePlayback(activeIndex >= 0 ? activeIndex : 0);
                    }
                }
            }, 200);
        }
    };

    const initScrollytellingWidgets = ($scope) => {
        $scope.find(".king-addons-scrollytelling").each(function () {
            initScrollytelling(this);
        });
    };

    $(window).on("elementor/frontend/init", function () {
        elementorFrontend.hooks.addAction(
            "frontend/element_ready/king-addons-scrollytelling-slides.default",
            function ($scope) {
                initScrollytellingWidgets($scope);
            }
        );
    });
})(jQuery);

```
