interface Component { name: string id: string | null timeElapsedSeconds?: number } class TopperTracker { private startTime: number private totalVisibleTime: number private timeElapsedSeconds: number | null private type: string private component: Element | null private id: string | null private observer: IntersectionObserver | null constructor({ type = 'flourish-topper' } = {}) { this.startTime = 0 this.totalVisibleTime = 0 this.timeElapsedSeconds = null this.type = type this.component = document.querySelector( `[data-component-type="${this.type}"]` ) this.id = this.component?.getAttribute('data-component-id') || null this.observer = null } init(): void { if (!window.IntersectionObserver || !this.component) { return } this.dispatchEvent('mount') this.observer = new IntersectionObserver(this.onChange.bind(this), { threshold: [0.75], }) this.observer.observe(this.component) } private dispatchEvent(action: string): void { const component: Component = { name: this.type, id: this.id, } if (this.timeElapsedSeconds) { component.timeElapsedSeconds = this.timeElapsedSeconds } const event = new CustomEvent('oTracking.event', { detail: { category: 'component', action: action, component, }, bubbles: true, }) document.body.dispatchEvent(event) } private onChange(changes: IntersectionObserverEntry[]): void { changes.forEach((change) => { if (change.target !== this.component) { return } if (change.isIntersecting || change.intersectionRatio >= 1) { this.dispatchEvent('view') this.startTime = performance.now() } if (!change.isIntersecting || change.intersectionRatio === 0) { this.totalVisibleTime = performance.now() - this.startTime this.timeElapsedSeconds = parseFloat( (this.totalVisibleTime / 1000).toFixed(2) ) this.dispatchEvent('stop-view') this.totalVisibleTime = 0 this.timeElapsedSeconds = null } }) } disconnect(): void { if (this.observer && this.component) { this.observer.unobserve(this.component) this.observer.disconnect() } } } export { TopperTracker }