import { Mirror , throttleOptions , listenerHandler , hookResetter , blockClass , IncrementalSource , addedNodeMutation , removedNodeMutation , textMutation , attributeMutation , mutationData , scrollData , inputData , DocumentDimension , IWindow , } from './types' ; import { INode , IGNORED_NODE , serializedNodeWithId , NodeType , isShadowRoot , } from 'rrweb-snapshot' ; export function on ( type , fn , target = document ) { const options = { capture : true , passive : true } ; target . addEventListener ( type , fn , options ) ; return ( ) => target . removeEventListener ( type , fn , options ) ; } export function createMirror ( ) { return { map : { } , getId ( n ) { if ( ! n || ! n . __sn ) { return - 1 ; } return n . __sn . id ; } , getNode ( id ) { return this . map [ id ] || null ; } , removeNodeFromMap ( n ) { const id = n . __sn && n . __sn . id ; delete this . map [ id ] ; if ( n . childNodes ) { n . childNodes . forEach ( ( child ) => this . removeNodeFromMap ( ( child as Node ) as INode ) ) ; } } , has ( id ) { return this . map . hasOwnProperty ( id ) ; } , reset ( ) { this . map = { } ; } , } ; } const DEPARTED_MIRROR_ACCESS_WARNING = 'Please stop import mirror directly. Instead of that,' + '\r\n' + 'now you can use replayer.getMirror() to access the mirror instance of a replayer,' + '\r\n' + 'or you can use record.mirror to access the mirror instance during recording.' ; export let _mirror = { map : { } , getId ( ) { console . error ( DEPARTED_MIRROR_ACCESS_WARNING ) ; return - 1 ; } , getNode ( ) { console . error ( DEPARTED_MIRROR_ACCESS_WARNING ) ; return null ; } , removeNodeFromMap ( ) { console . error ( DEPARTED_MIRROR_ACCESS_WARNING ) ; } , has ( ) { console . error ( DEPARTED_MIRROR_ACCESS_WARNING ) ; return false ; } , reset ( ) { console . error ( DEPARTED_MIRROR_ACCESS_WARNING ) ; } , } ; if ( typeof window !== 'undefined' && window . Proxy && window . Reflect ) { _mirror = new Proxy ( _mirror , { get ( target , prop , receiver ) { if ( prop === 'map' ) { console . error ( DEPARTED_MIRROR_ACCESS_WARNING ) ; } return Reflect . get ( target , prop , receiver ) ; } , } ) ; } export function throttle < T > ( func , wait , options = { } ) { let timeout = null ; let previous = 0 ; return function ( arg ) { let now = Date . now ( ) ; if ( ! previous && options . leading === false ) { previous = now ; } let remaining = wait - ( now - previous ) ; let context = this ; let args = arguments ; if ( remaining <= 0 || remaining > wait ) { if ( timeout ) { clearTimeout ( timeout ) ; timeout = null ; } previous = now ; func . apply ( context , args ) ; } else if ( ! timeout && options . trailing !== false ) { timeout = setTimeout ( ( ) => { previous = options . leading === false ? 0 : Date . now ( ) ; timeout = null ; func . apply ( context , args ) ; } , remaining ) ; } } ; } export function hookSetter < T > ( target , key , d , isRevoked ? , win = window ) { const original = win . Object . getOwnPropertyDescriptor ( target , key ) ; win . Object . defineProperty ( target , key , isRevoked ? d : { set ( value ) { setTimeout ( ( ) => { d . set ! . call ( this , value ) ; } , 0 ) ; if ( original && original . set ) { original . set . call ( this , value ) ; } } , } ) ; return ( ) => hookSetter ( target , key , original || { } , true ) ; } export function patch ( source , name , replacement ) { try { if ( ! ( name in source ) ) { return ( ) => { } ; } const original = source [ name ] as ( ) => unknown ; const wrapped = replacement ( original ) ; if ( typeof wrapped === 'function' ) { wrapped . prototype = wrapped . prototype || { } ; Object . defineProperties ( wrapped , { __rrweb_original__ : { enumerable : false , value : original , } , } ) ; } source [ name ] = wrapped ; return ( ) => { source [ name ] = original ; } ; } catch { return ( ) => { } ; } } export function getWindowHeight ( ) { return ( window . innerHeight || ( document . documentElement && document . documentElement . clientHeight ) || ( document . body && document . body . clientHeight ) ) ; } export function getWindowWidth ( ) { return ( window . innerWidth || ( document . documentElement && document . documentElement . clientWidth ) || ( document . body && document . body . clientWidth ) ) ; } export function isBlocked ( node , blockClass ) { if ( ! node ) { return false ; } if ( node . nodeType === node . ELEMENT_NODE ) { let needBlock = false ; if ( typeof blockClass === 'string' ) { needBlock = ( node as HTMLElement ) . classList . contains ( blockClass ) ; } else { ( node as HTMLElement ) . classList . forEach ( ( className ) => { if ( blockClass . test ( className ) ) { needBlock = true ; } } ) ; } return needBlock || isBlocked ( node . parentNode , blockClass ) ; } if ( node . nodeType === node . TEXT_NODE ) { return isBlocked ( node . parentNode , blockClass ) ; } return isBlocked ( node . parentNode , blockClass ) ; } export function isIgnored ( n ) { if ( '__sn' in n ) { return ( n as INode ) . __sn . id === IGNORED_NODE ; } return false ; } export function isAncestorRemoved ( target , mirror ) { if ( isShadowRoot ( target ) ) { return false ; } const id = mirror . getId ( target ) ; if ( ! mirror . has ( id ) ) { return true ; } if ( target . parentNode && target . parentNode . nodeType === target . DOCUMENT_NODE ) { return false ; } if ( ! target . parentNode ) { return true ; } return isAncestorRemoved ( ( target . parentNode as unknown ) as INode , mirror ) ; } export function isTouchEvent ( event ) { return Boolean ( ( event as TouchEvent ) . changedTouches ) ; } export function polyfill ( win = window ) { if ( 'NodeList' in win && ! win . NodeList . prototype . forEach ) { win . NodeList . prototype . forEach = ( Array . prototype . forEach as unknown ) as NodeList [ 'forEach' ] ; } if ( 'DOMTokenList' in win && ! win . DOMTokenList . prototype . forEach ) { win . DOMTokenList . prototype . forEach = ( Array . prototype . forEach as unknown ) as DOMTokenList [ 'forEach' ] ; } if ( ! Node . prototype . contains ) { Node . prototype . contains = function contains ( node ) { if ( ! ( 0 in arguments ) ) { throw new TypeError ( '1 argument is required' ) ; } do { if ( this === node ) { return true ; } } while ( ( node = node && node . parentNode ) ) ; return false ; } ; } } export type TreeNode = { id : number ; mutation : addedNodeMutation ; parent ? : TreeNode ; children : Record < number , TreeNode > ; texts : textMutation [ ] ; attributes : attributeMutation [ ] ; } ; export class TreeIndex { public tree ! : Record < number , TreeNode > ; private removeNodeMutations ! : removedNodeMutation [ ] ; private textMutations ! : textMutation [ ] ; private attributeMutations ! : attributeMutation [ ] ; private indexes ! : Map < number , TreeNode > ; private removeIdSet ! : Set < number > ; private scrollMap ! : Map < number , scrollData > ; private inputMap ! : Map < number , inputData > ; constructor ( ) { this . reset ( ) ; } public add ( mutation ) { const parentTreeNode = this . indexes . get ( mutation . parentId ) ; const treeNode = { id : mutation . node . id , mutation , children : [ ] , texts : [ ] , attributes : [ ] , } ; if ( ! parentTreeNode ) { this . tree [ treeNode . id ] = treeNode ; } else { treeNode . parent = parentTreeNode ; parentTreeNode . children [ treeNode . id ] = treeNode ; } this . indexes . set ( treeNode . id , treeNode ) ; } public remove ( mutation , mirror ) { const parentTreeNode = this . indexes . get ( mutation . parentId ) ; const treeNode = this . indexes . get ( mutation . id ) ; const deepRemoveFromMirror = ( id ) => { this . removeIdSet . add ( id ) ; const node = mirror . getNode ( id ) ; node ?. childNodes . forEach ( ( childNode ) => { if ( '__sn' in childNode ) { deepRemoveFromMirror ( ( ( childNode as unknown ) as INode ) . __sn . id ) ; } } ) ; } ; const deepRemoveFromTreeIndex = ( node ) => { this . removeIdSet . add ( node . id ) ; Object . values ( node . children ) . forEach ( ( n ) => deepRemoveFromTreeIndex ( n ) ) ; const _treeNode = this . indexes . get ( node . id ) ; if ( _treeNode ) { const _parentTreeNode = _treeNode . parent ; if ( _parentTreeNode ) { delete _treeNode . parent ; delete _parentTreeNode . children [ _treeNode . id ] ; this . indexes . delete ( mutation . id ) ; } } } ; if ( ! treeNode ) { this . removeNodeMutations . push ( mutation ) ; deepRemoveFromMirror ( mutation . id ) ; } else if ( ! parentTreeNode ) { delete this . tree [ treeNode . id ] ; this . indexes . delete ( treeNode . id ) ; deepRemoveFromTreeIndex ( treeNode ) ; } else { delete treeNode . parent ; delete parentTreeNode . children [ treeNode . id ] ; this . indexes . delete ( mutation . id ) ; deepRemoveFromTreeIndex ( treeNode ) ; } } public text ( mutation ) { const treeNode = this . indexes . get ( mutation . id ) ; if ( treeNode ) { treeNode . texts . push ( mutation ) ; } else { this . textMutations . push ( mutation ) ; } } public attribute ( mutation ) { const treeNode = this . indexes . get ( mutation . id ) ; if ( treeNode ) { treeNode . attributes . push ( mutation ) ; } else { this . attributeMutations . push ( mutation ) ; } } public scroll ( d ) { this . scrollMap . set ( d . id , d ) ; } public input ( d ) { this . inputMap . set ( d . id , d ) ; } public flush ( ) { const { tree , removeNodeMutations , textMutations , attributeMutations , } = this ; const batchMutationData = { source : IncrementalSource . Mutation , removes : removeNodeMutations , texts : textMutations , attributes : attributeMutations , adds : [ ] , } ; const walk = ( treeNode , removed ) => { if ( removed ) { this . removeIdSet . add ( treeNode . id ) ; } batchMutationData . texts = batchMutationData . texts . concat ( removed ? [ ] : treeNode . texts ) . filter ( ( m ) => ! this . removeIdSet . has ( m . id ) ) ; batchMutationData . attributes = batchMutationData . attributes . concat ( removed ? [ ] : treeNode . attributes ) . filter ( ( m ) => ! this . removeIdSet . has ( m . id ) ) ; if ( ! this . removeIdSet . has ( treeNode . id ) && ! this . removeIdSet . has ( treeNode . mutation . parentId ) && ! removed ) { batchMutationData . adds . push ( treeNode . mutation ) ; if ( treeNode . children ) { Object . values ( treeNode . children ) . forEach ( ( n ) => walk ( n , false ) ) ; } } else { Object . values ( treeNode . children ) . forEach ( ( n ) => walk ( n , true ) ) ; } } ; Object . values ( tree ) . forEach ( ( n ) => walk ( n , false ) ) ; for ( const id of this . scrollMap . keys ( ) ) { if ( this . removeIdSet . has ( id ) ) { this . scrollMap . delete ( id ) ; } } for ( const id of this . inputMap . keys ( ) ) { if ( this . removeIdSet . has ( id ) ) { this . inputMap . delete ( id ) ; } } const scrollMap = new Map ( this . scrollMap ) ; const inputMap = new Map ( this . inputMap ) ; this . reset ( ) ; return { mutationData : batchMutationData , scrollMap , inputMap , } ; } private reset ( ) { this . tree = [ ] ; this . indexes = new Map ( ) ; this . removeNodeMutations = [ ] ; this . textMutations = [ ] ; this . attributeMutations = [ ] ; this . removeIdSet = new Set ( ) ; this . scrollMap = new Map ( ) ; this . inputMap = new Map ( ) ; } public idRemoved ( id ) { return this . removeIdSet . has ( id ) ; } } type ResolveTree = { value : addedNodeMutation ; children : ResolveTree [ ] ; parent : ResolveTree | null ; } ; export function queueToResolveTrees ( queue ) { const queueNodeMap = { } ; const putIntoMap = ( m , parent ) : ResolveTree => { const nodeInTree = { value : m , parent , children : [ ] , } ; queueNodeMap [ m . node . id ] = nodeInTree ; return nodeInTree ; } ; const queueNodeTrees = [ ] ; for ( const mutation of queue ) { const { nextId , parentId } = mutation ; if ( nextId && nextId in queueNodeMap ) { const nextInTree = queueNodeMap [ nextId ] ; if ( nextInTree . parent ) { const idx = nextInTree . parent . children . indexOf ( nextInTree ) ; nextInTree . parent . children . splice ( idx , 0 , putIntoMap ( mutation , nextInTree . parent ) ) ; } else { const idx = queueNodeTrees . indexOf ( nextInTree ) ; queueNodeTrees . splice ( idx , 0 , putIntoMap ( mutation , null ) ) ; } continue ; } if ( parentId in queueNodeMap ) { const parentInTree = queueNodeMap [ parentId ] ; parentInTree . children . push ( putIntoMap ( mutation , parentInTree ) ) ; continue ; } queueNodeTrees . push ( putIntoMap ( mutation , null ) ) ; } return queueNodeTrees ; } export function iterateResolveTree ( tree , cb ) { cb ( tree . value ) ; /**
     * The resolve tree was designed to reflect the DOM layout,
     * but we need append next sibling first, so we do a reverse
     * loop here.
     */ for ( let i = tree . children . length - 1 ; i >= 0 ; i -- ) { iterateResolveTree ( tree . children [ i ] , cb ) ; } } type HTMLIFrameINode = HTMLIFrameElement & { __sn : serializedNodeWithId ; } ; export type AppendedIframe = { mutationInQueue : addedNodeMutation ; builtNode : HTMLIFrameINode ; } ; export function isIframeINode ( node ) { if ( '__sn' in node ) { return ( node . __sn . type === NodeType . Element && node . __sn . tagName === 'iframe' ) ; } return false ; } export function getBaseDimension ( node , rootIframe ) { const frameElement = node . ownerDocument ?. defaultView ?. frameElement ; if ( ! frameElement || frameElement === rootIframe ) { return { x : 0 , y : 0 , relativeScale : 1 , absoluteScale : 1 , } ; } const frameDimension = frameElement . getBoundingClientRect ( ) ; const frameBaseDimension = getBaseDimension ( frameElement , rootIframe ) ; const relativeScale = frameDimension . height / frameElement . clientHeight ; return { x : frameDimension . x * frameBaseDimension . relativeScale + frameBaseDimension . x , y : frameDimension . y * frameBaseDimension . relativeScale + frameBaseDimension . y , relativeScale , absoluteScale : frameBaseDimension . absoluteScale * relativeScale , } ; } export function hasShadowRoot < T extends Node > ( n ) { return Boolean ( ( ( n as unknown ) as Element ) ?. shadowRoot ) ; }