import { CarouselCardTrackingClient } from '../../CarouselCard/client/tracking' /** * CarouselTracker * @description Track the Carousel component * @param {HTMLElement} mountElement - The Carousel component */ class CarouselTrackingClient { private intersectionObserver: IntersectionObserver private readonly onTouchStartHandler: (event: TouchEvent) => void private readonly onTouchEndHandler: (event: TouchEvent) => void private readonly onMouseDownHandler: (event: MouseEvent) => void private readonly onMouseUpHandler: (event: MouseEvent) => void private readonly onScrollHandler: () => void private readonly onWheelHandler: (event: WheelEvent) => void component: HTMLElement cardTrackers: CarouselCardTrackingClient[] viewing: boolean timeElapsed: number startTime: null | number stopTime: null | number touchStartX: number | null touchStartY: number | null mouseStartX: number | null mouseStartY: number | null previousScrollLeft: number id: string | undefined constructor(component: HTMLElement) { this.touchStartX = null this.touchStartY = null this.mouseStartX = null this.mouseStartY = null this.component = component this.id = component.getAttribute('data-component-id') || undefined this.cardTrackers = CarouselCardTrackingClient.init(this.component) this.onTouchStartHandler = this.onTouchStart.bind(this) this.onTouchEndHandler = this.onTouchEnd.bind(this) this.onMouseDownHandler = this.onMouseDown.bind(this) this.onMouseUpHandler = this.onMouseUp.bind(this) this.onScrollHandler = this.onScroll.bind(this) this.onWheelHandler = this.onWheel.bind(this) this.previousScrollLeft = this.component.scrollLeft this.component.addEventListener('touchstart', this.onTouchStartHandler, { passive: true, }) this.component.addEventListener('touchend', this.onTouchEndHandler, { passive: true, }) this.component.addEventListener('mousedown', this.onMouseDownHandler, { passive: true, }) this.component.addEventListener('mouseup', this.onMouseUpHandler, { passive: true, }) window.addEventListener('mouseup', this.onMouseUpHandler, { passive: true, }) this.component.addEventListener('scroll', this.onScrollHandler, { passive: true, }) this.component.addEventListener('wheel', this.onWheelHandler, { passive: true, }) this.viewing = false this.intersectionObserver = new IntersectionObserver( this.onChange.bind(this), // adjust bottom margin to trigger event before card is fully in vertical view { rootMargin: `0px 0px -300px 0px`, threshold: 0, } ) this.intersectionObserver.observe(this.component) this.timeElapsed = 0 this.startTime = null this.stopTime = null this.destroy = this.destroy.bind(this) this.component.dataset.initialised = 'true' } dispatchEvent(value: Record = {}): void { const eventDetail = new CustomEvent('oTracking.event', { detail: { category: 'component', ...value, component: { id: this.id, name: 'cp-carousel', type: 'container', }, }, bubbles: true, }) document.body.dispatchEvent(eventDetail) } onView(time = performance.now()): void { const trackingData = { action: 'view', trigger_action: 'enter-viewport', url: window.location.href, } this.dispatchEvent(trackingData) this.viewing = true this.startTime = time } onChange(changes: IntersectionObserverEntry[]): void { changes.forEach((change) => { if (change.target !== this.component) { return } if (this.isInViewport(change)) { this.onView(change.time) } if (this.isOutOfViewport(change)) { this.onExitView(change.time) } }) } private onExitView(time = performance.now()): void { this.viewing = false if (this.startTime !== null) { this.timeElapsed += time - this.startTime this.startTime = null } const timeElapsedSeconds = parseFloat((this.timeElapsed / 1000).toFixed(2)) const trackingData = { action: 'view', trigger_action: 'leave-viewport', url: window.location.href, custom: [ { name: 'time-elapsed', value: timeElapsedSeconds, }, ], } this.dispatchEvent(trackingData) this.intersectionObserver?.unobserve(this.component) this.intersectionObserver?.disconnect() } private isInViewport(change: IntersectionObserverEntry): boolean { return ( this.viewing === false && change.boundingClientRect.height > 0 && (change.isIntersecting || change.intersectionRatio >= 1) ) } private isOutOfViewport(change: IntersectionObserverEntry): boolean { return ( this.viewing && (!change.isIntersecting || change.intersectionRatio === 0) ) } onTouchStart(e: TouchEvent): void { this.touchStartX = e.changedTouches[0]?.clientX || 0 this.touchStartY = e.changedTouches[0]?.clientY || 0 } onTouchEnd(e: TouchEvent): void { if (this.touchStartX === null || this.touchStartY === null) return const x = e.changedTouches[0]?.clientX || 0 const y = e.changedTouches[0]?.clientY || 0 if (this.touchStartX === x && this.touchStartY === y) return const xDiff = this.touchStartX - x const yDiff = this.touchStartY - y if (Math.abs(xDiff) > 30 && Math.abs(xDiff) > Math.abs(yDiff)) { this.onSwipe() this.removeSwipeListeners() } } private removeSwipeListeners(): void { this.component.removeEventListener('touchstart', this.onTouchStartHandler) this.component.removeEventListener('touchend', this.onTouchEndHandler) this.component.removeEventListener('mousedown', this.onMouseDownHandler) this.component.removeEventListener('mouseup', this.onMouseUpHandler) window.removeEventListener('mouseup', this.onMouseUpHandler) this.component.removeEventListener('scroll', this.onScrollHandler) this.component.removeEventListener('wheel', this.onWheelHandler) } private onMouseDown(event: MouseEvent): void { this.mouseStartX = event.clientX this.mouseStartY = event.clientY } private onMouseUp(event: MouseEvent): void { if (this.mouseStartX === null || this.mouseStartY === null) return const xDiff = this.mouseStartX - event.clientX const yDiff = this.mouseStartY - event.clientY this.mouseStartX = null this.mouseStartY = null if (Math.abs(xDiff) > 15 && Math.abs(xDiff) > Math.abs(yDiff)) { this.onSwipe() this.removeSwipeListeners() } } private onScroll(): void { const currentScrollLeft = this.component.scrollLeft const deltaX = currentScrollLeft - this.previousScrollLeft // Ignore tiny horizontal adjustments (e.g. scroll-snap/layout drift) if (Math.abs(deltaX) <= 30) { this.previousScrollLeft = currentScrollLeft return } this.previousScrollLeft = currentScrollLeft this.onSwipe() this.removeSwipeListeners() } private onWheel(event: WheelEvent): void { const isHorizontalWheel = Math.abs(event.deltaX) > Math.abs(event.deltaY) || (event.shiftKey && Math.abs(event.deltaY) > 0) if (!isHorizontalWheel) { return } this.onSwipe() this.removeSwipeListeners() } onSwipe(): void { const trackingData = { action: 'act', trigger_action: 'user-swipe', url: window.location.href, } this.dispatchEvent(trackingData) } destroy(): void { if (!this.component) return this.viewing && this.onExitView() this.removeSwipeListeners() this.intersectionObserver?.unobserve(this.component) this.intersectionObserver?.disconnect() this.component.removeAttribute('data-initialised') if (this.cardTrackers.length > 0) { this.cardTrackers.forEach((tracker) => tracker.destroy()) } } } export { CarouselTrackingClient }