UNPKG

lisn.js

Version:

Simply handle user gestures and actions. Includes widgets.

760 lines (707 loc) 30.6 kB
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/ViewWatcher */ import * as MC from "../globals/minification-constants.js"; import * as MH from "../globals/minification-helpers.js"; import { setNumericStyleJsVars } from "../utils/css-alter.js"; import { waitForInteractive } from "../utils/dom-events.js"; import { waitForMeasureTime, waitForSubsequentMeasureTime } from "../utils/dom-optimize.js"; import { logError } from "../utils/log.js"; import { omitKeys, compareValuesIn } from "../utils/misc.js"; import { createOverlay } from "../utils/overlays.js"; import { getClosestScrollable } from "../utils/scroll.js"; import { fetchViewportSize } from "../utils/size.js"; import { toMargins, objToStrKey } from "../utils/text.js"; import { VIEWS_SPACE, getViewsBitmask, parseScrollOffset } from "../utils/views.js"; import { wrapCallback } from "../modules/callback.js"; import { newXMap, newXWeakMap } from "../modules/x-map.js"; import { XIntersectionObserver } from "../modules/x-intersection-observer.js"; import { DOMWatcher } from "./dom-watcher.js"; import { ScrollWatcher } from "./scroll-watcher.js"; import { SizeWatcher } from "./size-watcher.js"; import debug from "../debug/debug.js"; /** * {@link ViewWatcher} monitors the position of a given target relative to the * given {@link ViewWatcherConfig.root | root} or the viewport. * * It's built on top of * {@link https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver | IntersectionObserver}. * * It manages registered callbacks globally and reuses IntersectionObservers * for more efficient performance. */ export class ViewWatcher { /** * Creates a new instance of ViewWatcher with the given * {@link ViewWatcherConfig}. It does not save it for future reuse. */ static create(config) { return new ViewWatcher(getConfig(config), CONSTRUCTOR_KEY); } /** * Returns an existing instance of ViewWatcher with the given * {@link ViewWatcherConfig}, 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) { var _instances$get; const myConfig = getConfig(config); const configStrKey = objToStrKey(omitKeys(myConfig, { _root: null })); let instance = (_instances$get = instances.get(myConfig._root)) === null || _instances$get === void 0 ? void 0 : _instances$get.get(configStrKey); if (!instance) { instance = new ViewWatcher(myConfig, CONSTRUCTOR_KEY); instances.sGet(myConfig._root).set(configStrKey, instance); } return instance; } constructor(config, key) { /** * Call the given handler whenever the {@link ViewWatcherConfig.root | root}'s * view relative to the target position changes, i.e. when the target enters * or leaves the root. * * Unless {@link OnViewOptions.skipInitial} is true, the handler is also * called (almost) immediately with the current view if it matches this * set of options*. * * **IMPORTANT:** The same handler can _not_ be added multiple times for the * same target, even if the options differ. If the handler has already been * added for this target, either using {@link trackView} or using * {@link onView}, then it will be removed and re-added with the current * options. So if previously it was also tracking position across root * using {@link trackView}, it will no longer do so. * * @throws {@link Errors.LisnUsageError | LisnUsageError} * If the target or the options are invalid. */ _defineProperty(this, "onView", void 0); /** * Removes a previously added handler. * * @throws {@link Errors.LisnUsageError | LisnUsageError} * If the target is invalid. */ _defineProperty(this, "offView", void 0); /** * This does more than just {@link onView}. The difference is that in * addition to a change of {@link View}, such as the target entering or * leaving the ViewWatcher's {@link ViewWatcherConfig.root | root} (by * default the viewport), the handler is also called each time the target's * relative view changes _while inside the root_. * * A change of relative position happens when: * - the target is resized * - the root is resized * - the any of the target's scrollable ancestors is scrolled * - the target's attributes changed that resulted in a change of position * * All of the above are accounted for. Internally it uses * {@link ScrollWatcher}, {@link DOMWatcher} and {@link SizeWatcher} to keep * track of all of this. * * If the target is leaves the ViewWatcher's * {@link ViewWatcherConfig.root | root}, the handler will be called with * the {@link ViewData}, and the above events will stop being tracked until * the target enters the watcher's root again. * * **IMPORTANT:** The same handler can _not_ be added multiple times for the * same target, even if the options differ. If the handler has already been * added for this target, either using {@link trackView} or using * {@link onView}, 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 the following set of CSS variables on the target's style and * represent its relative position: * * - `--lisn-js--r-top` * - `--lisn-js--r-bottom` * - `--lisn-js--r-left` * - `--lisn-js--r-right` * - `--lisn-js--r-width` * - `--lisn-js--r-height` * - `--lisn-js--r-h-middle` * - `--lisn-js--r-v-middle` * * See {@link ViewData.relative} for an explanation of each. * * Note that only Element targets are supported here and not offsets. * * @throws {@link Errors.LisnUsageError | LisnUsageError} * If the target or "views" are invalid. */ _defineProperty(this, "trackView", void 0); /** * Removes a previously added handler for {@link trackView}. * * @throws {@link Errors.LisnUsageError | LisnUsageError} * If the target is invalid. */ _defineProperty(this, "noTrackView", void 0); /** * Get the current view relative to the target. 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. */ _defineProperty(this, "fetchCurrentView", void 0); if (key !== CONSTRUCTOR_KEY) { throw MH.illegalConstructorError("ViewWatcher.create"); } const logger = debug ? new debug.Logger({ name: "ViewWatcher", logAtCreation: config }) : null; const allViewData = MH.newWeakMap(); const allCallbacks = newXWeakMap(() => MH.newMap()); const intersectionHandler = entries => { debug: logger === null || logger === void 0 || logger.debug9(`Got ${entries.length} new entries`, entries); for (const entry of entries) { processEntry(entry); } }; const observeOptions = { root: config._root, threshold: config._threshold, rootMargin: config._rootMargin }; const xObserver = new XIntersectionObserver(intersectionHandler, observeOptions); // ---------- const fetchCurrentView = (element, realtime = false) => { const fetchData = async entryOrElement => { const intersection = await fetchIntersectionData(config, entryOrElement, realtime); const data = await fetchViewData(intersection, realtime); return data; }; if (realtime) { return fetchData(element); } return MH.newPromise(resolve => { // Use a temp IntersectionObserver const observer = MH.newIntersectionObserver(entries => { const promise = fetchData(entries[0]); observer.disconnect(); promise.then(resolve); }, observeOptions); observer.observe(element); }); }; // ---------- 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 = wrapCallback(handler); callback.onRemove(() => { deleteHandler(handler, options); }); allCallbacks.sGet(element).set(handler, { _callback: callback, _trackType: trackType, _options: options }); return callback; }; // ---------- const setupOnView = async (target, handler, userOptions, trackType) => { const options = await fetchOptions(config._root, target, userOptions); const element = options._element; const callback = createCallback(handler, options, trackType); // View watcher should be used before the DOM is loaded since the initial // size of the root may be 0 or close to 0 and would lead to premature // triggering. await waitForInteractive(); // Initial call doesn't need to be realtime, and best to use an actual // IntersectionObserverEntry for that one. let viewData = await fetchCurrentView(element); if (viewData.rootBounds[MC.S_WIDTH] === 0 && viewData.rootBounds[MC.S_HEIGHT] === 0) { // Possibly the root is being setup now, wait for one AF debug: logger === null || logger === void 0 || logger.debug5("Got zero root size, deferring for a bit", config._root); await waitForSubsequentMeasureTime(); viewData = await fetchCurrentView(element); } if (trackType === TRACK_FULL) { // Detect resize or scroll await setupInviewTrack(options, callback, viewData); } if (callback.isRemoved()) { return; } // Always use observeLater to skip the initial call from the // IntersectionObserver, and call callbacks that have skipInitial: false // here. Otherwise, we can't tell from inside the intersectionHandler whether // a callback wants to skip its initial call or not. // // It's ok if already observed, won't do anything. xObserver.observeLater(element); if (!(userOptions !== null && userOptions !== void 0 && userOptions.skipInitial)) { debug: logger === null || logger === void 0 || logger.debug5("Calling initially with", element, viewData); if (viewsToBitmask(viewData.views) & options._viewsBitmask) { await invokeCallback(callback, element, viewData); } } }; // ---------- const removeOnView = async (target, handler, trackType) => { var _allCallbacks$get2; // For time sync, so that if called immediately after onView without // awaiting, it will remove the callback that is about to be added. // But if no such handler has been added we may unnecessarily // create an overlay... TODO const options = await fetchOptions(config._root, target, {}); 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 === setViewCssProps) { // delete the properties setViewCssProps(element, null); } } }; // ---------- const deleteHandler = (handler, options) => { const element = options._element; MH.deleteKey(allCallbacks.get(element), handler); allCallbacks.prune(element); if (!allCallbacks.has(element)) { debug: logger === null || logger === void 0 || logger.debug4("No more callbacks for target; unobserving", element); xObserver.unobserve(element); MH.deleteKey(allViewData, element); } }; // ---------- const processEntry = async entry => { // In reality, it can't be just a base Element const element = MH.targetOf(entry); // This doesn't need to be "realtime", since IntersectionObserver alone // introduces a delay. const intersection = await fetchIntersectionData(config, entry); const latestData = await fetchViewData(intersection); debug: logger === null || logger === void 0 || logger.debug9("Got ViewData", element, latestData); const viewsBitmask = viewsToBitmask(latestData.views); for (const entry of ((_allCallbacks$get3 = allCallbacks.get(element)) === null || _allCallbacks$get3 === void 0 ? void 0 : _allCallbacks$get3.values()) || []) { var _allCallbacks$get3; if (viewsBitmask & entry._options._viewsBitmask) { invokeCallback(entry._callback, element, latestData); } } }; // ---------- const setupInviewTrack = async (options, viewCallback, viewData) => { const element = options._element; debug: logger === null || logger === void 0 || logger.debug8("Setting up size, scroll and attribute tracking", element); const sizeWatcher = SizeWatcher.reuse(); const scrollWatcher = ScrollWatcher.reuse(); const realtime = options._debounceWindow === 0; // Detect when target's class or style attribute change const domWatcher = DOMWatcher.create({ root: element, // only direct children subtree: false }); // We need to remove the tracking callback when target leaves view and re-add // it when it enters view. But the OnViewCallback that is associated may have // already been added prior, by calling onView with this handler, so we can't // always wrap around it, in order to detect when it's called with a change // of view. So we setup another OnViewCallback tied to the tracking callback. let isInview = false; let removeTrackCallback = null; // Finds any scrollable ancestors of the element and detect scroll on them. const scrollableAncestors = await fetchScrollableAncestors(element, realtime); if (viewCallback.isRemoved()) { return; } const addTrackCallback = () => { var _config$_root; const trackCallback = wrapCallback(async () => { const prevData = allViewData.get(element); // Get the latest view data for the target const latestData = await fetchCurrentView(element, realtime); debug: logger === null || logger === void 0 || logger.debug9("Got ViewData", element, latestData); const changed = viewChanged(latestData, prevData); if (changed) { // When comparing for changes, we round the numbers to certain number // of decimal places, and allViewData serves as a "last threshold" // state, so only update it if there was a significant change. // Otherwise very quick changes in small increments would get // rejected as "no change". allViewData.set(element, latestData); if (isInview && !viewCallback.isRemoved()) { // Could have been removed during the debounce window await invokeCallback(viewCallback, element, latestData); } } else { debug: logger === null || logger === void 0 || logger.debug9("ViewData same as last"); } }); // TODO Is there a better way to detect when it's moved? viewCallback.onRemove(trackCallback.remove); removeTrackCallback = trackCallback.remove; // Detect when target's class or style attribute change domWatcher.onMutation(trackCallback, { categories: [MC.S_ATTRIBUTE], [MC.S_SKIP_INITIAL]: true }); // Detect when target is resized sizeWatcher.onResize(trackCallback, { target: element, [MC.S_DEBOUNCE_WINDOW]: options._debounceWindow, threshold: options._resizeThreshold, [MC.S_SKIP_INITIAL]: true }); // Detect when the root is resized sizeWatcher.onResize(trackCallback, { target: (_config$_root = config._root) !== null && _config$_root !== void 0 ? _config$_root : MH.getWindow(), [MC.S_DEBOUNCE_WINDOW]: options._debounceWindow, threshold: options._resizeThreshold, [MC.S_SKIP_INITIAL]: true }); // Detect when the target's scrollable ancestors are scrolled (this // will almost certainly include the main scrollable element). for (const ancestor of scrollableAncestors) { scrollWatcher.onScroll(trackCallback, { scrollable: ancestor, [MC.S_DEBOUNCE_WINDOW]: options._debounceWindow, threshold: options._scrollThreshold, [MC.S_SKIP_INITIAL]: true }); } }; const enterOrLeaveCallback = createCallback((target__ignored, viewData) => { if (viewData.views[0] === MC.S_AT) { if (!isInview) { isInview = true; addTrackCallback(); } } else if (removeTrackCallback) { isInview = false; removeTrackCallback(); removeTrackCallback = null; } }, MH.assign(options, { _viewsBitmask: VIEWS_SPACE.bitmask }), TRACK_REGULAR); viewCallback.onRemove(enterOrLeaveCallback.remove); allViewData.set(element, viewData); // to avoid duplicate initial call // Setup the track and the "inView" state if (!enterOrLeaveCallback.isRemoved()) { invokeCallback(enterOrLeaveCallback, element, viewData); } }; // ---------- this.fetchCurrentView = (target, realtime = false) => fetchElement(config._root, target).then(element => fetchCurrentView(element, realtime)); // ---------- this.trackView = (element, handler, options) => { if (!handler) { handler = setViewCssProps; } return setupOnView(element, handler, options, TRACK_FULL); }; // ---------- this.noTrackView = (element, handler) => { if (!handler) { handler = setViewCssProps; } removeOnView(element, handler, TRACK_FULL); // no need to await }; // ---------- this.onView = (target, handler, options) => setupOnView(target, handler, options, TRACK_REGULAR); // ---------- this.offView = (target, handler) => removeOnView(target, handler, TRACK_REGULAR); // no need to await } } /** * @interface */ /** * @interface */ /** * @interface */ /** * The handler is invoked with two arguments: * * - The element that is the target of the IntersectionObserver. If the call to * {@link ViewWatcher.onView} specified an element as the target, it will be * the same. If it specified an offset, then the element passed to the * callback will be an absolutely positioned trigger overlay that's created * as a result. * - the {@link ViewData} for relative to the target */ // ---------------------------------------- const CONSTRUCTOR_KEY = MC.SYMBOL(); const instances = newXMap(() => MH.newMap()); const getConfig = config => { var _config$root, _config$rootMargin, _config$threshold; return { _root: (_config$root = config === null || config === void 0 ? void 0 : config.root) !== null && _config$root !== void 0 ? _config$root : null, _rootMargin: (_config$rootMargin = config === null || config === void 0 ? void 0 : config.rootMargin) !== null && _config$rootMargin !== void 0 ? _config$rootMargin : "0px 0px 0px 0px", _threshold: (_config$threshold = config === null || config === void 0 ? void 0 : config.threshold) !== null && _config$threshold !== void 0 ? _config$threshold : 0 }; }; const TRACK_REGULAR = 1; // only entering/leaving root const TRACK_FULL = 2; // entering/leaving + moving across (fine-grained) // -------------------- const fetchOptions = async (root, target, options) => { return { _element: await fetchElement(root, target), _viewsBitmask: getViewsBitmask(options === null || options === void 0 ? void 0 : options.views), _debounceWindow: options === null || options === void 0 ? void 0 : options.debounceWindow, _resizeThreshold: options === null || options === void 0 ? void 0 : options.resizeThreshold, _scrollThreshold: options === null || options === void 0 ? void 0 : options.scrollThreshold }; }; const fetchScrollableAncestors = async (element, realtime) => { if (!realtime) { await waitForMeasureTime(); } const scrollableAncestors = []; let ancestor = element; while (ancestor = getClosestScrollable(ancestor, { active: true })) { scrollableAncestors.push(ancestor); } return scrollableAncestors; }; const viewChanged = (latestData, prevData) => !prevData || viewsToBitmask(prevData.views) !== viewsToBitmask(latestData.views) || !compareValuesIn(MH.copyBoundingRectProps(prevData.targetBounds), MH.copyBoundingRectProps(latestData.targetBounds)) || !compareValuesIn(prevData.rootBounds, latestData.rootBounds) || !compareValuesIn(prevData.relative, latestData.relative); const viewsToBitmask = views => VIEWS_SPACE.bit[views[0]] | (views[1] ? VIEWS_SPACE.bit[views[1]] : 0); const fetchIntersectionData = async (config, entryOrTarget, realtime = false) => { const root = config._root; const vpSize = await fetchViewportSize(realtime); const rootMargins = toMargins(config._rootMargin, vpSize); let target; let targetBounds; let rootBounds = null; let isIntersecting = null; let isCrossOrigin = null; if (MH.isInstanceOf(entryOrTarget, IntersectionObserverEntry)) { target = entryOrTarget.target; targetBounds = entryOrTarget.boundingClientRect; rootBounds = entryOrTarget.rootBounds; isIntersecting = entryOrTarget.isIntersecting; isCrossOrigin = !entryOrTarget.rootBounds; } else { target = entryOrTarget; targetBounds = await fetchBounds(target, realtime); } if (!rootBounds) { rootBounds = await fetchBounds(root, realtime, rootMargins); } return { _target: target, _targetBounds: targetBounds, _root: root, _rootMargins: rootMargins, _rootBounds: rootBounds, _isIntersecting: isIntersecting, _isCrossOrigin: isCrossOrigin }; }; const fetchBounds = async (root, realtime, rootMargins) => { let rect; if (root) { if (!realtime) { await waitForMeasureTime(); } rect = MH.copyBoundingRectProps(MH.getBoundingClientRect(root)); } else { const { width, height } = await fetchViewportSize(realtime); rect = { x: 0, left: 0, right: width, width, y: 0, top: 0, bottom: height, height }; } if (rootMargins) { rect.x = rect[MC.S_LEFT] -= rootMargins[3]; rect[MC.S_RIGHT] += rootMargins[1]; rect[MC.S_WIDTH] += rootMargins[1] + rootMargins[3]; rect.y = rect[MC.S_TOP] -= rootMargins[0]; rect[MC.S_BOTTOM] += rootMargins[2]; rect[MC.S_HEIGHT] += rootMargins[0] + rootMargins[2]; } return rect; }; const fetchViewData = async (intersection, realtime = false) => { var _intersection$_isInte; const vpSize = await fetchViewportSize(realtime); const vpHeight = vpSize[MC.S_HEIGHT]; const vpWidth = vpSize[MC.S_WIDTH]; const views = await fetchViews(intersection, realtime); const relative = MH.merge({ hMiddle: NaN, vMiddle: NaN }, MH.copyBoundingRectProps(intersection._targetBounds)); relative.y /= vpHeight; relative[MC.S_TOP] /= vpHeight; relative[MC.S_BOTTOM] /= vpHeight; relative[MC.S_HEIGHT] /= vpHeight; relative.x /= vpWidth; relative[MC.S_LEFT] /= vpWidth; relative[MC.S_RIGHT] /= vpWidth; relative[MC.S_WIDTH] /= vpWidth; relative.hMiddle = (relative[MC.S_LEFT] + relative[MC.S_RIGHT]) / 2; relative.vMiddle = (relative[MC.S_TOP] + relative[MC.S_BOTTOM]) / 2; const viewData = { isIntersecting: (_intersection$_isInte = intersection._isIntersecting) !== null && _intersection$_isInte !== void 0 ? _intersection$_isInte : views[0] === MC.S_AT, targetBounds: intersection._targetBounds, rootBounds: intersection._rootBounds, views, relative }; return viewData; }; const fetchViews = async (intersection, realtime, useScrollingAncestor) => { if (intersection._isIntersecting) { return [MC.S_AT]; } let rootBounds; if (useScrollingAncestor) { rootBounds = await fetchBounds(useScrollingAncestor, realtime, intersection._rootMargins); } else { rootBounds = intersection._rootBounds; } const targetBounds = intersection._targetBounds; const delta = { _left: rootBounds[MC.S_LEFT] - targetBounds[MC.S_LEFT], _right: targetBounds[MC.S_RIGHT] - rootBounds[MC.S_RIGHT], _top: rootBounds[MC.S_TOP] - targetBounds[MC.S_TOP], _bottom: targetBounds[MC.S_BOTTOM] - rootBounds[MC.S_BOTTOM] }; let xView = null; let yView = null; if (delta._left > 0 && delta._right > 0) { // Target is wider than root: use greater delta to determine position. // Remember, the view is the _root_ position relative to target. xView = delta._left > delta._right ? MC.S_RIGHT : MC.S_LEFT; } else if (delta._left > 0) { // Target is to the left of the root xView = MC.S_RIGHT; } else if (delta._right > 0) { // Target is to the right of the root xView = MC.S_LEFT; } // else target is horizontally contained in root, see below if (delta._top > 0 && delta._bottom > 0) { // Target is taller than root: use greater delta to determine position. // Remember, the view is the _root_ position relative to target. yView = delta._top > delta._bottom ? MC.S_BELOW : MC.S_ABOVE; } else if (delta._top > 0) { // Target is above the root yView = MC.S_BELOW; } else if (delta._bottom > 0) { // Target is below the root yView = MC.S_ABOVE; } // else target is vertically contained in root, see below if (xView && yView) { // diagonally out of vide return [xView, yView]; } else if (xView) { // horizontally out of vide return [xView]; } else if (yView) { // vertically out of vide return [yView]; } // The target is contained in the root bounds and yet isIntersecting was // not true. This means that either: // // 1. It may be intersecting, but we didn't get an actual // IntersectionObserverEntry and we don't know if it's intersecting // or not // 2. The target is inside a scrolling element that is _not_ being used as // the observer root, and the target has scrolled out of the scrollable // bounds but still inside the viewport // 3. We're inside a cross-origin iFrame and the iFrame is partially or // fully not-intersecting if (!intersection._isCrossOrigin) { // This is case 1. or 2. => get the views relative to the closest // scrollable ancestor relative to which it is _not_ intersecting, if // any. If it's nested inside several scrolling elements, we'll end up // looping over each one until we find the one for which the target is // outside its box. // // It is too risky to use active isScrollable check here since we could be // inside an onScroll handler, so just use passive. const scrollingAncestor = getClosestScrollable(useScrollingAncestor !== null && useScrollingAncestor !== void 0 ? useScrollingAncestor : intersection._target); if (scrollingAncestor) { return fetchViews(intersection, realtime, scrollingAncestor); } } // Either case 3. (cross-origin iframe outside the viewport) or case 1. and // the target is actually intersecting the root. Either way, it's to be // considered in-view of its root. return [MC.S_AT]; }; const setViewCssProps = (element, viewData) => { var _viewData$relative; const relative = (_viewData$relative = viewData === null || viewData === void 0 ? void 0 : viewData.relative) !== null && _viewData$relative !== void 0 ? _viewData$relative : {}; const props = { top: relative.top, bottom: relative.bottom, left: relative.left, right: relative.right, [MC.S_WIDTH]: relative[MC.S_WIDTH], [MC.S_HEIGHT]: relative[MC.S_HEIGHT], hMiddle: relative.hMiddle, vMiddle: relative.vMiddle }; setNumericStyleJsVars(element, props, { _prefix: "r-", _numDecimal: 4 }); // don't await here }; const fetchElement = async (root, target) => { if (MH.isElement(target)) { return target; } else if (!MH.isString(target)) { throw MH.usageError("'target' must be an offset string or an HTMLElement | SVGElement | MathMLElement"); } const overlayOptions = getOverlayOptions(root, target); return await createOverlay(overlayOptions); }; const getOverlayOptions = (root, target) => { const { reference, value } = parseScrollOffset(target); let ovrDimension; if (reference === MC.S_TOP || reference === MC.S_BOTTOM) { ovrDimension = MC.S_WIDTH; } else if (reference === MC.S_LEFT || reference === MC.S_RIGHT) { ovrDimension = MC.S_HEIGHT; } else { throw MH.usageError(`Invalid offset reference: '${reference}'`); } return { parent: MH.isHTMLElement(root) ? root : undefined, style: { [reference]: value, [ovrDimension]: "100%" } }; }; const invokeCallback = (callback, element, viewData) => callback.invoke(element, MH.copyObject(viewData)).catch(logError); //# sourceMappingURL=view-watcher.js.map