@egjs/grid
Version:
A component that can arrange items according to the type of grids
233 lines (208 loc) • 6.63 kB
text/typescript
import Component from "@egjs/component";
import { SizeRect } from "./types";
import { isString } from "./utils";
export interface ResizeWatherOptions {
resizeDebounce?: number;
maxResizeDebounce?: number;
useResizeObserver?: boolean;
useWindowResize?: boolean;
watchDirection?: "width" | "height" | "box" | false;
rectBox?: "border-box" | "content-box";
childrenRectBox?: "border-box" | "content-box";
}
export interface ResizeWatcherResizeEvent {
isResizeContainer: boolean;
childEntries: ResizeWatcherEntry[];
}
export interface ResizeWatcherEntry {
target: Element;
size?: { inlineSize: number, blockSize: number };
}
export class ResizeWatcher {
private _resizeTimer = 0;
private _maxResizeDebounceTimer = 0;
private _emitter: Component<{ resize: ResizeWatcherResizeEvent }>;
private _observer: ResizeObserver | null;
protected container: HTMLElement;
protected rect: SizeRect = { width: 0, height: 0 };
private _options!: Required<ResizeWatherOptions>;
private _updatedEntries: ResizeWatcherEntry[] = [];
constructor(container: HTMLElement | string, options: ResizeWatherOptions = {}) {
this._options = {
resizeDebounce: 100,
maxResizeDebounce: 0,
useResizeObserver: false,
useWindowResize: true,
watchDirection: false,
rectBox: "content-box",
childrenRectBox: "border-box",
...options,
};
this.container = isString(container) ? document.querySelector<HTMLElement>(container)! : container;
this._init();
}
public getRect() {
return this.rect;
}
public setRect(rect: SizeRect) {
this.rect = { ...rect };
}
public isObserverEnabled() {
return !!this._observer;
}
public resize() {
const container = this.container;
this.setRect(this._options.rectBox === "border-box" ? {
width: container.offsetWidth,
height: container.offsetHeight,
} : {
width: container.clientWidth,
height: container.clientHeight,
});
}
public observeChildren(children: Element[]) {
const observer = this._observer;
if (!observer) {
return;
}
const box = this._options.childrenRectBox;
children.forEach((element) => {
if (element) {
observer.observe(element, {
box,
});
}
});
}
public unobserveChildren(children: Element[]) {
const observer = this._observer;
if (!observer) {
return;
}
children.forEach((element) => {
if (element) {
observer.unobserve(element);
}
});
}
public listen(callback: (e: ResizeWatcherResizeEvent) => void) {
this._emitter.on("resize", callback);
return this;
}
public destroy() {
this._observer?.disconnect();
if (this._options.useWindowResize) {
window.removeEventListener("resize", this._onWindowResize);
}
}
private _init() {
const container = this.container;
const options = this._options;
this._emitter = new Component();
if (options.useResizeObserver && !!window.ResizeObserver) {
this._observer = new window.ResizeObserver(this._onObserve);
this._observer.observe(container, {
box: options.rectBox,
});
}
if (options.useWindowResize) {
window.addEventListener("resize", this._onWindowResize);
}
this.resize();
}
private _onWindowResize = () => {
this._scheduleResize([{
target: this.container,
}]);
}
private _onObserve = (entries: ResizeObserverEntry[]) => {
const options = this._options;
const container = this.container;
const containerRectBox = options.rectBox;
const childrenRectBox = options.childrenRectBox;
this._scheduleResize(entries.map((entry) => {
const target = entry.target;
const rectBox = target === container ? containerRectBox : childrenRectBox;
let sizes = (rectBox === "border-box" ? entry.borderBoxSize : entry.contentBoxSize);
// Safari < 15.3
if (!sizes) {
const contentRect = entry.contentRect;
sizes = [{
inlineSize: contentRect.width,
blockSize: contentRect.height,
}];
}
return {
// not array in old browser
size: sizes[0] || sizes as any,
target: entry.target,
};
}));
}
private _scheduleResize = (entries: ResizeWatcherEntry[]) => {
const {
resizeDebounce,
maxResizeDebounce,
} = this._options;
const updatedEntries = this._updatedEntries;
updatedEntries.push(...entries);
this._updatedEntries = updatedEntries.filter((entry, index) => {
return updatedEntries.lastIndexOf(entry) === index;
});
if (!this._maxResizeDebounceTimer && maxResizeDebounce >= resizeDebounce) {
this._maxResizeDebounceTimer = window.setTimeout(this._onResize, maxResizeDebounce);
}
if (this._resizeTimer) {
clearTimeout(this._resizeTimer);
this._resizeTimer = 0;
}
this._resizeTimer = window.setTimeout(this._onResize, resizeDebounce);
}
private _onResize = () => {
clearTimeout(this._resizeTimer);
clearTimeout(this._maxResizeDebounceTimer);
this._maxResizeDebounceTimer = 0;
this._resizeTimer = 0;
const updated = this._updatedEntries;
const container = this.container;
let containerEntry!: ResizeWatcherEntry;
const childEntries = updated.filter((entry) => {
if (entry.target === container) {
containerEntry = entry;
return false;
} else {
return true;
}
});
const isResizeChildren = childEntries.length > 0;
let isResizeContainer = !!containerEntry;
if (isResizeContainer) {
const watchDirection = this._options.watchDirection;
const prevRect = this.rect;
const containerEntrySize = containerEntry.size;
if (containerEntrySize) {
// ResizeObserver
this.setRect({
width: containerEntrySize.inlineSize,
height: containerEntrySize.blockSize,
});
} else {
// window's resize event
this.resize();
}
const rect = this.rect;
const isWatchWidth = watchDirection === "box" || watchDirection === "width";
const isWatchHeight = watchDirection === "box" || watchDirection === "height";
isResizeContainer = !watchDirection
|| (isWatchWidth && prevRect.width !== rect.width)
|| (isWatchHeight && prevRect.height !== rect.height);
}
this._updatedEntries = [];
if (isResizeContainer || isResizeChildren) {
this._emitter.trigger("resize", {
isResizeContainer,
childEntries,
});
}
}
}