lisn.js
Version:
Simply handle user gestures and actions. Includes widgets.
743 lines (683 loc) • 32.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ScrollWatcher = void 0;
var MC = _interopRequireWildcard(require("../globals/minification-constants.cjs"));
var MH = _interopRequireWildcard(require("../globals/minification-helpers.cjs"));
var _cssAlter = require("../utils/css-alter.cjs");
var _directions = require("../utils/directions.cjs");
var _domAlter = require("../utils/dom-alter.cjs");
var _domOptimize = require("../utils/dom-optimize.cjs");
var _event = require("../utils/event.cjs");
var _log = require("../utils/log.cjs");
var _math = require("../utils/math.cjs");
var _scroll = require("../utils/scroll.cjs");
var _text = require("../utils/text.cjs");
var _validation = require("../utils/validation.cjs");
var _callback = require("../modules/callback.cjs");
var _xMap = require("../modules/x-map.cjs");
var _domWatcher = require("./dom-watcher.cjs");
var _sizeWatcher = require("./size-watcher.cjs");
var _debug = _interopRequireDefault(require("../debug/debug.cjs"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /**
* @module Watchers/ScrollWatcher
*/
// re-export for convenience
/**
* {@link ScrollWatcher} listens for scroll events in any direction.
*
* It manages registered callbacks globally and reuses event listeners for more
* efficient performance.
*/
class ScrollWatcher {
/**
* 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() {
return (0, _scroll.fetchMainContentElement)();
}
/**
* 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() {
return (0, _scroll.fetchMainScrollableElement)();
}
/**
* Creates a new instance of ScrollWatcher with the given
* {@link ScrollWatcherConfig}. It does not save it for future reuse.
*/
static create(config) {
return new ScrollWatcher(getConfig(config), CONSTRUCTOR_KEY);
}
/**
* 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) {
const myConfig = getConfig(config);
const configStrKey = (0, _text.objToStrKey)(myConfig);
let instance = instances.get(configStrKey);
if (!instance) {
instance = new ScrollWatcher(myConfig, CONSTRUCTOR_KEY);
instances.set(configStrKey, instance);
}
return instance;
}
constructor(config, key) {
/**
* 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.
*/
_defineProperty(this, "onScroll", void 0);
/**
* Removes a previously added handler.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the scrollable is invalid.
*/
_defineProperty(this, "offScroll", void 0);
/**
* 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.
*/
_defineProperty(this, "trackScroll", void 0);
/**
* Removes a previously added handler for {@link trackScroll}.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the scrollable is invalid.
*/
_defineProperty(this, "noTrackScroll", void 0);
/**
* 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.
*/
_defineProperty(this, "fetchCurrentScroll", void 0);
/**
* Scrolls the given scrollable element to in the given direction.
*
* @throws {@link Errors.LisnUsageError | LisnUsageError}
* If the direction or options are invalid.
*/
_defineProperty(this, "scroll", void 0);
/**
* 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}.
*/
_defineProperty(this, "scrollTo", void 0);
/**
* 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.
*/
_defineProperty(this, "fetchCurrentScrollAction", void 0);
/**
* 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.
*/
_defineProperty(this, "stopUserScrolling", void 0);
if (key !== CONSTRUCTOR_KEY) {
throw MH.illegalConstructorError("ScrollWatcher.create");
}
const logger = _debug.default ? new _debug.default.Logger({
name: "ScrollWatcher",
logAtCreation: config
}) : null;
const allScrollData = MH.newWeakMap();
const activeListeners = MH.newWeakMap();
const allCallbacks = (0, _xMap.newXWeakMap)(() => MH.newMap());
// ----------
const fetchCurrentScroll = async (element, realtime = false, isScrollEvent = false) => {
// The scroll data can change event without a scroll event, e.g. by the
// element changing size, so always get the latest here.
const previousEventData = allScrollData.get(element);
const latestData = await fetchScrollData(element, previousEventData, realtime);
// If there hasn't been a scroll event, use the old scroll direction
if (!isScrollEvent && previousEventData) {
latestData.direction = previousEventData.direction;
}
return latestData;
};
// ----------
const createCallback = (handler, options, trackType) => {
var _allCallbacks$get;
const element = options._element;
MH.remove((_allCallbacks$get = allCallbacks.get(element)) === null || _allCallbacks$get === void 0 || (_allCallbacks$get = _allCallbacks$get.get(handler)) === null || _allCallbacks$get === void 0 ? void 0 : _allCallbacks$get._callback);
debug: logger === null || logger === void 0 || logger.debug5("Adding/updating handler", options);
const callback = (0, _callback.wrapCallback)(handler, options._debounceWindow);
callback.onRemove(() => {
deleteHandler(handler, options);
});
const entry = {
_callback: callback,
_trackType: trackType,
_options: options
};
allCallbacks.sGet(element).set(handler, entry);
return entry;
};
// ----------
const setupOnScroll = async (handler, userOptions, trackType) => {
const options = await fetchOnScrollOptions(config, userOptions !== null && userOptions !== void 0 ? userOptions : {});
const element = options._element;
// Don't await for the scroll data before creating the callback so that
// setupOnScroll and removeOnScroll have the same "timing" and therefore
// calling onScroll and offScroll immediately without awaiting removes the
// callback.
const entry = createCallback(handler, options, trackType);
const callback = entry._callback;
const eventTarget = options._eventTarget;
const scrollData = await fetchCurrentScroll(element, options._debounceWindow === 0);
if (callback.isRemoved()) {
return;
}
entry._data = scrollData;
allScrollData.set(element, scrollData);
if (trackType === TRACK_FULL) {
await setupSizeTrack(entry);
}
let listenerOptions = activeListeners.get(eventTarget);
if (!listenerOptions) {
debug: logger === null || logger === void 0 || logger.debug4("Adding scroll listener", eventTarget);
listenerOptions = {
_nRealtime: 0
};
activeListeners.set(eventTarget, listenerOptions);
// Don't debounce the scroll handler, only the callbacks.
(0, _event.addEventListenerTo)(eventTarget, MC.S_SCROLL, scrollHandler);
}
if (options._debounceWindow === 0) {
listenerOptions._nRealtime++;
}
const directions = options._directions;
if (!callback.isRemoved() && !(userOptions !== null && userOptions !== void 0 && userOptions.skipInitial) && directionMatches(directions, scrollData.direction)) {
debug: logger === null || logger === void 0 || logger.debug5("Calling initially with", element, scrollData);
// Use a one-off callback that's not debounced for the initial call.
await invokeCallback((0, _callback.wrapCallback)(handler), element, scrollData);
}
};
// ----------
const removeOnScroll = async (handler, scrollable, trackType) => {
var _allCallbacks$get2;
const options = await fetchOnScrollOptions(config, {
scrollable
});
const element = options._element;
const currEntry = (_allCallbacks$get2 = allCallbacks.get(element)) === null || _allCallbacks$get2 === void 0 ? void 0 : _allCallbacks$get2.get(handler);
if ((currEntry === null || currEntry === void 0 ? void 0 : currEntry._trackType) === trackType) {
MH.remove(currEntry._callback);
if (handler === setScrollCssProps) {
// delete the properties
setScrollCssProps(element, null);
}
}
};
// ----------
const deleteHandler = (handler, options) => {
const element = options._element;
const eventTarget = options._eventTarget;
MH.deleteKey(allCallbacks.get(element), handler);
allCallbacks.prune(element);
const listenerOptions = activeListeners.get(eventTarget);
if (listenerOptions && options._debounceWindow === 0) {
listenerOptions._nRealtime--;
}
if (!allCallbacks.has(element)) {
debug: logger === null || logger === void 0 || logger.debug4("No more callbacks for scrollable; removing listener", element);
MH.deleteKey(allScrollData, element);
(0, _event.removeEventListenerFrom)(eventTarget, MC.S_SCROLL, scrollHandler);
MH.deleteKey(activeListeners, eventTarget);
// TODO: Should we unwrap children if previously WE wrapped them?
}
};
// ----------
const setupSizeTrack = async entry => {
const options = entry._options;
const element = options._element;
const scrollCallback = entry._callback;
debug: logger === null || logger === void 0 || logger.debug8("Setting up size tracking", element);
const doc = MH.getDoc();
const docScrollingElement = MH.getDocScrollingElement();
const resizeCallback = (0, _callback.wrapCallback)(async () => {
// Get the latest scroll data for the scrollable
// Currently, the resize callback is already delayed by a frame due to
// the SizeWatcher, so we don't need to treat this as realtime.
const latestData = await fetchCurrentScroll(element);
const thresholdsExceeded = hasExceededThreshold(options, latestData, entry._data);
if (!thresholdsExceeded) {
debug: logger === null || logger === void 0 || logger.debug9("Threshold not exceeded", options, latestData, entry._data);
} else if (!scrollCallback.isRemoved()) {
await invokeCallback(scrollCallback, element, latestData);
}
});
scrollCallback.onRemove(resizeCallback.remove);
// Don't use default instance as it has a high threshold.
const sizeWatcher = _sizeWatcher.SizeWatcher.reuse();
const setupOnResize = target => sizeWatcher.onResize(resizeCallback, {
target,
[MC.S_DEBOUNCE_WINDOW]: options._debounceWindow,
// TODO maybe accepts resizeThreshold option
threshold: options._threshold
});
if (element === docScrollingElement) {
// In case we're tracking the main document scroll, then we only need to
// observe the viewport size and the size of the documentElement (which is
// the content size).
setupOnResize(); // viewport size
setupOnResize(doc); // content size
return;
}
// ResizeObserver only detects changes in offset width/height which is
// the visible size of the scrolling element, and that is not affected by the
// size of its content.
// But we also need to detect changes in the scroll width/height which is
// the size of the content.
// We also need to keep track of elements being added to the scrollable element.
const observedElements = MH.newSet([element]);
// Observe the scrolling element
setupOnResize(element);
// And also its children (if possible, a single wrapper around them
const wrapper = await (0, _domAlter.tryWrapContent)(element, {
_classNames: [MC.PREFIX_WRAPPER, PREFIX_WRAPPER]
});
if (wrapper) {
setupOnResize(wrapper);
observedElements.add(wrapper);
//
} else {
for (const child of MH.childrenOf(element)) {
setupOnResize(child);
observedElements.add(child);
}
}
// Watch for newly added elements
const domWatcher = _domWatcher.DOMWatcher.create({
root: element,
// only direct children
subtree: false
});
const onAddedCallback = (0, _callback.wrapCallback)(operation => {
const child = MH.currentTargetOf(operation);
// If we've just added the wrapper, it will be in DOMWatcher's queue,
// so check.
if (child !== wrapper) {
if (wrapper) {
// Move this child into the wrapper. If this results in change of size
// for wrapper, SizeWatcher will call us.
(0, _domAlter.moveElement)(child, {
to: wrapper,
ignoreMove: true
});
} else {
// Track the size of this child.
// Don't skip initial, call the callback now
setupOnResize(child);
observedElements.add(child);
}
}
});
domWatcher.onMutation(onAddedCallback, {
categories: [MC.S_ADDED]
});
resizeCallback.onRemove(onAddedCallback.remove);
};
// ----------
const scrollHandler = async event => {
var _activeListeners$get$, _activeListeners$get;
// We cannot use event.currentTarget because scrollHandler is called inside
// a setTimeout so by that time, currentTarget is null or something else.
//
// However, target and currentTarget only differ when the event is in the
// bubbling or capturing phase. Because
//
// - the scroll event only bubbles when fired on document, and (it only
// bubbles up to window)
// - and we never attach the listener to the capturing phase
// - and we always use document instead of window to listen for scroll on
// document
//
// then event.target suffices.
const scrollable = MH.targetOf(event);
/* istanbul ignore next */
if (!scrollable || !(MH.isElement(scrollable) || MH.isDoc(scrollable))) {
return;
}
const element = await (0, _scroll.fetchScrollableElement)(scrollable);
const realtime = ((_activeListeners$get$ = (_activeListeners$get = activeListeners.get(scrollable)) === null || _activeListeners$get === void 0 ? void 0 : _activeListeners$get._nRealtime) !== null && _activeListeners$get$ !== void 0 ? _activeListeners$get$ : 0) > 0;
const latestData = await fetchCurrentScroll(element, realtime, true);
allScrollData.set(element, latestData);
debug: logger === null || logger === void 0 || logger.debug9("Scroll event", element, latestData);
for (const entry of ((_allCallbacks$get3 = allCallbacks.get(element)) === null || _allCallbacks$get3 === void 0 ? void 0 : _allCallbacks$get3.values()) || []) {
var _allCallbacks$get3;
// Consider the direction since the last scroll event and not the
// direction based on the largest delta the last time the callback
// was called.
const options = entry._options;
const thresholdsExceeded = hasExceededThreshold(options, latestData, entry._data);
if (!thresholdsExceeded) {
debug: logger === null || logger === void 0 || logger.debug9("Threshold not exceeded", options, latestData, entry._data);
continue;
}
// If threshold has been exceeded, always update the latest data for
// this callback.
entry._data = latestData;
if (!directionMatches(options._directions, latestData.direction)) {
debug: logger === null || logger === void 0 || logger.debug9("Direction does not match", options, latestData);
continue;
}
invokeCallback(entry._callback, element, latestData);
}
};
// ----------
this.fetchCurrentScroll = (scrollable, realtime) => (0, _scroll.fetchScrollableElement)(scrollable).then(element => fetchCurrentScroll(element, realtime));
// ----------
this.scroll = (direction, options) => {
var _options$amount;
if (!(0, _scroll.isValidScrollDirection)(direction)) {
throw MH.usageError(`Unknown scroll direction: '${direction}'`);
}
const isVertical = direction === MC.S_UP || direction === MC.S_DOWN;
const sign = direction === MC.S_UP || direction === MC.S_LEFT ? -1 : 1;
let targetCoordinate;
const amount = (_options$amount = options === null || options === void 0 ? void 0 : options.amount) !== null && _options$amount !== void 0 ? _options$amount : 100;
const asFractionOf = options === null || options === void 0 ? void 0 : options.asFractionOf;
if (asFractionOf === "visible") {
targetCoordinate = isVertical ? el => el[MC.S_SCROLL_TOP] + sign * amount * (0, _scroll.getClientHeightNow)(el) / 100 : el => el[MC.S_SCROLL_LEFT] + sign * amount * (0, _scroll.getClientWidthNow)(el) / 100;
//
} else if (asFractionOf === "content") {
targetCoordinate = isVertical ? el => el[MC.S_SCROLL_TOP] + sign * amount * el[MC.S_SCROLL_HEIGHT] / 100 : el => el[MC.S_SCROLL_LEFT] + sign * amount * el[MC.S_SCROLL_WIDTH] / 100;
//
} else if (asFractionOf !== undefined && asFractionOf !== "pixel") {
throw MH.usageError(`Unknown 'asFractionOf' keyword: '${asFractionOf}'`);
//
} else {
targetCoordinate = isVertical ? el => el[MC.S_SCROLL_TOP] + sign * amount : el => el[MC.S_SCROLL_LEFT] + sign * amount;
}
const target = isVertical ? {
top: targetCoordinate
} : {
left: targetCoordinate
};
return this.scrollTo(target, options);
};
// ----------
this.scrollTo = async (to, options) => {
var _options$duration;
return (0, _scroll.scrollTo)(to, MH.merge(options, {
duration: (_options$duration = options === null || options === void 0 ? void 0 : options.duration) !== null && _options$duration !== void 0 ? _options$duration : config._scrollDuration,
// default
scrollable: await (0, _scroll.fetchScrollableElement)(options === null || options === void 0 ? void 0 : options.scrollable) // override
}));
};
// ----------
this.fetchCurrentScrollAction = scrollable => (0, _scroll.fetchScrollableElement)(scrollable).then(element => (0, _scroll.getCurrentScrollAction)(element));
// ----------
this.stopUserScrolling = async options => {
const element = await (0, _scroll.fetchScrollableElement)(options === null || options === void 0 ? void 0 : options.scrollable);
const stopScroll = () => MH.elScrollTo(element, {
top: element[MC.S_SCROLL_TOP],
left: element[MC.S_SCROLL_LEFT]
});
if (options !== null && options !== void 0 && options.immediate) {
stopScroll();
} else {
(0, _domOptimize.waitForMeasureTime)().then(stopScroll);
}
};
// ----------
this.trackScroll = (handler, options) => {
if (!handler) {
handler = setScrollCssProps;
}
return setupOnScroll(handler, options, TRACK_FULL);
};
// ----------
this.noTrackScroll = (handler, scrollable) => {
if (!handler) {
handler = setScrollCssProps;
}
removeOnScroll(handler, scrollable, TRACK_FULL); // no need to await
};
// ----------
this.onScroll = (handler, options) => setupOnScroll(handler, options, TRACK_REGULAR);
// ----------
this.offScroll = (handler, scrollable) => {
removeOnScroll(handler, scrollable, TRACK_REGULAR); // no need to await
};
}
}
/**
* @interface
*/
/**
* @interface
*/
/**
* @interface
*/
/**
* The handler is invoked with two arguments:
*
* - the element that has been resized
* - the {@link ScrollData} for the element
*/
// ----------------------------------------
exports.ScrollWatcher = ScrollWatcher;
const CONSTRUCTOR_KEY = MC.SYMBOL();
const instances = MH.newMap();
const PREFIX_WRAPPER = MH.prefixName("scroll-watcher-wrapper");
const getConfig = config => {
config !== null && config !== void 0 ? config : config = {};
return {
_debounceWindow: (0, _math.toNonNegNum)(config[MC.S_DEBOUNCE_WINDOW], 75),
// If threshold is 0, internally treat as 1 (pixel)
_scrollThreshold: (0, _math.toNonNegNum)(config.scrollThreshold, 50) || 1,
_scrollDuration: (0, _math.toNonNegNum)(config.scrollDuration, 1000)
};
};
const TRACK_REGULAR = 1; // only scroll events
const TRACK_FULL = 2; // scroll + resizing of content and/or wrapper
// --------------------
const fetchOnScrollOptions = async (config, options) => {
var _options$MC$S_DEBOUNC;
const directions = (0, _validation.validateStrList)("directions", options.directions, _scroll.isValidScrollDirection) || null;
const element = await (0, _scroll.fetchScrollableElement)(options.scrollable);
return {
_element: element,
_eventTarget: getEventTarget(element),
_directions: directions,
// If threshold is 0, internally treat as 1 (pixel)
_threshold: (0, _math.toNonNegNum)(options.threshold, config._scrollThreshold) || 1,
_debounceWindow: (_options$MC$S_DEBOUNC = options[MC.S_DEBOUNCE_WINDOW]) !== null && _options$MC$S_DEBOUNC !== void 0 ? _options$MC$S_DEBOUNC : config._debounceWindow
};
};
const directionMatches = (userDirections, latestDirection) => !userDirections || MH.includes(userDirections, latestDirection);
const hasExceededThreshold = (options, latestData, lastThresholdData) => {
const directions = options._directions;
const threshold = options._threshold;
if (!lastThresholdData) {
/* istanbul ignore */
return false;
}
const topDiff = (0, _math.maxAbs)(latestData[MC.S_SCROLL_TOP] - lastThresholdData[MC.S_SCROLL_TOP], latestData[MC.S_SCROLL_HEIGHT] - lastThresholdData[MC.S_SCROLL_HEIGHT], latestData[MC.S_CLIENT_HEIGHT] - lastThresholdData[MC.S_CLIENT_HEIGHT]);
const leftDiff = (0, _math.maxAbs)(latestData[MC.S_SCROLL_LEFT] - lastThresholdData[MC.S_SCROLL_LEFT], latestData[MC.S_SCROLL_WIDTH] - lastThresholdData[MC.S_SCROLL_WIDTH], latestData[MC.S_CLIENT_WIDTH] - lastThresholdData[MC.S_CLIENT_WIDTH]);
// If the callback is only interested in up/down, then only check the
// scrollTop delta, and similarly for left/right.
let checkTop = false,
checkLeft = false;
if (!directions || MH.includes(directions, MC.S_NONE) || MH.includes(directions, MC.S_AMBIGUOUS)) {
checkTop = checkLeft = true;
} else {
if (MH.includes(directions, MC.S_UP) || MH.includes(directions, MC.S_DOWN)) {
checkTop = true;
}
if (MH.includes(directions, MC.S_LEFT) || MH.includes(directions, MC.S_RIGHT)) {
checkLeft = true;
}
}
return checkTop && topDiff >= threshold || checkLeft && leftDiff >= threshold;
};
const fetchScrollData = async (element, previousEventData, realtime) => {
var _previousEventData$sc, _previousEventData$sc2;
if (!realtime) {
await (0, _domOptimize.waitForMeasureTime)();
}
const scrollTop = MH.ceil(element[MC.S_SCROLL_TOP]);
const scrollLeft = MH.ceil(element[MC.S_SCROLL_LEFT]);
const scrollWidth = element[MC.S_SCROLL_WIDTH];
const scrollHeight = element[MC.S_SCROLL_HEIGHT];
const clientWidth = (0, _scroll.getClientWidthNow)(element);
const clientHeight = (0, _scroll.getClientHeightNow)(element);
const scrollTopFraction = MH.round(scrollTop) / (scrollHeight - clientHeight || MC.INFINITY);
const scrollLeftFraction = MH.round(scrollLeft) / (scrollWidth - clientWidth || MC.INFINITY);
const prevScrollTop = (_previousEventData$sc = previousEventData === null || previousEventData === void 0 ? void 0 : previousEventData.scrollTop) !== null && _previousEventData$sc !== void 0 ? _previousEventData$sc : 0;
const prevScrollLeft = (_previousEventData$sc2 = previousEventData === null || previousEventData === void 0 ? void 0 : previousEventData.scrollLeft) !== null && _previousEventData$sc2 !== void 0 ? _previousEventData$sc2 : 0;
const direction = (0, _directions.getMaxDeltaDirection)(scrollLeft - prevScrollLeft, scrollTop - prevScrollTop);
return {
direction,
[MC.S_CLIENT_WIDTH]: clientWidth,
[MC.S_CLIENT_HEIGHT]: clientHeight,
[MC.S_SCROLL_WIDTH]: scrollWidth,
[MC.S_SCROLL_HEIGHT]: scrollHeight,
[MC.S_SCROLL_TOP]: scrollTop,
[MC.S_SCROLL_TOP_FRACTION]: scrollTopFraction,
[MC.S_SCROLL_LEFT]: scrollLeft,
[MC.S_SCROLL_LEFT_FRACTION]: scrollLeftFraction
};
};
const setScrollCssProps = (element, scrollData) => {
let prefix = "";
if (element === (0, _scroll.tryGetMainScrollableElement)()) {
// Set the CSS vars on the root element
element = MH.getDocElement();
prefix = "page-";
}
scrollData !== null && scrollData !== void 0 ? scrollData : scrollData = {};
const props = {
[MC.S_SCROLL_TOP]: scrollData[MC.S_SCROLL_TOP],
[MC.S_SCROLL_TOP_FRACTION]: scrollData[MC.S_SCROLL_TOP_FRACTION],
[MC.S_SCROLL_LEFT]: scrollData[MC.S_SCROLL_LEFT],
[MC.S_SCROLL_LEFT_FRACTION]: scrollData[MC.S_SCROLL_LEFT_FRACTION],
[MC.S_SCROLL_WIDTH]: scrollData[MC.S_SCROLL_WIDTH],
[MC.S_SCROLL_HEIGHT]: scrollData[MC.S_SCROLL_HEIGHT]
};
(0, _cssAlter.setNumericStyleJsVars)(element, props, {
_prefix: prefix
});
};
const getEventTarget = element => {
if (element === MH.getDocScrollingElement()) {
return MH.getDoc();
}
return element;
};
const invokeCallback = (callback, element, scrollData) => callback.invoke(element, MH.copyObject(scrollData)).catch(_log.logError);
//# sourceMappingURL=scroll-watcher.cjs.map