@v4fire/client
Version:
V4Fire client core library
316 lines (258 loc) • 6.71 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
import symbolGenerator from 'core/symbol';
import Async from 'core/async';
import type {
ResizeWatcherObserverOptions,
ResizeWatcherObservable,
ResizeWatcherInitOptions,
ResizeWatcherObservableElStore
} from 'core/dom/resize-observer/interface';
import {
RESIZE_WATCHER_OBSERVABLE_STORE,
RESIZE_WATCHER_ASYNC_GROUP
} from 'core/dom/resize-observer/const';
export * from 'core/dom/resize-observer/interface';
export * from 'core/dom/resize-observer/const';
export const
$$ = symbolGenerator();
export default class ResizeWatcher {
/**
* True if the environment supports ResizeObserver
*/
get isResizeObserverSupported(): boolean {
return 'ResizeObserver' in globalThis;
}
/**
* Async instance
*/
// eslint-disable-next-line @typescript-eslint/no-invalid-this
protected async: Async<this> = new Async(this);
/**
* Starts to observe resizing of the specified element
*
* @param el
* @param options
*/
observe(el: Element, options: ResizeWatcherInitOptions): Nullable<ResizeWatcherObservable> {
options = this.normalizeOptions(options);
if (!this.isResizeObserverSupported) {
return null;
}
if (this.isAlreadyBound(el, options)) {
this.unobserve(el, options);
}
const
observable = this.createObservable(el, options);
this.saveObservableToElStore(observable);
this.createResizeObserver(observable);
return observable;
}
/**
* Stops to observe resizing of the specified element
*
* @param el
* @param options
*/
unobserve(el: Element, options: ResizeWatcherInitOptions): boolean {
const
store = this.getObservableElStore(el),
callback = Object.isFunction(options) ? options : options.callback;
if (!store) {
return false;
}
const
observable = store.get(callback);
if (!observable) {
return false;
}
store.delete(callback);
observable.observer?.disconnect();
const
{ctx, id} = observable;
if (ctx) {
ctx.unsafe.$async.clearAll({
group: RESIZE_WATCHER_ASYNC_GROUP,
label: id
});
}
return true;
}
/**
* Removes all resize watchers from the specified element
* @param el
*/
clear(el: Element): void {
const
store = this.getObservableElStore(el);
if (store == null) {
return;
}
store.forEach((observable) => observable.destructor());
store.clear();
}
/**
* Returns `true` if the specified element with the specified callback is already being observed
*
* @param el
* @param options
*/
isAlreadyBound(el: Element, options: ResizeWatcherInitOptions): boolean {
const
store = this.getObservableElStore(el),
callback = Object.isFunction(options) ? options : options.callback;
if (store == null) {
return false;
}
return store.has(callback);
}
/**
* Stores an observable to the observable element store
* @param observable
*/
saveObservableToElStore(observable: ResizeWatcherObservable): void {
this.getOrCreateObservableElStore(observable.node).set(observable.callback, observable);
}
/**
* Returns an observable store from the specified element
* @param el
*/
getObservableElStore(el: Element): CanUndef<ResizeWatcherObservableElStore> {
return el[RESIZE_WATCHER_OBSERVABLE_STORE];
}
/**
* Returns an observables store from the specified element; if it does not exist, it will be created and returned
* @param el
*/
getOrCreateObservableElStore(el: Element): ResizeWatcherObservableElStore {
return this.getObservableElStore(el) ?? (el[RESIZE_WATCHER_OBSERVABLE_STORE] = new Map());
}
/**
* Returns normalized observable options
* @param options
*/
normalizeOptions(options: ResizeWatcherInitOptions): ResizeWatcherObserverOptions {
const
callback = Object.isFunction(options) ? options : options.callback;
return {
watchHeight: true,
watchWidth: true,
initial: true,
immediate: false,
once: false,
...options,
callback
};
}
/**
* Creates a new observable element
*
* @param el
* @param options
*/
protected createObservable(el: Element, options: ResizeWatcherObserverOptions): ResizeWatcherObservable {
return {
node: el,
id: String(Math.random()),
destructor: () => this.unobserve(el, options),
...options
};
}
/**
* Creates an instance of ResizeObserver
* @param observable
*/
protected createResizeObserver(observable: ResizeWatcherObservable): void {
observable.observer = new ResizeObserver(([{contentRect}]) => {
if (observable.rect === undefined) {
this.setInitialSize(observable, contentRect);
return;
}
this.onElementResize(observable, contentRect);
});
observable.observer.observe(observable.node);
}
/**
* Sets an initial size of the specified observable
*
* @param observable
* @param newRect
*/
protected setInitialSize(observable: ResizeWatcherObservable, newRect: DOMRectReadOnly): void {
observable.rect = newRect;
if (observable.initial) {
observable.callback(<Required<ResizeWatcherObservable>>observable, newRect);
}
}
/**
* Returns true if the observable callback should be executed
*
* @param observable
* @param newRect
* @param oldRect
*/
protected shouldInvokeCallback(
observable: ResizeWatcherObservable,
newRect: DOMRectReadOnly,
oldRect: DOMRectReadOnly
): boolean {
const {
watchWidth,
watchHeight
} = observable;
const {
width: oldWidth,
height: oldHeight
} = oldRect;
const {
width: newWidth,
height: newHeight
} = newRect;
let
res = false;
if (watchWidth) {
res = oldWidth !== newWidth;
}
if (watchHeight && !res) {
res = oldHeight !== newHeight;
}
return res;
}
/**
* Handler: element has been resized
*
* @param observable
* @param newRect
*/
protected onElementResize(observable: ResizeWatcherObservable, newRect: DOMRectReadOnly): void {
const oldRect = observable.rect!;
if (this.shouldInvokeCallback(observable, newRect, oldRect)) {
const cb = () => {
observable.callback(<Required<ResizeWatcherObservable>>observable, newRect, oldRect);
if (observable.once) {
observable.destructor();
}
};
if (observable.immediate) {
cb();
} else {
const $a = observable.ctx?.unsafe.$async ?? this.async;
// @ts-ignore (???)
$a.requestIdleCallback(cb, {
timeout: 50,
group: RESIZE_WATCHER_ASYNC_GROUP,
label: observable.id,
join: false
});
}
}
observable.rect = newRect;
}
}
const resizeWatcherInstance = new ResizeWatcher();
export { resizeWatcherInstance as ResizeWatcher };