@smoovy/observer
Version:
Simple and easy-to-use ticker
294 lines (293 loc) • 8.82 kB
JavaScript
"use strict";
import { EventEmitter } from "@smoovy/emitter";
import { listenCompose } from "@smoovy/listener";
export var ObservableEventType = /* @__PURE__ */ ((ObservableEventType2) => {
ObservableEventType2["VISIBILITY_CHANGE"] = "visibilitychange";
ObservableEventType2["DIMENSIONS_CHANGE"] = "dimensionschange";
return ObservableEventType2;
})(ObservableEventType || {});
export function observe(target, config) {
return new Observable({ target, ...config || {} });
}
export function unobserve(observable) {
observable.destroy();
}
const _Observable = class extends EventEmitter {
constructor(config) {
super();
this.config = config;
this.lastResize = 0;
this.visibilityTimer = -1;
this._left = 0;
this._top = 0;
this._width = 0;
this._height = 0;
this._scrollWidth = 0;
this._scrollHeight = 0;
this._visible = false;
this._interecKey = "{}";
if (config.autoAttach !== false) {
this.attach();
}
}
static handleEntries(entries, cb, intersecKey) {
entries.forEach((entry) => {
let observables = _Observable.items.get(entry.target);
if (observables) {
if (intersecKey) {
observables = observables.filter((o) => o.intersecKey === intersecKey);
}
for (let i = 0, len = observables.length; i < len; i++) {
cb(observables[i], entry);
}
}
});
}
get resizeDebounce() {
return this.config.resizeDebounce || 0;
}
get resizeDetection() {
return this.config.resizeDetection;
}
get visibilityDelay() {
return this.config.visibilityDelay || 0;
}
get visibilityThreshold() {
return typeof this.config.visibilityDetection === "object" ? this.config.visibilityDetection.threshold || 0 : 0;
}
get ref() {
return this.config.target;
}
get visible() {
return this._visible;
}
set visible(visible) {
if (visible !== this._visible) {
clearTimeout(this.visibilityTimer);
if (this.visibilityDelay > 0) {
this.visibilityTimer = setTimeout(
() => this.emitVisibility(visible),
this.visibilityDelay
);
} else {
this.emitVisibility(visible);
}
}
}
get left() {
return this._left;
}
get top() {
return this._top;
}
get x() {
return this._left;
}
get y() {
return this._top;
}
get width() {
return this._width;
}
get height() {
return this._height;
}
get scrollWidth() {
return this._scrollWidth;
}
get scrollHeight() {
return this._scrollHeight;
}
get scrollSize() {
return { width: this.scrollWidth, height: this.scrollHeight };
}
get size() {
return { width: this.width, height: this.height };
}
get pos() {
return { left: this.left, top: this.top };
}
get coord() {
return { x: this.left, y: this.top };
}
get intersecKey() {
return this._interecKey;
}
attach() {
const config = this.config;
if (!(config.target instanceof HTMLElement) && !(config.target instanceof Window)) {
throw new Error("target type is not valid: " + typeof config.target);
}
if (_Observable.items.has(config.target)) {
_Observable.items.get(config.target)?.push(this);
} else {
_Observable.items.set(config.target, [this]);
}
if (config.visibilityDetection && config.target instanceof HTMLElement) {
const observers = _Observable.intersecObservers;
const observerConfig = this.getIntersectionObserverConfig();
this._interecKey = this.getIdFromConfig(observerConfig);
if (!observers.has(this._interecKey)) {
observers.set(this._interecKey, new IntersectionObserver((entries) => {
_Observable.handleEntries(
entries,
(observable, entry) => {
observable.visible = entry.isIntersecting;
},
this._interecKey
);
}, observerConfig));
}
this.intersecObserver = observers.get(this._interecKey);
this.intersecObserver?.observe(config.target);
}
if (config.resizeDetection) {
if (!_Observable.resizeObserver) {
_Observable.resizeObserver = new ResizeObserver((entries) => {
_Observable.handleEntries(
entries,
(observable) => observable.update()
);
});
if (window) {
window.addEventListener("resize", () => {
_Observable.items.forEach((observables) => {
for (let i = 0, len = observables.length; i < len; i++) {
const observable = observables[i];
if (observable?.resizeDetection) {
const periods2 = typeof this.config.resizePeriods !== void 0 ? [250, 500] : this.config.resizePeriods;
requestAnimationFrame(() => observable.update());
if (Array.isArray(periods2)) {
periods2.forEach((ms) => {
setTimeout(() => observable.update(), ms);
});
}
}
}
});
});
}
}
if (config.target instanceof HTMLElement) {
_Observable.resizeObserver.observe(config.target, {
...typeof config.resizeDetection === "object" ? config.resizeDetection : {}
});
}
}
const periods = config.initUpdatePeriods !== void 0 ? config.initUpdatePeriods : [50, 250, 500, 1e3];
this.update();
requestAnimationFrame(() => this.update());
periods.forEach((ms) => setTimeout(() => this.update(), ms));
}
onDimChange(listener) {
return this.on("dimensionschange" /* DIMENSIONS_CHANGE */, listener);
}
onVisChange(listener) {
return this.on("visibilitychange" /* VISIBILITY_CHANGE */, listener);
}
onChange(listener) {
return listenCompose(
this.onDimChange(listener),
this.onVisChange(listener)
);
}
update() {
const now = window.performance.now();
let rect;
if (now - this.lastResize <= this.resizeDebounce) {
return;
}
this.lastResize = now;
if (this.ref instanceof Window) {
rect = {
left: 0,
top: 0,
width: window.innerWidth,
height: window.innerHeight
};
} else {
if (this.config.useBounds !== false) {
rect = this.getElementOffset(this.ref);
} else {
rect = this.ref.getBoundingClientRect();
}
}
if (rect.left !== this._left || rect.top !== this._top || rect.width !== this._width || rect.height !== this._height) {
this._left = rect.left;
this._top = rect.top;
this._width = rect.width;
this._height = rect.height;
if (this.ref instanceof Element) {
this._scrollHeight = this.ref.scrollHeight;
this._scrollWidth = this.ref.scrollWidth;
}
this.emit("dimensionschange" /* DIMENSIONS_CHANGE */, this);
}
}
destroy() {
const observables = _Observable.items.get(this.ref);
if (observables) {
const index = observables.indexOf(this);
if (index > -1) {
observables.splice(index, 1);
}
if (this.ref instanceof HTMLElement) {
this.intersecObserver?.unobserve(this.ref);
_Observable.resizeObserver?.unobserve(this.ref);
}
if (observables.length === 0) {
_Observable.items.delete(this.ref);
}
}
}
emitVisibility(visible) {
this._visible = visible;
if (visible && this.intersecObserver && this.config.detectVisibilityOnce) {
this.intersecObserver.unobserve(this.ref);
}
this.emit("visibilitychange" /* VISIBILITY_CHANGE */, this);
}
getIntersectionObserverConfig() {
const config = this.config.visibilityDetection;
if (typeof config === "object") {
return config;
}
return {};
}
getIdFromConfig(config) {
const secConfig = {
rt: void 0,
rm: config.rootMargin,
th: config.threshold
};
if (config.root instanceof HTMLElement) {
if (config.root.dataset.observerId) {
secConfig.rt = config.root.dataset.observerId;
} else {
const nextId = ++_Observable.intersecId;
secConfig.rt = config.root.dataset.observerId = nextId.toString();
}
}
return JSON.stringify(secConfig);
}
getElementOffset(element) {
let left = 0;
let top = 0;
let parent = element;
do {
left += parent.offsetLeft || 0;
top += parent.offsetTop || 0;
parent = parent.offsetParent;
} while (parent);
return {
left,
top,
width: element.offsetWidth,
height: element.offsetHeight
};
}
};
export let Observable = _Observable;
Observable.items = /* @__PURE__ */ new Map();
Observable.intersecObservers = /* @__PURE__ */ new Map();
Observable.intersecId = 0;