import { applyDefaultProjectionOptions , dom } from "./dom" ; /**
 * A projector is used to create the real DOM from the the virtual DOM and to keep it up-to-date afterwards.
 *
 * You can call [[append]], [[merge]], [[insertBefore]] and [[replace]] to add the virtual DOM to the real DOM.
 * The `renderFunction` callbacks will be called to create the real DOM immediately.
 * Afterwards, the `renderFunction` callbacks will be called again to update the DOM on the next animation-frame after:
 *
 *  - The Projector's [[scheduleRender]] function  was called
 *  - An event handler (like `onclick`) on a rendered [[VNode]] was called.
 *
 * The projector stops when [[stop]] is called or when an error is thrown during rendering.
 * It is possible to use `window.onerror` to handle these errors.
 * Instances of [[Projector]] can be created using [[createProjector]].
 *
 * If you need a customized projector, please check the maquette-advanced-projector project which provides a
 * versatile stand-in replacement.
 */ import { EventHandler , EventHandlerInterceptor , Projection , ProjectionOptions , Projector , ProjectorOptions , ProjectorPerformanceLogger , VNode , VNodeProperties , } from "./interfaces" ; let createParentNodePath = ( node , rootNode ) => { let parentNodePath = [ ] ; while ( node && node !== rootNode ) { parentNodePath . push ( node ) ; node = node . parentNode ; } return parentNodePath ; } ; let find ; if ( ( Array . prototype as any ) . find ) { find = ( items , predicate ) => items . find ( predicate ) ; } else { find = ( items , predicate ) => items . filter ( predicate ) [ 0 ] ; } let findVNodeByParentNodePath = ( vnode , parentNodePath ) : VNode | undefined => { let result = vnode ; parentNodePath . forEach ( ( node ) => { result = result && result . children ? find ( result . children , ( child ) => child . domNode === node ) : undefined ; } ) ; return result ; } ; let createEventHandlerInterceptor = ( projector , getProjection , performanceLogger ) : EventHandlerInterceptor => { return ( propertyName , eventHandler , domNode , properties ) => modifiedEventHandler ; function modifiedEventHandler ( this , evt ) { performanceLogger ( "domEvent" , evt ) ; let projection = getProjection ( ) ! ; let parentNodePath = createParentNodePath ( evt . currentTarget as Element , projection . domNode ) ; parentNodePath . reverse ( ) ; let matchingVNode = findVNodeByParentNodePath ( projection . getLastRender ( ) , parentNodePath ) ; projector . scheduleRender ( ) ; let result ; if ( matchingVNode ) { result = matchingVNode . properties ! [ `on${ evt . type }` ] . apply ( matchingVNode . properties ! . bind || this , arguments ) ; } performanceLogger ( "domEventProcessed" , evt ) ; return result ; } } ; param projectorOptions export let createProjector = ( projectorOptions ? ) : Projector => { let projector ; let projectionOptions = applyDefaultProjectionOptions ( projectorOptions ) ; let performanceLogger = projectionOptions . performanceLogger ! ; let renderCompleted = true ; let scheduled ; let stopped = false ; let projections = [ ] as Projection [ ] ; let renderFunctions = [ ] as ( ( ) => VNode ) [ ] ; let addProjection = ( domFunction , node , renderFunction ) : void => { let projection ; let getProjection = ( ) => projection ; projectionOptions . eventHandlerInterceptor = createEventHandlerInterceptor ( projector , getProjection , performanceLogger ) ; projection = domFunction ( node , renderFunction ( ) , projectionOptions ) ; projections . push ( projection ) ; renderFunctions . push ( renderFunction ) ; } ; let doRender = ( ) => { scheduled = undefined ; if ( ! renderCompleted ) { return ; } renderCompleted = false ; performanceLogger ( "renderStart" , undefined ) ; for ( let i = 0 ; i < projections . length ; i ++ ) { let updatedVnode = renderFunctions [ i ] ( ) ; performanceLogger ( "rendered" , undefined ) ; projections [ i ] . update ( updatedVnode ) ; performanceLogger ( "patched" , undefined ) ; } performanceLogger ( "renderDone" , undefined ) ; renderCompleted = true ; } ; projector = { renderNow : doRender , scheduleRender : ( ) => { if ( ! scheduled && ! stopped ) { scheduled = requestAnimationFrame ( doRender ) ; } } , stop : ( ) => { if ( scheduled ) { cancelAnimationFrame ( scheduled ) ; scheduled = undefined ; } stopped = true ; } , resume : ( ) => { stopped = false ; renderCompleted = true ; projector . scheduleRender ( ) ; } , append : ( parentNode , renderFunction ) => { addProjection ( dom . append , parentNode , renderFunction ) ; } , insertBefore : ( beforeNode , renderFunction ) => { addProjection ( dom . insertBefore , beforeNode , renderFunction ) ; } , merge : ( domNode , renderFunction ) => { addProjection ( dom . merge , domNode , renderFunction ) ; } , replace : ( domNode , renderFunction ) => { addProjection ( dom . replace , domNode , renderFunction ) ; } , detach : ( renderFunction ) => { for ( let i = 0 ; i < renderFunctions . length ; i ++ ) { if ( renderFunctions [ i ] === renderFunction ) { renderFunctions . splice ( i , 1 ) ; return projections . splice ( i , 1 ) [ 0 ] ; } } throw new Error ( "renderFunction was not found" ) ; } , } ; return projector ; } ;