import { INode , MaskInputOptions , SlimDOMOptions , maskInputValue , MaskInputFn , MaskTextFn , } from 'rrweb-snapshot' ; import { FontFaceDescriptors , FontFaceSet } from 'css-font-loading-module' ; import { throttle , on , hookSetter , getWindowHeight , getWindowWidth , isBlocked , isTouchEvent , patch , } from '../utils' ; import { mutationCallBack , observerParam , mousemoveCallBack , mousePosition , mouseInteractionCallBack , MouseInteractions , listenerHandler , scrollCallback , styleSheetRuleCallback , viewportResizeCallback , inputValue , inputCallback , hookResetter , blockClass , maskTextClass , IncrementalSource , hooksParam , Arguments , mediaInteractionCallback , MediaInteractions , SamplingStrategy , canvasMutationCallback , fontCallback , fontParam , Mirror , styleDeclarationCallback , IWindow , } from '../types' ; import MutationBuffer from './mutation' ; import { IframeManager } from './iframe-manager' ; import { ShadowDomManager } from './shadow-dom-manager' ; type WindowWithStoredMutationObserver = IWindow & { __rrMutationObserver ? : MutationObserver ; } ; type WindowWithAngularZone = IWindow & { Zone ? : { __symbol__ ? : ( key : string ) => string ; } ; } ; export const mutationBuffers = [ ] ; const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined' ; const isCSSMediaRuleSupported = typeof CSSMediaRule !== 'undefined' ; const isCSSSupportsRuleSupported = typeof CSSSupportsRule !== 'undefined' ; const isCSSConditionRuleSupported = typeof CSSConditionRule !== 'undefined' ; function getEventTarget ( event ) { try { if ( 'composedPath' in event ) { const path = event . composedPath ( ) ; if ( path . length ) { return path [ 0 ] ; } } else if ( 'path' in event && ( event as { path : EventTarget [ ] ; } ) . path . length ) { return ( event as { path : EventTarget [ ] ; } ) . path [ 0 ] ; } return event . target ; } catch { return event . target ; } } export function initMutationObserver ( cb , doc , blockClass , blockSelector , maskTextClass , maskTextSelector , inlineStylesheet , maskInputOptions , maskTextFn , maskInputFn , recordCanvas , slimDOMOptions , mirror , iframeManager , shadowDomManager , rootEl ) { const mutationBuffer = new MutationBuffer ( ) ; mutationBuffers . push ( mutationBuffer ) ; mutationBuffer . init ( cb , blockClass , blockSelector , maskTextClass , maskTextSelector , inlineStylesheet , maskInputOptions , maskTextFn , maskInputFn , recordCanvas , slimDOMOptions , doc , mirror , iframeManager , shadowDomManager ) ; let mutationObserverCtor = window . MutationObserver || /**
         * Some websites may disable MutationObserver by removing it from the window object.
         * If someone is using rrweb to build a browser extention or things like it, they
         * could not change the website's code but can have an opportunity to inject some
         * code before the website executing its JS logic.
         * Then they can do this to store the native MutationObserver:
         * window.__rrMutationObserver = MutationObserver
         */ ( window as WindowWithStoredMutationObserver ) . __rrMutationObserver ; const angularZoneSymbol = ( window as WindowWithAngularZone ) ?. Zone ?. __symbol__ ?. ( 'MutationObserver' ) ; if ( angularZoneSymbol && ( ( window as unknown ) as Record < string , typeof MutationObserver > ) [ angularZoneSymbol ] ) { mutationObserverCtor = ( ( window as unknown ) as Record < string , typeof MutationObserver > ) [ angularZoneSymbol ] ; } const observer = new mutationObserverCtor ( mutationBuffer . processMutations . bind ( mutationBuffer ) ) ; observer . observe ( rootEl , { attributes : true , attributeOldValue : true , characterData : true , characterDataOldValue : true , childList : true , subtree : true , } ) ; return observer ; } function initMoveObserver ( cb , sampling , doc , mirror ) { if ( sampling . mousemove === false ) { return ( ) => { } ; } const threshold = typeof sampling . mousemove === 'number' ? sampling . mousemove : 50 ; const callbackThreshold = typeof sampling . mousemoveCallback === 'number' ? sampling . mousemoveCallback : 500 ; let positions = [ ] ; let timeBaseline ; const wrappedCb = throttle ( ( source ) => { const totalOffset = Date . now ( ) - timeBaseline ! ; cb ( positions . map ( ( p ) => { p . timeOffset -= totalOffset ; return p ; } ) , source ) ; positions = [ ] ; timeBaseline = null ; } , callbackThreshold ) ; const updatePosition = throttle < MouseEvent | TouchEvent | DragEvent > ( ( evt ) => { const target = getEventTarget ( evt ) ; const { clientX , clientY } = isTouchEvent ( evt ) ? evt . changedTouches [ 0 ] : evt ; if ( ! timeBaseline ) { timeBaseline = Date . now ( ) ; } positions . push ( { x : clientX , y : clientY , id : mirror . getId ( target as INode ) , timeOffset : Date . now ( ) - timeBaseline , } ) ; wrappedCb ( typeof DragEvent !== 'undefined' && evt instanceof DragEvent ? IncrementalSource . Drag : evt instanceof MouseEvent ? IncrementalSource . MouseMove : IncrementalSource . TouchMove ) ; } , threshold , { trailing : false , } ) ; const handlers = [ on ( 'mousemove' , updatePosition , doc ) , on ( 'touchmove' , updatePosition , doc ) , on ( 'drag' , updatePosition , doc ) , ] ; return ( ) => { handlers . forEach ( ( h ) => h ( ) ) ; } ; } function initMouseInteractionObserver ( cb , doc , mirror , blockClass , sampling ) { if ( sampling . mouseInteraction === false ) { return ( ) => { } ; } const disableMap = sampling . mouseInteraction === true || sampling . mouseInteraction === undefined ? { } : sampling . mouseInteraction ; const handlers = [ ] ; const getHandler = ( eventKey ) => { return ( event ) => { const target = getEventTarget ( event ) as Node ; if ( isBlocked ( target as Node , blockClass ) ) { return ; } const e = isTouchEvent ( event ) ? event . changedTouches [ 0 ] : event ; if ( ! e ) { return ; } const id = mirror . getId ( target as INode ) ; const { clientX , clientY } = e ; cb ( { type : MouseInteractions [ eventKey ] , id , x : clientX , y : clientY , } ) ; } ; } ; Object . keys ( MouseInteractions ) . filter ( ( key ) => Number . isNaN ( Number ( key ) ) && ! key . endsWith ( '_Departed' ) && disableMap [ key ] !== false ) . forEach ( ( eventKey ) => { const eventName = eventKey . toLowerCase ( ) ; const handler = getHandler ( eventKey ) ; handlers . push ( on ( eventName , handler , doc ) ) ; } ) ; return ( ) => { handlers . forEach ( ( h ) => h ( ) ) ; } ; } export function initScrollObserver ( cb , doc , mirror , blockClass , sampling ) { const updatePosition = throttle < UIEvent > ( ( evt ) => { const target = getEventTarget ( evt ) ; if ( ! target || isBlocked ( target as Node , blockClass ) ) { return ; } const id = mirror . getId ( target as INode ) ; if ( target === doc ) { const scrollEl = ( doc . scrollingElement || doc . documentElement ) ! ; cb ( { id , x : scrollEl . scrollLeft , y : scrollEl . scrollTop , } ) ; } else { cb ( { id , x : ( target as HTMLElement ) . scrollLeft , y : ( target as HTMLElement ) . scrollTop , } ) ; } } , sampling . scroll || 100 ) ; return on ( 'scroll' , updatePosition , doc ) ; } function initViewportResizeObserver ( cb ) { let lastH = - 1 ; let lastW = - 1 ; const updateDimension = throttle ( ( ) => { const height = getWindowHeight ( ) ; const width = getWindowWidth ( ) ; if ( lastH !== height || lastW !== width ) { cb ( { width : Number ( width ) , height : Number ( height ) , } ) ; lastH = height ; lastW = width ; } } , 200 ) ; return on ( 'resize' , updateDimension , window ) ; } function wrapEventWithUserTriggeredFlag ( v , enable ) { const value = { ... v } ; if ( ! enable ) delete value . userTriggered ; return value ; } export const INPUT_TAGS = [ 'INPUT' , 'TEXTAREA' , 'SELECT' ] ; const lastInputValueMap = new WeakMap ( ) ; function initInputObserver ( cb , doc , mirror , blockClass , ignoreClass , maskInputOptions , maskInputFn , sampling , userTriggeredOnInput ) { function eventHandler ( event ) { const target = getEventTarget ( event ) ; const userTriggered = event . isTrusted ; if ( ! target || ! ( target as Element ) . tagName || INPUT_TAGS . indexOf ( ( target as Element ) . tagName ) < 0 || isBlocked ( target as Node , blockClass ) ) { return ; } const type = ( target as HTMLInputElement ) . type ; if ( ( target as HTMLElement ) . classList . contains ( ignoreClass ) ) { return ; } let text = ( target as HTMLInputElement ) . value ; let isChecked = false ; if ( type === 'radio' || type === 'checkbox' ) { isChecked = ( target as HTMLInputElement ) . checked ; } else if ( maskInputOptions [ ( target as Element ) . tagName . toLowerCase ( ) as keyof MaskInputOptions ] || maskInputOptions [ type as keyof MaskInputOptions ] ) { text = maskInputValue ( { maskInputOptions , tagName : ( target as HTMLElement ) . tagName , type , value : text , maskInputFn , } ) ; } cbWithDedup ( target , wrapEventWithUserTriggeredFlag ( { text , isChecked , userTriggered } , userTriggeredOnInput ) ) ; const name = ( target as HTMLInputElement ) . name ; if ( type === 'radio' && name && isChecked ) { doc . querySelectorAll ( `input[type="radio"][name="${ name }"]` ) . forEach ( ( el ) => { if ( el !== target ) { cbWithDedup ( el , wrapEventWithUserTriggeredFlag ( { text : ( el as HTMLInputElement ) . value , isChecked : ! isChecked , userTriggered : false , } , userTriggeredOnInput ) ) ; } } ) ; } } function cbWithDedup ( target , v ) { const lastInputValue = lastInputValueMap . get ( target ) ; if ( ! lastInputValue || lastInputValue . text !== v . text || lastInputValue . isChecked !== v . isChecked ) { lastInputValueMap . set ( target , v ) ; const id = mirror . getId ( target as INode ) ; cb ( { ... v , id , } ) ; } } const events = sampling . input === 'last' ? [ 'change' ] : [ 'input' , 'change' ] ; const handlers = events . map ( ( eventName ) => on ( eventName , eventHandler , doc ) ) ; const propertyDescriptor = Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , 'value' ) ; const hookProperties = [ [ HTMLInputElement . prototype , 'value' ] , [ HTMLInputElement . prototype , 'checked' ] , [ HTMLSelectElement . prototype , 'value' ] , [ HTMLTextAreaElement . prototype , 'value' ] , [ HTMLSelectElement . prototype , 'selectedIndex' ] , ] ; if ( propertyDescriptor && propertyDescriptor . set ) { handlers . push ( ... hookProperties . map ( ( p ) => hookSetter < HTMLElement > ( p [ 0 ] , p [ 1 ] , { set ( ) { eventHandler ( { target : this } as Event ) ; } , } ) ) ) ; } return ( ) => { handlers . forEach ( ( h ) => h ( ) ) ; } ; } type GroupingCSSRule = CSSGroupingRule | CSSMediaRule | CSSSupportsRule | CSSConditionRule ; type GroupingCSSRuleTypes = typeof CSSGroupingRule | typeof CSSMediaRule | typeof CSSSupportsRule | typeof CSSConditionRule ; function getNestedCSSRulePositions ( rule ) { const positions = [ ] ; function recurse ( childRule , pos ) { if ( ( isCSSGroupingRuleSupported && childRule . parentRule instanceof CSSGroupingRule ) || ( isCSSMediaRuleSupported && childRule . parentRule instanceof CSSMediaRule ) || ( isCSSSupportsRuleSupported && childRule . parentRule instanceof CSSSupportsRule ) || ( isCSSConditionRuleSupported && childRule . parentRule instanceof CSSConditionRule ) ) { const rules = Array . from ( ( childRule . parentRule as GroupingCSSRule ) . cssRules ) ; const index = rules . indexOf ( childRule ) ; pos . unshift ( index ) ; } else { const rules = Array . from ( childRule . parentStyleSheet ! . cssRules ) ; const index = rules . indexOf ( childRule ) ; pos . unshift ( index ) ; } return pos ; } return recurse ( rule , positions ) ; } function initStyleSheetObserver ( cb , win , mirror ) { const insertRule = win . CSSStyleSheet . prototype . insertRule ; win . CSSStyleSheet . prototype . insertRule = function ( rule , index ? ) { const id = mirror . getId ( this . ownerNode as INode ) ; if ( id !== - 1 ) { cb ( { id , adds : [ { rule , index } ] , } ) ; } return insertRule . apply ( this , arguments ) ; } ; const deleteRule = win . CSSStyleSheet . prototype . deleteRule ; win . CSSStyleSheet . prototype . deleteRule = function ( index ) { const id = mirror . getId ( this . ownerNode as INode ) ; if ( id !== - 1 ) { cb ( { id , removes : [ { index } ] , } ) ; } return deleteRule . apply ( this , arguments ) ; } ; const supportedNestedCSSRuleTypes = { } ; if ( isCSSGroupingRuleSupported ) { supportedNestedCSSRuleTypes . CSSGroupingRule = win . CSSGroupingRule ; } else { if ( isCSSMediaRuleSupported ) { supportedNestedCSSRuleTypes . CSSMediaRule = win . CSSMediaRule ; } if ( isCSSConditionRuleSupported ) { supportedNestedCSSRuleTypes . CSSConditionRule = win . CSSConditionRule ; } if ( isCSSSupportsRuleSupported ) { supportedNestedCSSRuleTypes . CSSSupportsRule = win . CSSSupportsRule ; } } const unmodifiedFunctions = { } ; Object . entries ( supportedNestedCSSRuleTypes ) . forEach ( ( [ typeKey , type ] ) => { unmodifiedFunctions [ typeKey ] = { insertRule : ( type as GroupingCSSRuleTypes ) . prototype . insertRule , deleteRule : ( type as GroupingCSSRuleTypes ) . prototype . deleteRule , } ; type . prototype . insertRule = function ( rule , index ? ) { const id = mirror . getId ( this . parentStyleSheet . ownerNode as INode ) ; if ( id !== - 1 ) { cb ( { id , adds : [ { rule , index : [ ... getNestedCSSRulePositions ( this ) , index || 0 , ] , } , ] , } ) ; } return unmodifiedFunctions [ typeKey ] . insertRule . apply ( this , arguments ) ; } ; type . prototype . deleteRule = function ( index ) { const id = mirror . getId ( this . parentStyleSheet . ownerNode as INode ) ; if ( id !== - 1 ) { cb ( { id , removes : [ { index : [ ... getNestedCSSRulePositions ( this ) , index ] } ] , } ) ; } return unmodifiedFunctions [ typeKey ] . deleteRule . apply ( this , arguments ) ; } ; } ) ; return ( ) => { win . CSSStyleSheet . prototype . insertRule = insertRule ; win . CSSStyleSheet . prototype . deleteRule = deleteRule ; Object . entries ( supportedNestedCSSRuleTypes ) . forEach ( ( [ typeKey , type ] ) => { type . prototype . insertRule = unmodifiedFunctions [ typeKey ] . insertRule ; type . prototype . deleteRule = unmodifiedFunctions [ typeKey ] . deleteRule ; } ) ; } ; } function initStyleDeclarationObserver ( cb , win , mirror ) { const setProperty = win . CSSStyleDeclaration . prototype . setProperty ; win . CSSStyleDeclaration . prototype . setProperty = function ( this , property , value , priority ) { const id = mirror . getId ( ( this . parentRule ?. parentStyleSheet ?. ownerNode as unknown ) as INode ) ; if ( id !== - 1 ) { cb ( { id , set : { property , value , priority , } , index : getNestedCSSRulePositions ( this . parentRule ! ) , } ) ; } return setProperty . apply ( this , arguments ) ; } ; const removeProperty = win . CSSStyleDeclaration . prototype . removeProperty ; win . CSSStyleDeclaration . prototype . removeProperty = function ( this , property ) { const id = mirror . getId ( ( this . parentRule ?. parentStyleSheet ?. ownerNode as unknown ) as INode ) ; if ( id !== - 1 ) { cb ( { id , remove : { property , } , index : getNestedCSSRulePositions ( this . parentRule ! ) , } ) ; } return removeProperty . apply ( this , arguments ) ; } ; return ( ) => { win . CSSStyleDeclaration . prototype . setProperty = setProperty ; win . CSSStyleDeclaration . prototype . removeProperty = removeProperty ; } ; } function initMediaInteractionObserver ( mediaInteractionCb , blockClass , mirror , sampling ) { const handler = ( type ) => throttle ( ( event ) => { const target = getEventTarget ( event ) ; if ( ! target || isBlocked ( target as Node , blockClass ) ) { return ; } const { currentTime , volume , muted } = target as HTMLMediaElement ; mediaInteractionCb ( { type , id : mirror . getId ( target as INode ) , currentTime , volume , muted , } ) ; } , sampling . media || 500 ) ; const handlers = [ on ( 'play' , handler ( MediaInteractions . Play ) ) , on ( 'pause' , handler ( MediaInteractions . Pause ) ) , on ( 'seeked' , handler ( MediaInteractions . Seeked ) ) , on ( 'volumechange' , handler ( MediaInteractions . VolumeChange ) ) , ] ; return ( ) => { handlers . forEach ( ( h ) => h ( ) ) ; } ; } function initCanvasMutationObserver ( cb , win , blockClass , mirror ) { const props = Object . getOwnPropertyNames ( win . CanvasRenderingContext2D . prototype ) ; const handlers = [ ] ; for ( const prop of props ) { try { if ( typeof win . CanvasRenderingContext2D . prototype [ prop as keyof CanvasRenderingContext2D ] !== 'function' ) { continue ; } const restoreHandler = patch ( win . CanvasRenderingContext2D . prototype , prop , function ( original ) { return function ( this , ... args ) { if ( ! isBlocked ( this . canvas , blockClass ) ) { setTimeout ( ( ) => { const recordArgs = [ ... args ] ; if ( prop === 'drawImage' ) { if ( recordArgs [ 0 ] && recordArgs [ 0 ] instanceof HTMLCanvasElement ) { const canvas = recordArgs [ 0 ] ; const ctx = canvas . getContext ( '2d' ) ; let imgd = ctx ?. getImageData ( 0 , 0 , canvas . width , canvas . height ) ; let pix = imgd ?. data ; recordArgs [ 0 ] = JSON . stringify ( pix ) ; } } cb ( { id : mirror . getId ( ( this . canvas as unknown ) as INode ) , property : prop , args : recordArgs , } ) ; } , 0 ) ; } return original . apply ( this , args ) ; } ; } ) ; handlers . push ( restoreHandler ) ; } catch { const hookHandler = hookSetter < CanvasRenderingContext2D > ( win . CanvasRenderingContext2D . prototype , prop , { set ( v ) { cb ( { id : mirror . getId ( ( this . canvas as unknown ) as INode ) , property : prop , args : [ v ] , setter : true , } ) ; } , } ) ; handlers . push ( hookHandler ) ; } } return ( ) => { handlers . forEach ( ( h ) => h ( ) ) ; } ; } function initFontObserver ( cb , doc ) { const win = doc . defaultView as IWindow ; if ( ! win ) { return ( ) => { } ; } const handlers = [ ] ; const fontMap = new WeakMap < FontFace , fontParam > ( ) ; const originalFontFace = win . FontFace ; win . FontFace = ( function FontFace ( family , source , descriptors ? ) { const fontFace = new originalFontFace ( family , source , descriptors ) ; fontMap . set ( fontFace , { family , buffer : typeof source !== 'string' , descriptors , fontSource : typeof source === 'string' ? source : JSON . stringify ( Array . from ( new Uint8Array ( source as any ) ) ) , } ) ; return fontFace ; } as unknown ) as typeof FontFace ; const restoreHandler = patch ( doc . fonts , 'add' , function ( original ) { return function ( this , fontFace ) { setTimeout ( ( ) => { const p = fontMap . get ( fontFace ) ; if ( p ) { cb ( p ) ; fontMap . delete ( fontFace ) ; } } , 0 ) ; return original . apply ( this , [ fontFace ] ) ; } ; } ) ; handlers . push ( ( ) => { win . FontFace = originalFontFace ; } ) ; handlers . push ( restoreHandler ) ; return ( ) => { handlers . forEach ( ( h ) => h ( ) ) ; } ; } function mergeHooks ( o , hooks ) { const { mutationCb , mousemoveCb , mouseInteractionCb , scrollCb , viewportResizeCb , inputCb , mediaInteractionCb , styleSheetRuleCb , styleDeclarationCb , canvasMutationCb , fontCb , } = o ; o . mutationCb = ( ... p ) => { if ( hooks . mutation ) { hooks . mutation ( ... p ) ; } mutationCb ( ... p ) ; } ; o . mousemoveCb = ( ... p ) => { if ( hooks . mousemove ) { hooks . mousemove ( ... p ) ; } mousemoveCb ( ... p ) ; } ; o . mouseInteractionCb = ( ... p ) => { if ( hooks . mouseInteraction ) { hooks . mouseInteraction ( ... p ) ; } mouseInteractionCb ( ... p ) ; } ; o . scrollCb = ( ... p ) => { if ( hooks . scroll ) { hooks . scroll ( ... p ) ; } scrollCb ( ... p ) ; } ; o . viewportResizeCb = ( ... p ) => { if ( hooks . viewportResize ) { hooks . viewportResize ( ... p ) ; } viewportResizeCb ( ... p ) ; } ; o . inputCb = ( ... p ) => { if ( hooks . input ) { hooks . input ( ... p ) ; } inputCb ( ... p ) ; } ; o . mediaInteractionCb = ( ... p ) => { if ( hooks . mediaInteaction ) { hooks . mediaInteaction ( ... p ) ; } mediaInteractionCb ( ... p ) ; } ; o . styleSheetRuleCb = ( ... p ) => { if ( hooks . styleSheetRule ) { hooks . styleSheetRule ( ... p ) ; } styleSheetRuleCb ( ... p ) ; } ; o . styleDeclarationCb = ( ... p ) => { if ( hooks . styleDeclaration ) { hooks . styleDeclaration ( ... p ) ; } styleDeclarationCb ( ... p ) ; } ; o . canvasMutationCb = ( ... p ) => { if ( hooks . canvasMutation ) { hooks . canvasMutation ( ... p ) ; } canvasMutationCb ( ... p ) ; } ; o . fontCb = ( ... p ) => { if ( hooks . font ) { hooks . font ( ... p ) ; } fontCb ( ... p ) ; } ; } export function initObservers ( o , hooks = { } ) { const currentWindow = o . doc . defaultView ; if ( ! currentWindow ) { return ( ) => { } ; } mergeHooks ( o , hooks ) ; const mutationObserver = initMutationObserver ( o . mutationCb , o . doc , o . blockClass , o . blockSelector , o . maskTextClass , o . maskTextSelector , o . inlineStylesheet , o . maskInputOptions , o . maskTextFn , o . maskInputFn , o . recordCanvas , o . slimDOMOptions , o . mirror , o . iframeManager , o . shadowDomManager , o . doc ) ; const mousemoveHandler = initMoveObserver ( o . mousemoveCb , o . sampling , o . doc , o . mirror ) ; const mouseInteractionHandler = initMouseInteractionObserver ( o . mouseInteractionCb , o . doc , o . mirror , o . blockClass , o . sampling ) ; const scrollHandler = initScrollObserver ( o . scrollCb , o . doc , o . mirror , o . blockClass , o . sampling ) ; const viewportResizeHandler = initViewportResizeObserver ( o . viewportResizeCb ) ; const inputHandler = initInputObserver ( o . inputCb , o . doc , o . mirror , o . blockClass , o . ignoreClass , o . maskInputOptions , o . maskInputFn , o . sampling , o . userTriggeredOnInput ) ; const mediaInteractionHandler = initMediaInteractionObserver ( o . mediaInteractionCb , o . blockClass , o . mirror , o . sampling ) ; const styleSheetObserver = initStyleSheetObserver ( o . styleSheetRuleCb , currentWindow , o . mirror ) ; const styleDeclarationObserver = initStyleDeclarationObserver ( o . styleDeclarationCb , currentWindow , o . mirror ) ; const canvasMutationObserver = o . recordCanvas ? initCanvasMutationObserver ( o . canvasMutationCb , currentWindow , o . blockClass , o . mirror ) : ( ) => { } ; const fontObserver = o . collectFonts ? initFontObserver ( o . fontCb , o . doc ) : ( ) => { } ; const pluginHandlers = [ ] ; for ( const plugin of o . plugins ) { pluginHandlers . push ( plugin . observer ( plugin . callback , currentWindow , plugin . options ) ) ; } return ( ) => { mutationObserver . disconnect ( ) ; mousemoveHandler ( ) ; mouseInteractionHandler ( ) ; scrollHandler ( ) ; viewportResizeHandler ( ) ; inputHandler ( ) ; mediaInteractionHandler ( ) ; styleSheetObserver ( ) ; styleDeclarationObserver ( ) ; canvasMutationObserver ( ) ; fontObserver ( ) ; pluginHandlers . forEach ( ( h ) => h ( ) ) ; } ; }