lisn.js
Version:
Simply handle user gestures and actions. Includes widgets.
373 lines • 15.8 kB
TypeScript
/**
* @module Watchers/ScrollWatcher
*/
import { XYDirection, ScrollDirection } from "../globals/types.cjs";
import { ScrollTarget, TargetCoordinates, CommaSeparatedStr } from "../globals/types.cjs";
import { ScrollAction, ScrollToOptions } from "../utils/scroll.cjs";
import { CallbackHandler, Callback } from "../modules/callback.cjs";
export type { ScrollAction, ScrollToOptions } from "../utils/scroll.cjs";
/**
* {@link ScrollWatcher} listens for scroll events in any direction.
*
* It manages registered callbacks globally and reuses event listeners for more
* efficient performance.
*/
export declare class ScrollWatcher {
/**
* Call the given handler whenever the given scrollable is scrolled.
*
* Unless {@link OnScrollOptions.skipInitial} is true, the handler is also
* called (almost) immediately with the latest scroll data. If a scroll has
* not yet been observed on the scrollable and its `scrollTop` and
* `scrollLeft` are 0, then the direction is {@link Types.NoDirection} and
* the handler is only called if {@link Types.NoDirection} is part of the
* supplied {@link OnScrollOptions.directions | options.directions} (or
* {@link OnScrollOptions.directions | options.directions} is not given).
*
* **IMPORTANT:** The same handler can _not_ be added multiple times for the
* same scrollable, even if the options differ. If the handler has already
* been added for this scrollable, either using {@link trackScroll} or using
* {@link onScroll}, then it will be removed and re-added with the current
* options. So if previously it was also tracking content size changes using
* {@link trackScroll}, it will no longer do so.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the options are invalid.
*/
readonly onScroll: (handler: OnScrollHandler, options?: OnScrollOptions) => Promise<void>;
/**
* Removes a previously added handler.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the scrollable is invalid.
*/
readonly offScroll: (handler: OnScrollHandler, scrollable?: ScrollTarget) => void;
/**
* This everything that {@link onScroll} does plus more:
*
* In addition to a scroll event, the handler is also called when either the
* offset size or scroll (content) size of the scrollable changes as that
* would affect its `scrollTopFraction` and `scrollLeftFraction` and possibly
* the `scrollTop` and `scrollLeft` as well.
*
* **IMPORTANT:** The same handler can _not_ be added multiple times for the
* same scrollable, even if the options differ. If the handler has already
* been added for this scrollable, either using {@link trackScroll} or using
* {@link onScroll}, then it will be removed and re-added with the current
* options.
*
* ------
*
* If `handler` is not given, then it defaults to an internal handler that
* updates a set of CSS variables on the scrollable element's style:
*
* - If {@link OnScrollOptions.scrollable | options.scrollable} is not given,
* or is `null`, `window` or `document`, the following CSS variables are
* set on the root (`html`) element and represent the scroll of the
* {@link Settings.settings.mainScrollableElementSelector | the main scrolling element}:
* - `--lisn-js--page-scroll-top`
* - `--lisn-js--page-scroll-top-fraction`
* - `--lisn-js--page-scroll-left`
* - `--lisn-js--page-scroll-left-fraction`
* - `--lisn-js--page-scroll-width`
* - `--lisn-js--page-scroll-height`
*
* - Otherwise, the following variables are set on the scrollable itself,
* and represent its scroll offset:
* - `--lisn-js--scroll-top`
* - `--lisn-js--scroll-top-fraction`
* - `--lisn-js--scroll-left`
* - `--lisn-js--scroll-left-fraction`
* - `--lisn-js--scroll-width`
* - `--lisn-js--scroll-height`
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the options are invalid.
*/
readonly trackScroll: (handler?: OnScrollHandler | null, options?: OnScrollOptions) => Promise<void>;
/**
* Removes a previously added handler for {@link trackScroll}.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the scrollable is invalid.
*/
readonly noTrackScroll: (handler?: OnScrollHandler | null, scrollable?: ScrollTarget) => void;
/**
* Get the scroll offset of the given scrollable. By default, it will
* {@link waitForMeasureTime} and so will be delayed by one frame.
*
* @param realtime If true, it will not {@link waitForMeasureTime}. Use
* this only when doing realtime scroll-based animations
* as it may cause a forced layout.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the scrollable is invalid.
*/
readonly fetchCurrentScroll: (scrollable?: ScrollTarget, realtime?: boolean) => Promise<ScrollData>;
/**
* Scrolls the given scrollable element to in the given direction.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the direction or options are invalid.
*/
readonly scroll: (direction: XYDirection, options?: ScrollOptions) => Promise<ScrollAction | null>;
/**
* Scrolls the given scrollable element to the given `to` scrollable.
*
* Returns `null` if there's an ongoing scroll that is not cancellable.
*
* Note that if `to` is an element or a selector, then it _must_ be a
* descendant of the scrollable element.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the "to" coordinates or options are invalid.
*
* @param to If this is an element, then its top-left position is used as
* the target coordinates. If it is a string, then it is treated
* as a selector for an element using `querySelector`.
* @param [options.scrollable]
* If not given, it defaults to
* {@link Settings.settings.mainScrollableElementSelector | the main scrolling element}.
*
* @returns `null` if there's an ongoing scroll that is not cancellable,
* otherwise a {@link ScrollAction}.
*/
readonly scrollTo: (to: TargetCoordinates | Element | string, options?: ScrollToOptions) => Promise<ScrollAction | null>;
/**
* Returns the current {@link ScrollAction} if any.
*
* @param scrollable If not given, it defaults to
* {@link Settings.settings.mainScrollableElementSelector | the main scrolling element}
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the scrollable is invalid.
*/
readonly fetchCurrentScrollAction: (scrollable?: Element) => Promise<ScrollAction | null>;
/**
* Cancels the ongoing scroll that's resulting from smooth scrolling
* triggered in the past. Does not interrupt or prevent further scrolling.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the scrollable is invalid.
*
* @param [options.immediate] If true, then it will not use
* {@link waitForMeasureTime} or
* {@link Utils.waitForMutateTime | waitForMutateTime}.
* Warning: this will likely result in forced layout.
*/
readonly stopUserScrolling: (options?: {
scrollable?: ScrollTarget;
immediate?: boolean;
}) => Promise<void>;
/**
* Returns the element that holds the main page content. By default it's
* `document.body` but is overridden by
* {@link Settings.settings.mainScrollableElementSelector}.
*
* It will wait for the element to be available if not already.
*/
static fetchMainContentElement(): Promise<HTMLElement>;
/**
* Returns the scrollable element that holds the wrapper around the main page
* content. By default it's `document.scrollable` (unless `document.body` is
* actually scrollable, in which case it will be used) but it will be
* different if {@link Settings.settings.mainScrollableElementSelector} is set.
*
* It will wait for the element to be available if not already.
*/
static fetchMainScrollableElement(): Promise<HTMLElement>;
/**
* Creates a new instance of ScrollWatcher with the given
* {@link ScrollWatcherConfig}. It does not save it for future reuse.
*/
static create(config?: ScrollWatcherConfig): ScrollWatcher;
/**
* Returns an existing instance of ScrollWatcher with the given
* {@link ScrollWatcherConfig}, or creates a new one.
*
* **NOTE:** It saves it for future reuse, so don't use this for temporary
* short-lived watchers.
*/
static reuse(config?: ScrollWatcherConfig): ScrollWatcher;
private constructor();
}
/**
* @interface
*/
export type ScrollWatcherConfig = {
/**
* The default value for
* {@link OnScrollOptions.debounceWindow | debounceWindow} in calls to
* {@link ScrollWatcher.onScroll}.
*
* @defaultValue 75
*/
debounceWindow?: number;
/**
* The default value for
* {@link OnScrollOptions.threshold | threshold} in calls to
* {@link ScrollWatcher.onScroll}.
*
* @defaultValue 50
*/
scrollThreshold?: number;
/**
* The default value for
* {@link ScrollOptions.duration | duration} in calls to
* {@link ScrollWatcher.scroll} and {@link ScrollWatcher.scrollTo}.
*
* @defaultValue 1000
*/
scrollDuration?: number;
};
/**
* @interface
*/
export type OnScrollOptions = {
/**
* If it is not given, or is `null`, `window` or `document`, then it will
* track the scroll of the
* {@link Settings.settings.mainScrollableElementSelector | the main scrolling element}.
*
* Other values must be an `Element` and are taken literally.
*
* @defaultValue undefined
*/
scrollable?: ScrollTarget;
/**
* If non-0, the scroll handler will only be called when the scrollable's
* scroll offset in the observed direction has changed at least
* `scrollThreshold` pixels since the last time the handler was called.
*
* @defaultValue {@link ScrollWatcherConfig.scrollThreshold}
*/
threshold?: number;
/**
* Specifies a list of {@link ScrollDirection}s to listen for.
*
* It can be a comma-separated list of direction names or an array of such
* names.
*
* If not given, then it listens for scrolls in any direction, including
* {@link Types.NoDirection} and {@link Types.AmbiguousDirection}.
*
* The {@link Types.NoDirection} occurs when the callback is called initially
* (if `skipInitial` is not `true`) and there hasn't yet been a scroll
* observed on the scrollable _and_ it's `scrollTop`/`scrollLeft` are 0.
*
* The {@link Types.AmbiguousDirection} occurs when the user scrolls
* diagonally with close to equal deltas in both horizontal and vertical
* direction.
*
* **IMPORTANT:**
*
* The direction of a scroll event is always based on comparing `deltaX` and
* `deltaY` relative to the _last scroll event_ (within the `debounceWindow`,
* and not to the scroll data for the last time the callback was called (in
* case it was skipped because `threshold` was not exceeded or in case it was
* debounced by a larger window than the watcher).
*
* I.e. if you have both `threshold` and `directions` restricted, or if the
* callback has a larger debounce window than the watcher, it is possible for
* there to be a change in the relevant `scrollTop`/`scrollLeft` offset that
* exceeds the threshold, and for the callback to _not_ be called.
*
* For a callback to be called, both of these must be true:
* - the change in `scrollTop`/`scrollLeft`, _compared to the last time the
* callback was called_ must exceed the {@link threshold}
* - the effective scroll direction, _compared to the last scroll event_
* prior to the _watcher's_ debounce window must be one of the given
* {@link directions}.
*
* @defaultValue undefined
*/
directions?: CommaSeparatedStr<ScrollDirection> | ScrollDirection[];
/**
* Do not call the handler until there's a future scroll of the element.
*
* By default we call the handler (almost) immediately if there's been a
* scroll in one of the given directions, or if there has not been a scroll
* but directions includes {@link Types.NoDirection}, but you can disable
* this initial call here.
*
* @defaultValue false
*/
skipInitial?: boolean;
/**
* If non-0, the handler will be "debounced" so it's called at most
* `debounceWindow` milliseconds.
*
* **IMPORTANT:**
* If the debounce window is non-0 (default), then the callback is always
* delayed by at least an animation frame following a scroll event to allow
* for optimized `scrollTop`/`scrollLeft` measurements via
* {@link waitForMeasureTime}.
*
* If you set this is 0, this indicates that the callback should be
* "realtime" and will therefore skip {@link waitForMeasureTime}, which could
* lead to forced re-layouts, but you probably need this when doing
* scroll-based animations.
*
* @defaultValue {@link ScrollWatcherConfig.debounceWindow}
*/
debounceWindow?: number;
};
/**
* @interface
*/
export type ScrollOptions = ScrollToOptions & {
/**
* How much to scroll in the given direction.
*
* @defaultValue 100
*/
amount?: number;
/**
* If set to "pixel" (default), `amount` is taken as absolute pixels.
*
* If set to "visible", `amount` is taken as percent of the element's visible
* size in the scrolling direction (100 means full client width for
* horizontal or height for vertical scroll).
*
* If set to "content", `amount` is taken as percent of the element's full
* content size in the scrolling direction (100 means full scroll width for
* horizontal or height for vertical scroll).
*
* @defaultValue "pixel"
*/
asFractionOf?: "pixel" | "visible" | "content";
};
/**
* The handler is invoked with two arguments:
*
* - the element that has been resized
* - the {@link ScrollData} for the element
*/
export type OnScrollHandlerArgs = [Element, ScrollData];
export type OnScrollCallback = Callback<OnScrollHandlerArgs>;
export type OnScrollHandler = CallbackHandler<OnScrollHandlerArgs> | OnScrollCallback;
export type ScrollData = {
clientWidth: number;
clientHeight: number;
scrollWidth: number;
scrollHeight: number;
scrollTop: number;
/**
* This is the `scrollTop` relative to the full `scrollHeight` minus the
* `clientHeight`, ranging from 0 to 1.
*/
scrollTopFraction: number;
scrollLeft: number;
/**
* This is the `scrollLeft` relative to the full `scrollWidth` minus the
* `clientWidth`, ranging from 0 to 1.
*/
scrollLeftFraction: number;
/**
* This is the direction of the last scroll action, i.e. _compared to the
* last scroll event_, not necessarily based on the deltas compared to the
* last time this callback was called.
*/
direction: ScrollDirection;
};
//# sourceMappingURL=scroll-watcher.d.ts.map