UNPKG

@smoovy/observer

Version:
294 lines (293 loc) 8.82 kB
"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;