UNPKG

@pronotron/io

Version:

Reliable viewport tracking without missed targets, unlike the default IntersectionObserver API.

218 lines (207 loc) 7.43 kB
import { RequireAtLeastOne } from '@pronotron/utils'; /** * Jumpy scroll might cause an element "fast forward", * eg: first "bottom-enter" following with "top-exit" in the same loop without being visible. */ type FastForwardOptions = "skip_both" | "execute_last" | "execute_both"; /** * Event dispatch options */ type IODispatchFunction<TEvents extends string> = Record<TEvents, ( () => void ) | ({ dispatch: () => void; limit: number; })>; type IOEventsDispatch<TEvents extends string> = IODispatchFunction<TEvents> & { onInViewport: ( normalizedPosition: number ) => void }; type IODispatchOptions<TEvents extends string> = RequireAtLeastOne<IOEventsDispatch<TEvents>> & { /** * Jumpy scroll might cause an element "fast forward", * eg: first "bottom-enter" following with "top-exit" in the same loop without being visible. */ onFastForward?: FastForwardOptions; }; /** * Object that needs to pass application as member */ type IONodeOptions<TEvents extends string> = { /** * Node creation reference, to be used to avoid duplicates and respond remove requests. */ ref: PronotronIONodeRef; dispatch: IODispatchOptions<TEvents>; /** * How to get IONode's absolute position (not relative to scroll value). * Will be executed if the layout has been changed. */ getBounds: () => { start: number; end: number; }; /** * Will be added/deleted symetricly to both side */ offset?: number; onRemoveNode?: () => void; }; /** * To be able to avoid duplicate nodes and respond to remove request, * we need a value from client to use as KEY. */ type PronotronIONodeRef = HTMLElement; /** * Points the internal ID of PronotronIONode object. */ type PronotronIONodeID = number; declare abstract class PronotronIOBase<TEvents extends string> { /** * Current scroll direction (based on the last scroll value). */ abstract direction: string; /** * @param nodeCountHint To populate fixed typed array length, will be expanded if needed * @param useRounded Whether integers are used instead of floating-point numbers. Default is true. */ constructor(nodeCountHint?: number, useRounded?: boolean); /** * Creates an IONode. * * @param newNodeOptions IONode creation options * @returns false if error, IONode internal id if success */ addNode(newNodeOptions: IONodeOptions<TEvents>): PronotronIONodeID | false; /** * Removes an IONode by its ref {@link PronotronIONodeRef} * * @param existingNodeRef Reference passed while executing addNode() */ removeNode(existingNodeRef: PronotronIONodeRef): void; /** * Modifies the last scroll value * * @param scrollValue Scroll value */ setLastScroll(scrollValue: number): void; /** * Bulk updates all IONode positions. * Should be executed when the layout changes, e.g.: * - Screen resize * - Resizing in-page elements (accordion, etc.) * * @param maximumValue - Maximum possible position (e.g., `document.documentElement.scrollHeight`). */ updatePositions(maximumValue: number): void; /** * Updates viewport layout data used in calculations. * Should be called on: * - Mobile viewport changes (status bar collapse/expand) * - Pinch-zoom changes * - Screen or in-page resizes * * @param start - Start position of the viewport. * @param end - End position of the viewport. */ updateViewportLayout(start: number, end: number): void; } declare abstract class PronotronIOEventDispatcher<TEvents extends string> extends PronotronIOBase<TEvents> { /** * Handles scroll events and updates the current scroll direction, * then recalculates intersections. * * @param scrollValue Current scroll value */ handleScroll(scrollValue: number): void; } type VerticalEvent = "onTopEnter" | "onTopExit" | "onBottomEnter" | "onBottomExit"; type IOVerticalOptions = IONodeOptions<VerticalEvent>; /** * PronotronIO - A custom intersection observer solution * * @example * const pronotronIO = new PronotronIOVerticalObserver(); * pronotronIO.setLastScroll( 0 ); * pronotronIO.addNode({ * ref: HTMLElement, * dispatch: { * onInViewport: ( normalizedPosition: number ) => console.log( "Element is in viewport", normalizedPosition ), * onTopEnter: () => console.log( "Element entered from top" ), * onTopExit: { * dispatch: () => console.log( "Element exited from top" ), * limit: 2 * }, * onBottomEnter..., * onBottomExit..., * onFastForward: "execute_both", * }, * onRemoveNode: () => element.dataset.ioActive = "0", * getBounds: () => { * const { top, bottom } = element.getBoundingClientRect(); * const start = top + window.scrollY; * const end = bottom + window.scrollY; * return { start, end }; * } * offset: 100, // In pixels, applied to both directions * }); * // Recommended: wrap in a throttled handler for performance * window.addEventListener( 'scroll', () => pronotronIO.handleScroll( window.scrollY ) ); */ declare class PronotronIOVerticalObserver extends PronotronIOEventDispatcher<VerticalEvent> { direction: "up" | "down"; _scrollDirectionNames: { _negative: string; _positive: string; }; _eventNames: { readonly _negativeEnterEvent: "onTopEnter"; readonly _negativeExitEvent: "onTopExit"; readonly _positiveEnterEvent: "onBottomEnter"; readonly _positiveExitEvent: "onBottomExit"; }; } type HorizontalEvent = "onLeftEnter" | "onLeftExit" | "onRightEnter" | "onRightExit"; type IOHorizontalOptions = IONodeOptions<HorizontalEvent>; /** * PronotronIO - A custom intersection observer solution * * @example * const pronotronIO = new PronotronIOHorizontalObserver(); * pronotronIO.setLastScroll( 0 ); * pronotronIO.addNode({ * ref: HTMLElement, * dispatch: { * onInViewport: ( normalizedPosition: number ) => console.log( "Element is in viewport", normalizedPosition ), * onLeftEnter: () => console.log( "Element entered from left" ), * onLeftExit: { * dispatch: () => console.log( "Element exited from left" ), * limit: 2 * }, * onRightEnter..., * onRightExit..., * onFastForward: "execute_both", * }, * onRemoveNode: () => element.dataset.ioActive = "0", * getBounds: () => { * const { left, right } = element.getBoundingClientRect(); * const start = left + window.scrollX; * const end = right + window.scrollX; * return { start, end }; * } * offset: 100, // In pixels, applied to both directions * }); * // Recommended: wrap in a throttled handler for performance * window.addEventListener( 'scroll', () => pronotronIO.handleScroll( window.scrollX ) ); */ declare class PronotronIOHorizontalObserver extends PronotronIOEventDispatcher<HorizontalEvent> { direction: "left" | "right"; _scrollDirectionNames: { _negative: string; _positive: string; }; _eventNames: { readonly _negativeEnterEvent: "onLeftEnter"; readonly _negativeExitEvent: "onLeftExit"; readonly _positiveEnterEvent: "onRightEnter"; readonly _positiveExitEvent: "onRightExit"; }; } export { type IOHorizontalOptions, type IOVerticalOptions, PronotronIOHorizontalObserver, PronotronIOVerticalObserver };