All files scroll.ts

46.66% Statements 28/60
37.5% Branches 3/8
60% Functions 3/5
46.66% Lines 28/60

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107                                                    2x 26x 26x 26x                                   2x 26x                         26x 26x 26x           26x     2x   2x     2x 26x 26x 26x 26x 26x   26x 26x   26x 26x 26x 26x 26x 26x 26x     2x                    
/** Internal {@link ScrollToOptions | `ScrollToOptions`}: `left` and `top` properties always have values */
interface _ScrollPosition extends ScrollToOptions {
    left: number;
    top: number;
}
 
export interface ScrollPositionElement extends ScrollToOptions {
    /**
     * A valid CSS selector. Some special characters need to be escaped (https://mathiasbynens.be/notes/css-escapes).
     * @example
     * Here are some examples:
     *
     * - `.title`
     * - `.content:first-child`
     * - `#marker`
     * - `#marker\~with\~symbols`
     * - `#marker.with.dot`: Selects `class="with dot" id="marker"`, not `id="marker.with.dot"`
     *
     */
    el: string | Element;
}
 
/** Scroll parameters */
export type ScrollPosition = ScrollToOptions | ScrollPositionElement;
 
/** Get current window scroll position */
export const winScrollPos = (): _ScrollPosition => ({
    left: window.scrollX,
    top: window.scrollY
});
 
/** Get element position for scrolling in document */
function getElementPosition(
    el: Element,
    offset: ScrollToOptions
): _ScrollPosition {
    const docRect = document.documentElement.getBoundingClientRect();
    const elRect = el.getBoundingClientRect();
 
    return {
        behavior: offset.behavior,
        left: elRect.left - docRect.left - (offset.left || 0),
        top: elRect.top - docRect.top - (offset.top || 0)
    };
}
 
/** Scroll to specified position */
export function scrollToPosition(position: ScrollPosition): void {
    if ('el' in position) {
        const positionEl = position.el;
 
        const el =
            typeof positionEl === 'string'
                ? document.querySelector(positionEl)
                : positionEl;
 
        if (!el) return;
 
        position = getElementPosition(el, position);
    }
 
    if ('scrollBehavior' in document.documentElement.style) {
        window.scrollTo(position);
    } else {
        window.scrollTo(
            Number.isFinite(position.left) ? position.left! : window.scrollX,
            Number.isFinite(position.top) ? position.top! : window.scrollY
        );
    }
}
 
/** Stored scroll positions */
export const scrollPositions = new Map<string, _ScrollPosition>();
 
const POSITION_KEY = '__scroll_position_key';
 
/** Save scroll position */
export function saveScrollPosition(
    key: string,
    scrollPosition = winScrollPos()
) {
    scrollPosition = { ...scrollPosition };
    scrollPositions.set(key, scrollPosition);
 
    try {
        if (location.href !== key) return;
        // preserve the existing history state as it could be overridden by the user
        const stateCopy = {
            ...(history.state || {}),
            [POSITION_KEY]: scrollPosition
        };
        history.replaceState(stateCopy, '');
    } catch (error) {}
}
 
/** Get saved scroll position */
export function getSavedScrollPosition(
    key: string,
    defaultValue: _ScrollPosition | null = null
): _ScrollPosition | null {
    const scroll = scrollPositions.get(key) || history.state[POSITION_KEY];
 
    // Saved scroll position should not be used multiple times, next time should use newly saved position
    scrollPositions.delete(key);
    return scroll || defaultValue;
}