/**
 * Exports here are NOT re-exported to maquette
 */ import { Projection , ProjectionOptions , VNode , VNodeProperties } from "./interfaces" ; const NAMESPACE_W3 = "http://www.w3.org/" ; const NAMESPACE_SVG = `${ NAMESPACE_W3 }2000/svg` ; const NAMESPACE_XLINK = `${ NAMESPACE_W3 }1999/xlink` ; let emptyArray = < VNode [ ] > [ ] ; export let extend = < T > ( base , overrides ) : T => { let result = { } as any ; Object . keys ( base ) . forEach ( ( key ) => { result [ key ] = ( base as any ) [ key ] ; } ) ; if ( overrides ) { Object . keys ( overrides ) . forEach ( ( key ) => { result [ key ] = overrides [ key ] ; } ) ; } return result ; } ; let same = ( vnode1 , vnode2 ) => { if ( vnode1 . vnodeSelector !== vnode2 . vnodeSelector ) { return false ; } if ( vnode1 . properties && vnode2 . properties ) { if ( vnode1 . properties . key !== vnode2 . properties . key ) { return false ; } return vnode1 . properties . bind === vnode2 . properties . bind ; } return ! vnode1 . properties && ! vnode2 . properties ; } ; let checkStyleValue = ( styleValue ) => { if ( typeof styleValue !== "string" ) { throw new Error ( "Style values must be strings" ) ; } } ; let findIndexOfChild = ( children , sameAs , start ) => { if ( sameAs . vnodeSelector !== "" ) { for ( let i = start ; i < children . length ; i ++ ) { if ( same ( children [ i ] , sameAs ) ) { return i ; } } } return - 1 ; } ; let checkDistinguishable = ( childNodes , indexToCheck , parentVNode , operation ) => { let childNode = childNodes [ indexToCheck ] ; if ( childNode . vnodeSelector === "" ) { return ; } let properties = childNode . properties ; let key = properties ? properties . key === undefined ? properties . bind : properties . key : undefined ; if ( ! key ) { for ( let i = 0 ; i < childNodes . length ; i ++ ) { if ( i !== indexToCheck ) { let node = childNodes [ i ] ; if ( same ( node , childNode ) ) { throw new Error ( `${ parentVNode . vnodeSelector } had a ${ childNode . vnodeSelector } child ${ operation === "added" ? operation : "removed" }, but there is now more than one. You must add unique key properties to make them distinguishable.` ) ; } } } } } ; let nodeAdded = ( vNode ) => { if ( vNode . properties ) { let enterAnimation = vNode . properties . enterAnimation ; if ( enterAnimation ) { enterAnimation ( vNode . domNode as Element , vNode . properties ) ; } } } ; let removedNodes = [ ] ; let requestedIdleCallback = false ; let visitRemovedNode = ( node ) => { ( node . children || [ ] ) . forEach ( visitRemovedNode ) ; if ( node . properties && node . properties . afterRemoved ) { node . properties . afterRemoved . apply ( node . properties . bind || node . properties , [ < Element > node . domNode , ] ) ; } } ; let processPendingNodeRemovals = ( ) : void => { requestedIdleCallback = false ; removedNodes . forEach ( visitRemovedNode ) ; removedNodes . length = 0 ; } ; let scheduleNodeRemoval = ( vNode ) : void => { removedNodes . push ( vNode ) ; if ( ! requestedIdleCallback ) { requestedIdleCallback = true ; if ( typeof window !== "undefined" && "requestIdleCallback" in window ) { window . requestIdleCallback ( processPendingNodeRemovals , { timeout : 16 } ) ; } else { setTimeout ( processPendingNodeRemovals , 16 ) ; } } } ; let nodeToRemove = ( vNode ) => { let domNode = vNode . domNode ! ; if ( vNode . properties ) { let exitAnimation = vNode . properties . exitAnimation ; if ( exitAnimation ) { ( domNode as HTMLElement ) . style . pointerEvents = "none" ; let removeDomNode = ( ) => { if ( domNode . parentNode ) { domNode . parentNode . removeChild ( domNode ) ; scheduleNodeRemoval ( vNode ) ; } } ; exitAnimation ( domNode as Element , removeDomNode , vNode . properties ) ; return ; } } if ( domNode . parentNode ) { domNode . parentNode . removeChild ( domNode ) ; scheduleNodeRemoval ( vNode ) ; } } ; let setProperties = ( domNode , properties , projectionOptions ) => { if ( ! properties ) { return ; } let eventHandlerInterceptor = projectionOptions . eventHandlerInterceptor ; let propNames = Object . keys ( properties ) ; let propCount = propNames . length ; for ( let i = 0 ; i < propCount ; i ++ ) { let propName = propNames [ i ] ; let propValue = properties [ propName ] ; if ( propName === "className" ) { throw new Error ( 'Property "className" is not supported, use "class".' ) ; } else if ( propName === "class" ) { toggleClasses ( domNode as HTMLElement , propValue as string , true ) ; } else if ( propName === "classes" ) { let classNames = Object . keys ( propValue ) ; let classNameCount = classNames . length ; for ( let j = 0 ; j < classNameCount ; j ++ ) { let className = classNames [ j ] ; if ( propValue [ className ] ) { ( domNode as Element ) . classList . add ( className ) ; } } } else if ( propName === "styles" ) { let styleNames = Object . keys ( propValue ) ; let styleCount = styleNames . length ; for ( let j = 0 ; j < styleCount ; j ++ ) { let styleName = styleNames [ j ] ; let styleValue = propValue [ styleName ] ; if ( styleValue ) { checkStyleValue ( styleValue ) ; projectionOptions . styleApplyer ! ( < HTMLElement > domNode , styleName , styleValue ) ; } } } else if ( propName !== "key" && propValue !== null && propValue !== undefined ) { let type = typeof propValue ; if ( type === "function" ) { if ( propName . lastIndexOf ( "on" , 0 ) === 0 ) { if ( eventHandlerInterceptor ) { propValue = eventHandlerInterceptor ( propName , propValue , domNode , properties ) ; } if ( propName === "oninput" ) { ( function ( ) { let oldPropValue = propValue ; propValue = function ( this , evt ) { oldPropValue . apply ( this , [ evt ] ) ; ( evt . target as any ) [ "oninput-value" ] = ( evt . target as HTMLInputElement ) . value ; } ; } ) ( ) ; } } ( domNode as any ) [ propName ] = propValue ; } else if ( projectionOptions . namespace === NAMESPACE_SVG ) { if ( propName === "href" ) { ( domNode as Element ) . setAttributeNS ( NAMESPACE_XLINK , propName , propValue ) ; } else { ( domNode as Element ) . setAttribute ( propName , propValue ) ; } } else if ( type === "string" && propName !== "value" && propName !== "innerHTML" ) { ( domNode as Element ) . setAttribute ( propName , propValue ) ; } else { ( domNode as any ) [ propName ] = propValue ; } } } } ; let addChildren = ( domNode , children , projectionOptions ) => { if ( ! children ) { return ; } for ( let child of children ) { createDom ( child , domNode , undefined , projectionOptions ) ; } } ; export let initPropertiesAndChildren = ( domNode , vnode , projectionOptions ) : void => { addChildren ( domNode , vnode . children , projectionOptions ) ; if ( vnode . text ) { domNode . textContent = vnode . text ; } setProperties ( domNode , vnode . properties , projectionOptions ) ; if ( vnode . properties && vnode . properties . afterCreate ) { vnode . properties . afterCreate . apply ( vnode . properties . bind || vnode . properties , [ domNode as Element , projectionOptions , vnode . vnodeSelector , vnode . properties , vnode . children , ] ) ; } } ; export let createDom = ( vnode , parentNode , insertBefore , projectionOptions ) : void => { let domNode ; let start = 0 ; let vnodeSelector = vnode . vnodeSelector ; let doc = parentNode . ownerDocument ! ; if ( vnodeSelector === "" ) { domNode = vnode . domNode = doc . createTextNode ( vnode . text ! ) ; if ( insertBefore !== undefined ) { parentNode . insertBefore ( domNode , insertBefore ) ; } else { parentNode . appendChild ( domNode ) ; } } else { for ( let i = 0 ; i <= vnodeSelector . length ; ++ i ) { let c = vnodeSelector . charAt ( i ) ; if ( i === vnodeSelector . length || c === "." || c === "#" ) { let type = vnodeSelector . charAt ( start - 1 ) ; let found = vnodeSelector . slice ( start , i ) ; if ( type === "." ) { ( domNode ! as HTMLElement ) . classList . add ( found ) ; } else if ( type === "#" ) { ( domNode ! as Element ) . id = found ; } else { if ( found === "svg" ) { projectionOptions = extend ( projectionOptions , { namespace : NAMESPACE_SVG , } ) ; } if ( projectionOptions . namespace !== undefined ) { domNode = vnode . domNode = doc . createElementNS ( projectionOptions . namespace , found ) ; } else { domNode = vnode . domNode = vnode . domNode || doc . createElement ( found ) ; if ( found === "input" && vnode . properties && vnode . properties . type !== undefined ) { ( domNode as Element ) . setAttribute ( "type" , vnode . properties . type ) ; } } if ( insertBefore !== undefined ) { parentNode . insertBefore ( domNode , insertBefore ) ; } else if ( domNode . parentNode !== parentNode ) { parentNode . appendChild ( domNode ) ; } } start = i + 1 ; } } initPropertiesAndChildren ( domNode ! , vnode , projectionOptions ) ; } } ; let updateDom ; param domNode param classes param on let toggleClasses = ( domNode , classes , on ) => { if ( ! classes ) { return ; } classes . split ( " " ) . forEach ( ( classToToggle ) => { if ( classToToggle ) { domNode . classList . toggle ( classToToggle , on ) ; } } ) ; } ; let updateProperties = ( domNode , previousProperties , properties , projectionOptions ) => { if ( ! properties ) { return ; } let propertiesUpdated = false ; let propNames = Object . keys ( properties ) ; let propCount = propNames . length ; for ( let i = 0 ; i < propCount ; i ++ ) { let propName = propNames [ i ] ; let propValue = properties [ propName ] ; let previousValue = previousProperties ! [ propName ] ; if ( propName === "class" ) { if ( previousValue !== propValue ) { toggleClasses ( domNode as HTMLElement , previousValue , false ) ; toggleClasses ( domNode as HTMLElement , propValue , true ) ; } } else if ( propName === "classes" ) { let classList = ( domNode as Element ) . classList ; let classNames = Object . keys ( propValue ) ; let classNameCount = classNames . length ; for ( let j = 0 ; j < classNameCount ; j ++ ) { let className = classNames [ j ] ; let on = ! ! propValue [ className ] ; let previousOn = ! ! previousValue [ className ] ; if ( on === previousOn ) { continue ; } propertiesUpdated = true ; if ( on ) { classList . add ( className ) ; } else { classList . remove ( className ) ; } } } else if ( propName === "styles" ) { let styleNames = Object . keys ( propValue ) ; let styleCount = styleNames . length ; for ( let j = 0 ; j < styleCount ; j ++ ) { let styleName = styleNames [ j ] ; let newStyleValue = propValue [ styleName ] ; let oldStyleValue = previousValue [ styleName ] ; if ( newStyleValue === oldStyleValue ) { continue ; } propertiesUpdated = true ; if ( newStyleValue ) { checkStyleValue ( newStyleValue ) ; projectionOptions . styleApplyer ! ( domNode as HTMLElement , styleName , newStyleValue ) ; } else { projectionOptions . styleApplyer ! ( domNode as HTMLElement , styleName , "" ) ; } } } else { if ( ! propValue && typeof previousValue === "string" ) { propValue = "" ; } if ( propName === "value" ) { let domValue = ( domNode as any ) [ propName ] ; if ( domValue !== propValue && ( ( domNode as any ) [ "oninput-value" ] ? domValue === ( domNode as any ) [ "oninput-value" ] : propValue !== previousValue ) ) { ( domNode as any ) [ propName ] = propValue ; ( domNode as any ) [ "oninput-value" ] = undefined ; } if ( propValue !== previousValue ) { propertiesUpdated = true ; } } else if ( propValue !== previousValue ) { let type = typeof propValue ; if ( type !== "function" || ! projectionOptions . eventHandlerInterceptor ) { if ( projectionOptions . namespace === NAMESPACE_SVG ) { if ( propName === "href" ) { ( domNode as Element ) . setAttributeNS ( NAMESPACE_XLINK , propName , propValue ) ; } else { ( domNode as Element ) . setAttribute ( propName , propValue ) ; } } else if ( type === "string" && propName !== "innerHTML" ) { if ( propName === "role" && propValue === "" ) { ( domNode as any ) . removeAttribute ( propName ) ; } else { ( domNode as Element ) . setAttribute ( propName , propValue ) ; } } else if ( ( domNode as any ) [ propName ] !== propValue ) { ( domNode as any ) [ propName ] = propValue ; } propertiesUpdated = true ; } } } } return propertiesUpdated ; } ; let updateChildren = ( vnode , domNode , oldChildren , newChildren , projectionOptions ) => { if ( oldChildren === newChildren ) { return false ; } oldChildren = oldChildren || emptyArray ; newChildren = newChildren || emptyArray ; let oldChildrenLength = oldChildren . length ; let newChildrenLength = newChildren . length ; let oldIndex = 0 ; let newIndex = 0 ; let i ; let textUpdated = false ; while ( newIndex < newChildrenLength ) { let oldChild = oldIndex < oldChildrenLength ? oldChildren [ oldIndex ] : undefined ; let newChild = newChildren [ newIndex ] ; if ( oldChild !== undefined && same ( oldChild , newChild ) ) { textUpdated = updateDom ( oldChild , newChild , projectionOptions ) || textUpdated ; oldIndex ++ ; } else { let findOldIndex = findIndexOfChild ( oldChildren , newChild , oldIndex + 1 ) ; if ( findOldIndex >= 0 ) { for ( i = oldIndex ; i < findOldIndex ; i ++ ) { nodeToRemove ( oldChildren [ i ] ) ; checkDistinguishable ( oldChildren , i , vnode , "removed" ) ; } textUpdated = updateDom ( oldChildren [ findOldIndex ] , newChild , projectionOptions ) || textUpdated ; oldIndex = findOldIndex + 1 ; } else { createDom ( newChild , domNode , oldIndex < oldChildrenLength ? oldChildren [ oldIndex ] . domNode : undefined , projectionOptions ) ; nodeAdded ( newChild ) ; checkDistinguishable ( newChildren , newIndex , vnode , "added" ) ; } } newIndex ++ ; } if ( oldChildrenLength > oldIndex ) { for ( i = oldIndex ; i < oldChildrenLength ; i ++ ) { nodeToRemove ( oldChildren [ i ] ) ; checkDistinguishable ( oldChildren , i , vnode , "removed" ) ; } } return textUpdated ; } ; updateDom = ( previous , vnode , projectionOptions ) => { let domNode = previous . domNode ! ; let textUpdated = false ; if ( previous === vnode ) { return false ; } let updated = false ; if ( vnode . vnodeSelector === "" ) { if ( vnode . text !== previous . text ) { let newTextNode = domNode . ownerDocument ! . createTextNode ( vnode . text ! ) ; domNode . parentNode ! . replaceChild ( newTextNode , domNode ) ; vnode . domNode = newTextNode ; textUpdated = true ; return textUpdated ; } vnode . domNode = domNode ; } else { if ( vnode . vnodeSelector . lastIndexOf ( "svg" , 0 ) === 0 ) { projectionOptions = extend ( projectionOptions , { namespace : NAMESPACE_SVG , } ) ; } if ( previous . text !== vnode . text ) { updated = true ; if ( vnode . text === undefined ) { domNode . removeChild ( domNode . firstChild ! ) ; } else { domNode . textContent = vnode . text ; } } vnode . domNode = domNode ; updated = updateChildren ( vnode , domNode , previous . children , vnode . children , projectionOptions ) || updated ; updated = updateProperties ( domNode , previous . properties , vnode . properties , projectionOptions ) || updated ; if ( vnode . properties && vnode . properties . afterUpdate ) { vnode . properties . afterUpdate . apply ( vnode . properties . bind || vnode . properties , [ < Element > domNode , projectionOptions , vnode . vnodeSelector , vnode . properties , vnode . children , ] ) ; } } if ( updated && vnode . properties && vnode . properties . updateAnimation ) { vnode . properties . updateAnimation ( < Element > domNode , vnode . properties , previous . properties ) ; } return textUpdated ; } ; export let createProjection = ( vnode , projectionOptions ) : Projection => { return { getLastRender : ( ) => vnode , update : ( updatedVnode ) => { if ( vnode . vnodeSelector !== updatedVnode . vnodeSelector ) { throw new Error ( "The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)" ) ; } let previousVNode = vnode ; vnode = updatedVnode ; updateDom ( previousVNode , updatedVnode , projectionOptions ) ; } , domNode : < Element > vnode . domNode , } ; } ;