import { useRef, useLayoutEffect } from 'react';
const onBeforeunload = () => {
    window.scrollTo(0, 0);
};
class Stickers {
    scrollContainer;
    offset;
    #scrollDebounce;
    #resizeDebounce;
    #isWindowScroll;
    #scrollIntervalStart;
    #scrollIntervalChange;
    #scrollDirection;
    #priorScrollY;
    #resizeTimeout;
    #scrollEndTimeout;
    #isScrolling;
    #vpHeightSnapshot;
    #sfHeightSnapshot;
    #stickers;
    #scrollHandler;
    #resizeHandler;
    constructor({ elements, scrollContainer = window, offset = 0 }) {
        this.scrollContainer = scrollContainer;
        this.offset = offset;
        this.#scrollDebounce = 100;
        this.#resizeDebounce = 300;
        this.#resizeTimeout = NaN;
        this.#scrollEndTimeout = NaN;
        this.#priorScrollY = NaN;
        this.#isScrolling = false;
        this.#vpHeightSnapshot = NaN;
        this.#sfHeightSnapshot = NaN;
        this.#scrollHandler = () => { };
        this.#resizeHandler = () => { };
        this.#isWindowScroll = this.scrollContainer === window;
        // Only gets reset after scroll stop timeout fires
        this.#scrollIntervalStart = this.getScrollY();
        // Used to determine scroll change within debounced scroll handler
        this.#scrollIntervalChange = 0;
        // Used to determine scroll direction by +1(page down) -1(page up)
        this.#scrollDirection = 0;
        this.#stickers = this.setStickers(elements);
        this.snapshotItemRects();
        this.addHandlers();
        this.testForStick();
    }
    setStickers(elements) {
        this.#stickers = new Set((typeof elements === 'string'
            ? [...document.querySelectorAll(elements)]
            : [...elements]).map(element => ({
            element,
            isStuck: false,
            top: NaN,
            bottom: NaN,
            height: NaN,
            priorStickHeight: NaN
        })));
        return this.#stickers;
    }
    addHandlers() {
        this.rmHandlers();
        this.#scrollHandler = this.onScroll.bind(this);
        this.#resizeHandler = this.onResize.bind(this);
        this.scrollContainer.addEventListener('scroll', this.#scrollHandler);
        window.addEventListener('resize', this.#resizeHandler);
        // Not sure we need this. Maybe when DOM was persisted...
        window.addEventListener('beforeunload', onBeforeunload);
    }
    rmHandlers() {
        this.scrollContainer.removeEventListener('scroll', this.#scrollHandler);
        window.removeEventListener('resize', this.#resizeHandler);
        window.removeEventListener('beforeunload', onBeforeunload);
    }
    snapshotItemRects() {
        this.#stickers.forEach(sticker => {
            const rect = sticker.element.getBoundingClientRect();
            sticker.top = rect.top;
            sticker.bottom = rect.bottom;
            sticker.height = rect.height;
        });
    }
    onScroll() {
        clearTimeout(this.#scrollEndTimeout);
        const currentScrollY = this.getScrollY();
        this.#priorScrollY = !Number.isNaN(this.#priorScrollY)
            ? this.#priorScrollY
            : this.#scrollIntervalStart;
        // Diff between priorScrollY and new currentScrollY to determine direction
        this.#scrollDirection = currentScrollY - this.#priorScrollY > 0 ? 1 : -1;
        // How much scroll changed + or - within the debounce interval
        this.#scrollIntervalChange = currentScrollY - this.#scrollIntervalStart;
        // Update old to new for next scroll event
        this.#priorScrollY = currentScrollY;
        // When scrolling has stopped
        this.#scrollEndTimeout = window.setTimeout(() => {
            // Reset for next scroll starting point
            this.#scrollIntervalStart = this.#priorScrollY;
            // Reset until next scroll
            this.#vpHeightSnapshot = NaN;
            this.#sfHeightSnapshot = NaN;
            this.#isScrolling = false;
        }, this.#scrollDebounce);
        // Additional scroll calls always have to test for stick state change
        if (this.#isScrolling) {
            this.testForStick();
            return;
        }
        // Only reaches here on first call and we note as in a scrolling state
        this.#isScrolling = true;
        // grab the current rect points for stickers to test against while scrolling
        this.snapshotItemRects();
        /**
         * Get once and cache until scroll settles
         * Unlikely to change and avoids additional reflows on scroll
         */
        this.#vpHeightSnapshot = this.getViewportHeight();
        this.#sfHeightSnapshot = this.getScrollFrameHeight();
        this.testForStick();
    }
    onResize() {
        clearTimeout(this.#resizeTimeout);
        this.#resizeTimeout = window.setTimeout(() => {
            this.snapshotItemRects();
            this.#stickers.forEach(sticker => {
                if (sticker.isStuck)
                    this.addStick(sticker);
            });
        }, this.#resizeDebounce);
    }
    getScrollY() {
        return this.#isWindowScroll
            ? this.scrollContainer.scrollY
            : this.scrollContainer.scrollTop;
    }
    getViewportHeight() {
        // If we have a height snapshot use it since this has likely been called on scroll
        if (!Number.isNaN(this.#vpHeightSnapshot)) {
            return this.#vpHeightSnapshot;
        }
        return Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
    }
    getScrollFrameHeight() {
        // If we have a height snapshot use it since this has likely been called on scroll
        if (!Number.isNaN(this.#sfHeightSnapshot)) {
            return this.#sfHeightSnapshot;
        }
        const viewportHeight = this.getViewportHeight();
        let sfh = viewportHeight;
        if (!this.#isWindowScroll) {
            sfh = viewportHeight - this.scrollContainer.getBoundingClientRect().top;
        }
        return sfh;
    }
    testBottomInView(sticker) {
        const vph = this.getViewportHeight();
        const sfh = this.getScrollFrameHeight();
        const sic = this.#scrollIntervalChange;
        return sfh + sic + (vph - sfh) >= sticker.bottom + this.offset;
    }
    testForStick() {
        // Don't do anything if scrolling up
        if (this.#scrollDirection === -1)
            return;
        this.#stickers.forEach(sticker => {
            const bottomIsInView = this.testBottomInView(sticker);
            const hasHeightChange = !Number.isNaN(sticker.priorStickHeight) && sticker.height !== sticker.priorStickHeight;
            if (bottomIsInView) {
                if (!sticker.isStuck || hasHeightChange) {
                    window.requestAnimationFrame(() => this.addStick(sticker));
                }
            }
            else if (sticker.isStuck && hasHeightChange) {
                const marginTop = this.getScrollY() + sticker.top - this.offset;
                window.requestAnimationFrame(() => {
                    this.removeStick(sticker);
                    sticker.element.style.marginTop = `${marginTop}px`;
                });
            }
        });
        return this;
    }
    addStick(sticker) {
        sticker.element.style.position = 'sticky';
        sticker.element.style.marginTop = '';
        sticker.element.style.top = `${this.getStickTop(sticker)}px`;
        sticker.priorStickHeight = sticker.height;
        sticker.isStuck = true;
        return this;
    }
    removeStick(sticker) {
        sticker.element.style.position = '';
        sticker.element.style.marginTop = '';
        sticker.element.style.top = '';
        sticker.isStuck = false;
        return this;
    }
    getStickTop(sticker) {
        const spHeight = this.getScrollFrameHeight();
        let offset = this.offset;
        if (sticker.height + this.offset > spHeight) {
            offset = spHeight - sticker.height - offset;
        }
        return offset;
    }
    cleanup() {
        this.rmHandlers();
        this.#stickers.forEach(this.removeStick);
    }
}
/**
 * @example useScrollStick({ elements: [myColumnElements] | '.my-columns', offset: 20 });
 * @param options An object to set the sticky elements, the scroll container and an stuck offset in pixels.
 * @returns void.
 */
const useScrollStick = (options) => {
    const instance = useRef();
    useLayoutEffect(() => {
        if (options?.elements) {
            instance.current = new Stickers(options);
        }
        return () => {
            instance.current?.cleanup();
            instance.current = undefined;
        };
    }, [options?.scrollContainer, options?.offset, ...(options?.elements ?? [])]);
};
export default useScrollStick;
//# sourceMappingURL=useScrollStick.js.map