import Notifications , { CallbackType } from './notification' ; import Registry from './registry' ; export interface IOptions { root ? : HTMLElement ; rootMargin ? : string ; threshold ? : number ; [ key ] : any ; } type StateForRoot = { elements : [ HTMLElement ] ; options : IOptions ; intersectionObserver : any ; } ; type PotentialRootEntry = { [ stringifiedOptions ] : StateForRoot ; } ; export default class IntersectionObserverAdmin extends Notifications { private elementRegistry : Registry ; constructor ( ) { super ( ) ; this . elementRegistry = new Registry ( ) ; } method param { HTMLElement | Window } element param { Object } options public public observe ( element , options = { } ) { if ( ! element ) { return ; } this . elementRegistry . addElement ( element , { ... options } ) ; this . setupObserver ( element , { ... options } ) ; } method param { HTMLElement | Window } target param { Object } options public public unobserve ( target , options ) { const matchingRootEntry = this . findMatchingRootEntry ( options ) ; if ( matchingRootEntry ) { const { intersectionObserver } = matchingRootEntry ; intersectionObserver . unobserve ( target ) ; } } method public public addEnterCallback ( element , callback ) { this . addCallback ( CallbackType . enter , element , callback ) ; } method public public addExitCallback ( element , callback ) { this . addCallback ( CallbackType . exit , element , callback ) ; } method public public dispatchEnterCallback ( element , entry ) { this . dispatchCallback ( CallbackType . enter , element , entry ) ; } method public public dispatchExitCallback ( element , entry ) { this . dispatchCallback ( CallbackType . exit , element , entry ) ; } method public public destroy ( ) { this . elementRegistry . destroyRegistry ( ) ; } method param { Object } options protected setupOnIntersection ( options ) { return ( ioEntries ) => { return this . onIntersection ( options , ioEntries ) ; } ; } protected setupObserver ( element , options ) { const { root = window } = options ; const potentialRootMatch = this . findRootFromRegistry ( root ) ; let matchingEntryForRoot ; if ( potentialRootMatch ) { matchingEntryForRoot = this . determineMatchingElements ( options , potentialRootMatch ) ; } if ( matchingEntryForRoot ) { const { elements , intersectionObserver } = matchingEntryForRoot ; elements . push ( element ) ; if ( intersectionObserver ) { intersectionObserver . observe ( element ) ; } } else { const intersectionObserver = this . newObserver ( element , options ) ; const observerEntry = { elements : [ element ] , intersectionObserver , options } ; const stringifiedOptions = this . stringifyOptions ( options ) ; if ( potentialRootMatch ) { potentialRootMatch [ stringifiedOptions ] = observerEntry ; } else { this . elementRegistry . addElement ( root , { [ stringifiedOptions ] : observerEntry } ) ; } } } private newObserver ( element , options ) { const { root , rootMargin , threshold } = options ; const newIO = new IntersectionObserver ( this . setupOnIntersection ( options ) . bind ( this ) , { root , rootMargin , threshold } ) ; newIO . observe ( element ) ; return newIO ; } method param { Object } options param { Array } ioEntries private private onIntersection ( options , ioEntries ) { ioEntries . forEach ( entry => { const { isIntersecting , intersectionRatio } = entry ; let threshold = options . threshold || 0 ; if ( Array . isArray ( threshold ) ) { threshold = threshold [ threshold . length - 1 ] ; } const matchingRootEntry = this . findMatchingRootEntry ( options ) ; if ( isIntersecting || intersectionRatio > threshold ) { if ( matchingRootEntry ) { matchingRootEntry . elements . some ( ( element ) => { if ( element && element === entry . target ) { this . dispatchEnterCallback ( element , entry ) ; return true ; } return false ; } ) ; } } else { if ( matchingRootEntry ) { matchingRootEntry . elements . some ( ( element ) => { if ( element && element === entry . target ) { this . dispatchExitCallback ( element , entry ) ; return true ; } return false ; } ) ; } } } ) ; } method param { HTMLElement | Window } root private return { Object } private findRootFromRegistry ( root ) { if ( this . elementRegistry ) { return this . elementRegistry . getElement ( root ) ; } } method param { Object } options return { Object } private findMatchingRootEntry ( options ) { const { root = window } = options ; const matchingRoot = this . findRootFromRegistry ( root ) ; if ( matchingRoot ) { const stringifiedOptions = this . stringifyOptions ( options ) ; return matchingRoot [ stringifiedOptions ] ; } } method param { Object } options param { Object } potentialRootMatch private return { Object } private determineMatchingElements ( options , potentialRootMatch ) { const matchingStringifiedOptions = Object . keys ( potentialRootMatch ) . filter ( key => { const { options : comparableOptions } = potentialRootMatch [ key ] ; return this . areOptionsSame ( options , comparableOptions ) ; } ) [ 0 ] ; return potentialRootMatch [ matchingStringifiedOptions ] ; } method param { any } a param { any } b private return { boolean } private areOptionsSame ( a , b ) { if ( a === b ) { return true ; } const type1 = Object . prototype . toString . call ( a ) ; const type2 = Object . prototype . toString . call ( b ) ; if ( type1 !== type2 ) { return false ; } else if ( type1 !== '[object Object]' && type2 !== '[object Object]' ) { return a === b ; } if ( a && b && typeof a === 'object' && typeof b === 'object' ) { for ( const key in a ) { if ( Object . prototype . hasOwnProperty . call ( a , key ) ) { if ( this . areOptionsSame ( a [ key ] , b [ key ] ) === false ) { return false ; } } } } return true ; } param { Object } options private return { String } private stringifyOptions ( options ) { const { root } = options ; const replacer = ( key , value ) : string => { if ( key === 'root' && root ) { const classList = Array . prototype . slice . call ( root . classList ) ; const classToken = classList . reduce ( ( acc , item ) => { return ( acc += item ) ; } , '' ) ; const id = root . id ; return `${ id }-${ classToken }` ; } return value ; } ; return JSON . stringify ( options , replacer ) ; } }