152 lines
4.6 KiB
JavaScript
152 lines
4.6 KiB
JavaScript
import { isElement, isFunction } from "@thednp/shorty";
|
|
//#region package.json
|
|
var version = "1.1.3";
|
|
//#endregion
|
|
//#region src/index.ts
|
|
const errorString = "PositionObserver Error";
|
|
/**
|
|
* The PositionObserver class is a utility class that observes the position
|
|
* of DOM elements and triggers a callback when their position changes.
|
|
*/
|
|
var PositionObserver = class {
|
|
entries;
|
|
static version = version;
|
|
/** `PositionObserver.tick` */
|
|
_t;
|
|
/** `PositionObserver.root` */
|
|
_r;
|
|
/** `PositionObserver.root.clientWidth` */
|
|
_w;
|
|
/** `PositionObserver.root.clientHeight` */
|
|
_h;
|
|
/** `IntersectionObserver.options.rootMargin` */
|
|
_rm;
|
|
/** `IntersectionObserver.options.threshold` */
|
|
_th;
|
|
/** `PositionObserver.callback` */
|
|
_c;
|
|
/**
|
|
* The constructor takes two arguments, a `callback`, which is called
|
|
* whenever the position of an observed element changes and an `options` object.
|
|
* The callback function takes an array of `PositionObserverEntry` objects
|
|
* as its first argument and the PositionObserver instance as its second argument.
|
|
*
|
|
* @param callback the callback that applies to all targets of this observer
|
|
* @param options the options of this observer
|
|
*/
|
|
constructor(callback, options) {
|
|
if (!isFunction(callback)) throw new Error(`${errorString}: ${callback} is not a function.`);
|
|
this.entries = /* @__PURE__ */ new Map();
|
|
this._c = callback;
|
|
this._t = 0;
|
|
const root = isElement(options?.root) ? options.root : document?.documentElement;
|
|
this._r = root;
|
|
this._rm = options?.rootMargin;
|
|
this._th = options?.threshold;
|
|
this._w = root.clientWidth;
|
|
this._h = root.clientHeight;
|
|
}
|
|
/**
|
|
* Start observing the position of the specified element.
|
|
* If the element is not currently attached to the DOM,
|
|
* it will NOT be added to the entries.
|
|
*
|
|
* @param target an `Element` target
|
|
*/
|
|
observe = (target) => {
|
|
if (!isElement(target)) throw new Error(`${errorString}: ${target} is not an instance of Element.`);
|
|
/* istanbul ignore else @preserve - a guard must be set */
|
|
if (!this._r.contains(target)) return;
|
|
this._n(target).then((ioEntry) => {
|
|
/* istanbul ignore else @preserve - don't allow duplicate entries */
|
|
if (ioEntry.boundingClientRect && !this.getEntry(target)) this.entries.set(target, ioEntry);
|
|
/* istanbul ignore else @preserve */
|
|
if (!this._t) this._t = requestAnimationFrame(this._rc);
|
|
});
|
|
};
|
|
/**
|
|
* Stop observing the position of the specified element.
|
|
*
|
|
* @param target an `Element` target
|
|
*/
|
|
unobserve = (target) => {
|
|
/* istanbul ignore else @preserve */
|
|
if (this.entries.has(target)) this.entries.delete(target);
|
|
};
|
|
/**
|
|
* Private method responsible for all the heavy duty,
|
|
* the observer's runtime.
|
|
* `PositionObserver.runCallback`
|
|
*/
|
|
_rc = () => {
|
|
/* istanbul ignore if @preserve - a guard must be set */
|
|
if (!this.entries.size) {
|
|
this._t = 0;
|
|
return;
|
|
}
|
|
const { clientWidth, clientHeight } = this._r;
|
|
const queue = new Promise((resolve) => {
|
|
const updates = [];
|
|
this.entries.forEach(({ target, boundingClientRect: oldBoundingBox }) => {
|
|
/* istanbul ignore if @preserve - a guard must be set when target has been removed */
|
|
if (!this._r.contains(target)) return;
|
|
this._n(target).then((ioEntry) => {
|
|
/* istanbul ignore if @preserve - make sure to only count visible entries */
|
|
if (!ioEntry.isIntersecting) return;
|
|
const { left, top } = ioEntry.boundingClientRect;
|
|
/* istanbul ignore else @preserve - only schedule entries that changed position */
|
|
if (oldBoundingBox.top !== top || oldBoundingBox.left !== left || this._w !== clientWidth || this._h !== clientHeight) {
|
|
this.entries.set(target, ioEntry);
|
|
updates.push(ioEntry);
|
|
}
|
|
});
|
|
});
|
|
this._w = clientWidth;
|
|
this._h = clientHeight;
|
|
resolve(updates);
|
|
});
|
|
this._t = requestAnimationFrame(async () => {
|
|
const updates = await queue;
|
|
/* istanbul ignore else @preserve */
|
|
if (updates.length) this._c(updates, this);
|
|
this._rc();
|
|
});
|
|
};
|
|
/**
|
|
* Check intersection status and resolve it
|
|
* right away.
|
|
*
|
|
* `PositionObserver.newEntryForTarget`
|
|
*
|
|
* @param target an `Element` target
|
|
*/
|
|
_n = (target) => {
|
|
return new Promise((resolve) => {
|
|
new IntersectionObserver(([ioEntry], ob) => {
|
|
ob.disconnect();
|
|
resolve(ioEntry);
|
|
}, {
|
|
threshold: this._th,
|
|
rootMargin: this._rm
|
|
}).observe(target);
|
|
});
|
|
};
|
|
/**
|
|
* Find the entry for a given target.
|
|
*
|
|
* @param target an `HTMLElement` target
|
|
*/
|
|
getEntry = (target) => this.entries.get(target);
|
|
/**
|
|
* Immediately stop observing all elements.
|
|
*/
|
|
disconnect = () => {
|
|
cancelAnimationFrame(this._t);
|
|
this.entries.clear();
|
|
this._t = 0;
|
|
};
|
|
};
|
|
//#endregion
|
|
export { PositionObserver as default };
|
|
|
|
//# sourceMappingURL=index.js.map
|