import { JSUtils } from "../JSUtils.js"; import { OverflowDirection } from "./types/overflow-direction.js" import { OnOverflowCallBack } from "./types/overflow-callback.js" import { calcMarginSize } from '../ElementUtils.js'; export * from "./types/overflow-direction.js"; export * from "./types/overflow-callback.js"; export const OVERFLOWED_CLASS_NAME = 'overflowed'; export default class OverflowWatcher { private _onResize:OnOverflowCallBack; private _resizeObserver:ResizeObserver; private _lastContainerSize:number|undefined = undefined; private _lastContainerInstance: HTMLElement | undefined = undefined; private _scrollDirection = OverflowDirection.HORIZONTAL; private _propSize:string; private _hiddenItemsProps:Map = new Map(); private _notOverFlowPros:Map = new Map(); private _deltaSize:number; private _notOverFlow: string[] = []; readonly DATA_ELEMENT_ID = 'data-element-id'; /** * Cria uma instancia do OverflowWatcher * * @param element - Elemento HTML que o overflow será observado. * @param callback - Função que sera chamada quando ocorrer overflow no elemento. * @param overFlowDirection - Indica direção que o overflow será monitorado. * @param deltaSize - Variação de tamanho que será considerada como overflow. * @param debounce - Tempo até execução do callback em milissegundos. * @param notOverFlow - Lista de ids ou data-element-ids dos elementos que não devem sofrer overFlow. */ constructor({ element, callback, overFlowDirection = OverflowDirection.HORIZONTAL, debounce = 200, deltaSize = 0, notOverFlow = [] }: OverFlowWatcherParams){ this._onResize = callback; this._scrollDirection = overFlowDirection; this._propSize = (OverflowDirection.HORIZONTAL === overFlowDirection) ? "width" : "height"; this._resizeObserver = new ResizeObserver(JSUtils.debounce((entries: ResizeObserverEntry[]) => this.handleResize(entries), debounce)); this._resizeObserver.observe(element); this._deltaSize = deltaSize; this._notOverFlow = notOverFlow; } public addNotOverFlowElement(elementId: string){ if(!this._notOverFlow.includes(elementId)){ this._notOverFlow.push(elementId); } } public destroy(){ this._resizeObserver.disconnect(); } public forceUpdate(){ if(this._lastContainerSize && this._lastContainerInstance){ this.updateOverFlowedItems(this._lastContainerInstance, this._lastContainerSize); } } private handleResize(entries: ResizeObserverEntry[]){ if(!entries || entries.length === 0) return; const container = entries[0]; const containerSize:number = (container.contentRect as any)[this._propSize]; if(!containerSize) return; if(this.hasChangedSize(containerSize)){ this.updateOverFlowedItems(container.target as HTMLElement, containerSize); } } private updateOverFlowedItems(container: HTMLElement, containerSize: number){ const children:Element[] = Array.from(container.children); this.registerNotOverflowProps(children); this.proccessElements(containerSize, children); this._lastContainerSize = containerSize; this._lastContainerInstance = container; } private registerNotOverflowProps(children: Element[]) { children.forEach(childElement => { const id = childElement.id || this.getDataElementId(childElement); if (this.canNotRegisterNotOverFlow(id)) return; this._notOverFlowPros.set(id, this.getElementSizeProps(childElement)); }); } private canNotRegisterNotOverFlow(id: string) { return !id || !this._notOverFlow.includes(id); } private hasChangedSize(elementSize: number):boolean{ if(!this._lastContainerSize) return true; const variation = elementSize - this._lastContainerSize; if(variation < 0){ const absoluteVariation = Math.abs(variation); return (absoluteVariation > this._deltaSize); } return variation > 0; } private proccessElements(elementSize:number, children:Element[]){ if(children.length === 0) return; const childrenSize = this.calcChildrenSize(children); let diff = Number((elementSize - childrenSize).toFixed(4)); if(diff > 0){ this.clearOverFlow(); return; } this.proccessElementsOverFlow(children, elementSize); } private clearOverFlow(){ this._hiddenItemsProps = new Map(); this._onResize([]); } private proccessElementsOverFlow(allElements:Element[], avaliableSize:number){ const elementsThatFit: Element[] = []; const avaliableSizeConsideringDelta = (avaliableSize - this._deltaSize); let sumElementsSize = 0; for (const element of allElements) { sumElementsSize += this.calcElementSize(element); if(this.exceedsAvaliableSize(sumElementsSize, elementsThatFit, avaliableSizeConsideringDelta)) break; elementsThatFit.push(element); } const overFlowedElements = allElements.filter(element => this.isElementOverFlowing(elementsThatFit, element)); overFlowedElements.forEach(overFlowed => { if(!this._hiddenItemsProps.has(overFlowed)){ this.registerElementSize(overFlowed); } }); this._onResize(overFlowedElements); } private isElementOverFlowing(elementsThatFit: Element[], element: Element) { return !elementsThatFit.includes(element) && this.canOverFlowElement(element); } private canOverFlowElement(element: Element) { return !this._notOverFlow.includes(element.id) && !this._notOverFlow.includes(this.getDataElementId(element)); } private getDataElementId(element: Element): string { return (element as HTMLElement).getAttribute('data-element-id') ?? ""; } private exceedsAvaliableSize(sumElementsSize: number, elements: Element[], avaliableSize: number): boolean { if(!this._notOverFlow.length) return sumElementsSize > avaliableSize const elementIdsToCalculate = this.canNotOverFlowNotIncludedIds(elements); if(!elementIdsToCalculate.length) return sumElementsSize > avaliableSize const variation = this.calculateVariation(elementIdsToCalculate); const occupiedSize = sumElementsSize + variation; return occupiedSize > avaliableSize; } private calculateVariation(elementIdsToCalculate: string[]) { let variation = 0 elementIdsToCalculate.forEach(id => { const sizeProps = this._notOverFlowPros.get(id); variation += sizeProps?.size ?? 0; variation += sizeProps?.margin ?? 0; }); return variation; } private canNotOverFlowNotIncludedIds(elements: Element[]): string[]{ const elementsIdList = elements.map(el => el.id || this.getDataElementId(el)).filter(id => !!id); return this._notOverFlow.filter(id => !elementsIdList.includes(id)); } private registerElementSize(element: Element) { const sizeProps = this.getElementSizeProps(element); this._hiddenItemsProps.set(element, sizeProps); } private getElementSizeProps(element: Element) { const sizeProps: SizeProps = { size: (element.getBoundingClientRect() as any)[this._propSize], margin: calcMarginSize(element, this._scrollDirection), }; return sizeProps; } private calcChildrenSize(children:Element[]):number{ let sumChildren = 0; sumChildren += this._deltaSize; Array.from(children).forEach(el => sumChildren += this.calcElementSize(el)); return sumChildren; } private calcElementSize(el: Element) { let size = 0 if (this.isOverFlowed(el)) { const sizeProps = this._hiddenItemsProps.get(el); size += sizeProps?.size ?? 0; size += sizeProps?.margin ?? 0; return size; } size += (el.getBoundingClientRect() as any)[this._propSize]; size += calcMarginSize(el, this._scrollDirection); return size; } private isOverFlowed(el: Element) { return el.classList.contains(OVERFLOWED_CLASS_NAME); } } export interface OverFlowWatcherParams { element:HTMLElement, callback:OnOverflowCallBack, overFlowDirection?:OverflowDirection, deltaSize?:number, debounce?: number, notOverFlow?: string[] } interface SizeProps { size: number, margin: number, }