| |
| |
| |
| |
|
|
| |
| export interface ResizeHandlerOptions { |
| |
| rapidResizeThresholdMs?: number; |
| |
| rapidResizeCountThreshold?: number; |
| |
| resizeDebounceMs?: number; |
| |
| onPositionUpdate: () => void; |
| |
| getCurrentSvg: () => SVGSVGElement | undefined; |
| |
| onTransitionStart?: () => void; |
| } |
|
|
| export class ResizeHandler { |
| private resizeObserver?: ResizeObserver; |
| private baseNode: HTMLElement; |
| private options: Required<Omit<ResizeHandlerOptions, 'onPositionUpdate' | 'getCurrentSvg'>> & Pick<ResizeHandlerOptions, 'onPositionUpdate' | 'getCurrentSvg'>; |
| |
| |
| private lastResizeTime = 0; |
| private resizeEventCount = 0; |
| private resizeEndTimer?: number; |
| private positionUpdateTimer?: number; |
|
|
| constructor(baseNode: HTMLElement, options: ResizeHandlerOptions) { |
| this.baseNode = baseNode; |
| this.options = { |
| rapidResizeThresholdMs: options.rapidResizeThresholdMs ?? 100, |
| rapidResizeCountThreshold: options.rapidResizeCountThreshold ?? 3, |
| resizeDebounceMs: options.resizeDebounceMs ?? 100, |
| onPositionUpdate: options.onPositionUpdate, |
| getCurrentSvg: options.getCurrentSvg, |
| onTransitionStart: options.onTransitionStart, |
| }; |
| } |
|
|
| |
| |
| |
| setup(): void { |
| |
| if (this.resizeObserver) { |
| return; |
| } |
| |
| |
| this.resizeObserver = new ResizeObserver((entries) => { |
| const now = Date.now(); |
| const timeSinceLastResize = now - this.lastResizeTime; |
| |
| |
| const isRapidChange = timeSinceLastResize < this.options.rapidResizeThresholdMs; |
| |
| if (isRapidChange) { |
| |
| this.resizeEventCount++; |
| } else { |
| |
| this.resizeEventCount = 1; |
| } |
| |
| this.lastResizeTime = now; |
| |
| |
| const isInTransition = this.resizeEventCount >= this.options.rapidResizeCountThreshold; |
| |
| if (isInTransition) { |
| this.handleRapidResize(); |
| } else { |
| this.handleSingleResize(); |
| } |
| }); |
| |
| |
| this.resizeObserver.observe(this.baseNode); |
| } |
|
|
| |
| |
| |
| private handleRapidResize(): void { |
| const svg = this.options.getCurrentSvg(); |
|
|
| |
| if (svg && svg.style.opacity !== '0') { |
| svg.style.opacity = '0'; |
| svg.style.pointerEvents = 'none'; |
| } |
|
|
| |
| if (this.options.onTransitionStart) { |
| this.options.onTransitionStart(); |
| } |
|
|
| |
| if (this.positionUpdateTimer !== undefined) { |
| cancelAnimationFrame(this.positionUpdateTimer); |
| this.positionUpdateTimer = undefined; |
| } |
| |
| |
| if (this.resizeEndTimer !== undefined) { |
| clearTimeout(this.resizeEndTimer); |
| } |
| |
| |
| this.resizeEndTimer = window.setTimeout(() => { |
| this.resizeEventCount = 0; |
| |
| |
| this.options.onPositionUpdate(); |
| |
| |
| const svg = this.options.getCurrentSvg(); |
| if (svg) { |
| svg.style.opacity = '1'; |
| svg.style.pointerEvents = ''; |
| } |
| |
| this.resizeEndTimer = undefined; |
| }, this.options.resizeDebounceMs); |
| } |
|
|
| |
| |
| |
| private handleSingleResize(): void { |
| |
| |
| if (this.resizeEndTimer !== undefined) { |
| clearTimeout(this.resizeEndTimer); |
| this.resizeEndTimer = undefined; |
| } |
| |
| |
| if (this.positionUpdateTimer !== undefined) { |
| cancelAnimationFrame(this.positionUpdateTimer); |
| } |
| |
| |
| this.positionUpdateTimer = requestAnimationFrame(() => { |
| this.options.onPositionUpdate(); |
| this.positionUpdateTimer = undefined; |
| }); |
| } |
|
|
| |
| |
| |
| destroy(): void { |
| |
| if (this.resizeObserver) { |
| this.resizeObserver.disconnect(); |
| this.resizeObserver = undefined; |
| } |
| |
| |
| if (this.positionUpdateTimer !== undefined) { |
| cancelAnimationFrame(this.positionUpdateTimer); |
| this.positionUpdateTimer = undefined; |
| } |
| |
| |
| if (this.resizeEndTimer !== undefined) { |
| clearTimeout(this.resizeEndTimer); |
| this.resizeEndTimer = undefined; |
| } |
| } |
| } |
|
|
|
|