/** * trackEntireRead * @description Track the entire read of a truncated post * @param {HTMLElement} post - The post element */ class TrackEntireRead { private observer: IntersectionObserver | null = null private readElement: Element | null // data can be any key value pair that needs to be tracked private data: Record constructor(readElement: Element, data: Record) { this.readElement = readElement as Element this.data = data this.init() } private init() { //Intersection observer that observes readElement and when in view tracks the entire read this.observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { this.handleEntireRead() this.observer?.disconnect() } }) }) this.observer.observe(this.readElement as Element) } private handleEntireRead = () => { const trackingData = { action: 'entire_read', ...this.data, } const event = new CustomEvent('oTracking.event', { detail: trackingData, bubbles: true, }) document.body.dispatchEvent(event) } destroy(): void { this.observer?.disconnect() this.observer = null } } export class ExpanderClient { /** * The container element for all the elements that should be showned or be hidden. */ public container: HTMLElement | null /** * An optional element to which the expander will dispatch the events to. * If not provided, events will not be dispatched. */ private dispatchBoundary?: Element | Document | null /** * Element that will be showned at the end of the content when the expander is expanded. * We use this to track when the entire post is read. */ private endContent: Element | null /** * Track the entire read of a truncated post */ private trackingEntireRead: TrackEntireRead /** * The button that will expand the content */ private expanderButton: HTMLElement | null /** * The button that will collapse the content */ private collapserButton: HTMLElement | null /** * The tracking data that will be sent when the entire post is read */ private trackingData: Record /** * * @param el * @param dispatchBoundary */ constructor( el: HTMLElement, dispatchBoundary?: Element | Document | null, trackingData?: Record ) { this.container = el this.trackingData = trackingData || {} this.expanderButton = el.querySelector( '.cp-expander__expand [data-action="expand"]' ) as HTMLElement this.collapserButton = el.querySelector( '.cp-expander__collapse [data-action="collapse"]' ) as HTMLElement this.endContent = el.querySelector('.cp-expander__collapse') this.expanderButton?.addEventListener('click', this.handleExpand) this.collapserButton?.addEventListener('click', this.handleCollapse) this.container.classList.replace( 'cp-expander--not-initialised', 'cp-expander--initialised' ) this.dispatchBoundary = dispatchBoundary || this.container ;(this.trackingData['post_id'] = this.container .querySelector('[data-trackable-context-truncated-id]') ?.getAttribute('data-trackable-context-truncated-id') || ''), (this.trackingEntireRead = new TrackEntireRead( this.endContent as Element, this.trackingData )) } expand() { this.container?.setAttribute('data-state', 'expanded') this.dispatchBoundary?.dispatchEvent( new CustomEvent('expander:expanded', { bubbles: true, detail: { component: this, }, }) ) } collapse() { this.container?.setAttribute('data-state', 'collapsed') this.container?.parentElement?.parentElement?.scrollIntoView() this.dispatchBoundary?.dispatchEvent( new CustomEvent('expander:collapsed', { bubbles: true, detail: { component: this, }, }) ) } private handleExpand = (e: Event) => { e.preventDefault() this.expand() this.expanderButton?.setAttribute('aria-expanded', 'true') this.expanderButton?.setAttribute('aria-hidden', 'true') this.collapserButton?.setAttribute('aria-expanded', 'true') this.collapserButton?.setAttribute('aria-hidden', 'false') } private handleCollapse = (e: Event) => { e.preventDefault() this.collapse() this.expanderButton?.setAttribute('aria-expanded', 'false') this.expanderButton?.setAttribute('aria-hidden', 'false') this.collapserButton?.setAttribute('aria-expanded', 'false') this.collapserButton?.setAttribute('aria-hidden', 'true') } destroy(): void { this.container?.setAttribute('data-state', 'collapsed') this.container ?.querySelector('.cp-expander__expand') ?.removeEventListener('click', this.expand) this.container ?.querySelector('.cp-expander__collapse') ?.removeEventListener('click', this.collapse) this.container?.classList.remove('cp-expander--initialised') this.trackingEntireRead.destroy() } static init(data?: { rootElement?: HTMLElement | null dispatchBoundary?: Element | Document | null trackingData?: Record }): ExpanderClient[] { const { rootElement, dispatchBoundary, trackingData } = data || {} const root = rootElement || document.body return (Array.from( root.querySelectorAll( ':not(.cp-expander--initialised)[data-component="expander"]' ) ).map((el: Element) => { if (!el.classList.contains('expander--initialised')) { return new ExpanderClient( el as HTMLElement, dispatchBoundary, trackingData ) } }) || []) as ExpanderClient[] } } export default ExpanderClient