@pronotron/io
Version:
Reliable viewport tracking without missed targets, unlike the default IntersectionObserver API.
218 lines (207 loc) • 7.43 kB
text/typescript
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 };