import { Module } from "./modules/module" ; import { vnode , VNode } from "./vnode" ; import * as is from "./is" ; import { htmlDomApi , DOMAPI } from "./htmldomapi" ; type NonUndefined < T > = T extends undefined ? never : T ; function isUndef ( s ) { return s === undefined ; } function isDef < A > ( s ) { return s !== undefined ; } type VNodeQueue = VNode [ ] ; const emptyNode = vnode ( "" , { } , [ ] , undefined , undefined ) ; function sameVnode ( vnode1 , vnode2 ) { const isSameKey = vnode1 . key === vnode2 . key ; const isSameIs = vnode1 . data ?. is === vnode2 . data ?. is ; const isSameSel = vnode1 . sel === vnode2 . sel ; return isSameSel && isSameKey && isSameIs ; } todo function documentFragmentIsNotSupported ( ) { throw new Error ( "The document fragment is not supported on this platform." ) ; } function isElement ( api , vnode ) { return api . isElement ( vnode as any ) ; } function isDocumentFragment ( api , vnode ) { return api . isDocumentFragment ! ( vnode as any ) ; } type KeyToIndexMap = { [ key ] : number ; } ; type ArraysOf < T > = { [ K in keyof T ] : Array < T [ K ] > ; } ; type ModuleHooks = ArraysOf < Required < Module > > ; function createKeyToOldIdx ( children , beginIdx , endIdx ) { const map = { } ; for ( let i = beginIdx ; i <= endIdx ; ++ i ) { const key = children [ i ] ?. key ; if ( key !== undefined ) { map [ key as string ] = i ; } } return map ; } const hooks = [ "create" , "update" , "remove" , "destroy" , "pre" , "post" , ] ; type Options = { experimental ? : { fragments ? : boolean ; } ; } ; export function init ( modules , domApi ? , options ? ) { const cbs = { create : [ ] , update : [ ] , remove : [ ] , destroy : [ ] , pre : [ ] , post : [ ] , } ; const api = domApi !== undefined ? domApi : htmlDomApi ; for ( const hook of hooks ) { for ( const module of modules ) { const currentHook = module [ hook ] ; if ( currentHook !== undefined ) { ( cbs [ hook ] as any [ ] ) . push ( currentHook ) ; } } } function emptyNodeAt ( elm ) { const id = elm . id ? "#" + elm . id : "" ; const classes = elm . getAttribute ( "class" ) ; const c = classes ? "." + classes . split ( " " ) . join ( "." ) : "" ; return vnode ( api . tagName ( elm ) . toLowerCase ( ) + id + c , { } , [ ] , undefined , elm ) ; } function emptyDocumentFragmentAt ( frag ) { return vnode ( undefined , { } , [ ] , undefined , frag ) ; } function createRmCb ( childElm , listeners ) { return function rmCb ( ) { if ( -- listeners === 0 ) { const parent = api . parentNode ( childElm ) as Node ; api . removeChild ( parent , childElm ) ; } } ; } function createElm ( vnode , insertedVnodeQueue ) { let i ; let data = vnode . data ; if ( data !== undefined ) { const init = data . hook ?. init ; if ( isDef ( init ) ) { init ( vnode ) ; data = vnode . data ; } } const children = vnode . children ; const sel = vnode . sel ; if ( sel === "!" ) { if ( isUndef ( vnode . text ) ) { vnode . text = "" ; } vnode . elm = api . createComment ( vnode . text ! ) ; } else if ( sel !== undefined ) { const hashIdx = sel . indexOf ( "#" ) ; const dotIdx = sel . indexOf ( "." , hashIdx ) ; const hash = hashIdx > 0 ? hashIdx : sel . length ; const dot = dotIdx > 0 ? dotIdx : sel . length ; const tag = hashIdx !== - 1 || dotIdx !== - 1 ? sel . slice ( 0 , Math . min ( hash , dot ) ) : sel ; const elm = ( vnode . elm = isDef ( data ) && isDef ( ( i = data . ns ) ) ? api . createElementNS ( i , tag , data ) : api . createElement ( tag , data ) ) ; if ( hash < dot ) elm . setAttribute ( "id" , sel . slice ( hash + 1 , dot ) ) ; if ( dotIdx > 0 ) elm . setAttribute ( "class" , sel . slice ( dot + 1 ) . replace ( /\./g , " " ) ) ; for ( i = 0 ; i < cbs . create . length ; ++ i ) cbs . create [ i ] ( emptyNode , vnode ) ; if ( is . array ( children ) ) { for ( i = 0 ; i < children . length ; ++ i ) { const ch = children [ i ] ; if ( ch != null ) { api . appendChild ( elm , createElm ( ch as VNode , insertedVnodeQueue ) ) ; } } } else if ( is . primitive ( vnode . text ) ) { api . appendChild ( elm , api . createTextNode ( vnode . text ) ) ; } const hook = vnode . data ! . hook ; if ( isDef ( hook ) ) { hook . create ?. ( emptyNode , vnode ) ; if ( hook . insert ) { insertedVnodeQueue . push ( vnode ) ; } } } else if ( options ?. experimental ?. fragments && vnode . children ) { const children = vnode . children ; vnode . elm = ( api . createDocumentFragment ?? documentFragmentIsNotSupported ) ( ) ; for ( i = 0 ; i < cbs . create . length ; ++ i ) cbs . create [ i ] ( emptyNode , vnode ) ; for ( i = 0 ; i < children . length ; ++ i ) { const ch = children [ i ] ; if ( ch != null ) { api . appendChild ( vnode . elm , createElm ( ch as VNode , insertedVnodeQueue ) ) ; } } } else { vnode . elm = api . createTextNode ( vnode . text ! ) ; } return vnode . elm ; } function addVnodes ( parentElm , before , vnodes , startIdx , endIdx , insertedVnodeQueue ) { for ( ; startIdx <= endIdx ; ++ startIdx ) { const ch = vnodes [ startIdx ] ; if ( ch != null ) { api . insertBefore ( parentElm , createElm ( ch , insertedVnodeQueue ) , before ) ; } } } function invokeDestroyHook ( vnode ) { const data = vnode . data ; if ( data !== undefined ) { data ?. hook ?. destroy ?. ( vnode ) ; for ( let i = 0 ; i < cbs . destroy . length ; ++ i ) cbs . destroy [ i ] ( vnode ) ; if ( vnode . children !== undefined ) { for ( let j = 0 ; j < vnode . children . length ; ++ j ) { const child = vnode . children [ j ] ; if ( child != null && typeof child !== "string" ) { invokeDestroyHook ( child ) ; } } } } } function removeVnodes ( parentElm , vnodes , startIdx , endIdx ) { for ( ; startIdx <= endIdx ; ++ startIdx ) { let listeners ; let rm ; const ch = vnodes [ startIdx ] ; if ( ch != null ) { if ( isDef ( ch . sel ) ) { invokeDestroyHook ( ch ) ; listeners = cbs . remove . length + 1 ; rm = createRmCb ( ch . elm ! , listeners ) ; for ( let i = 0 ; i < cbs . remove . length ; ++ i ) cbs . remove [ i ] ( ch , rm ) ; const removeHook = ch ?. data ?. hook ?. remove ; if ( isDef ( removeHook ) ) { removeHook ( ch , rm ) ; } else { rm ( ) ; } } else { api . removeChild ( parentElm , ch . elm ! ) ; } } } } function updateChildren ( parentElm , oldCh , newCh , insertedVnodeQueue ) { let oldStartIdx = 0 ; let newStartIdx = 0 ; let oldEndIdx = oldCh . length - 1 ; let oldStartVnode = oldCh [ 0 ] ; let oldEndVnode = oldCh [ oldEndIdx ] ; let newEndIdx = newCh . length - 1 ; let newStartVnode = newCh [ 0 ] ; let newEndVnode = newCh [ newEndIdx ] ; let oldKeyToIdx ; let idxInOld ; let elmToMove ; let before ; while ( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx ) { if ( oldStartVnode == null ) { oldStartVnode = oldCh [ ++ oldStartIdx ] ; } else if ( oldEndVnode == null ) { oldEndVnode = oldCh [ -- oldEndIdx ] ; } else if ( newStartVnode == null ) { newStartVnode = newCh [ ++ newStartIdx ] ; } else if ( newEndVnode == null ) { newEndVnode = newCh [ -- newEndIdx ] ; } else if ( sameVnode ( oldStartVnode , newStartVnode ) ) { patchVnode ( oldStartVnode , newStartVnode , insertedVnodeQueue ) ; oldStartVnode = oldCh [ ++ oldStartIdx ] ; newStartVnode = newCh [ ++ newStartIdx ] ; } else if ( sameVnode ( oldEndVnode , newEndVnode ) ) { patchVnode ( oldEndVnode , newEndVnode , insertedVnodeQueue ) ; oldEndVnode = oldCh [ -- oldEndIdx ] ; newEndVnode = newCh [ -- newEndIdx ] ; } else if ( sameVnode ( oldStartVnode , newEndVnode ) ) { patchVnode ( oldStartVnode , newEndVnode , insertedVnodeQueue ) ; api . insertBefore ( parentElm , oldStartVnode . elm ! , api . nextSibling ( oldEndVnode . elm ! ) ) ; oldStartVnode = oldCh [ ++ oldStartIdx ] ; newEndVnode = newCh [ -- newEndIdx ] ; } else if ( sameVnode ( oldEndVnode , newStartVnode ) ) { patchVnode ( oldEndVnode , newStartVnode , insertedVnodeQueue ) ; api . insertBefore ( parentElm , oldEndVnode . elm ! , oldStartVnode . elm ! ) ; oldEndVnode = oldCh [ -- oldEndIdx ] ; newStartVnode = newCh [ ++ newStartIdx ] ; } else { if ( oldKeyToIdx === undefined ) { oldKeyToIdx = createKeyToOldIdx ( oldCh , oldStartIdx , oldEndIdx ) ; } idxInOld = oldKeyToIdx [ newStartVnode . key as string ] ; if ( isUndef ( idxInOld ) ) { api . insertBefore ( parentElm , createElm ( newStartVnode , insertedVnodeQueue ) , oldStartVnode . elm ! ) ; } else { elmToMove = oldCh [ idxInOld ] ; if ( elmToMove . sel !== newStartVnode . sel ) { api . insertBefore ( parentElm , createElm ( newStartVnode , insertedVnodeQueue ) , oldStartVnode . elm ! ) ; } else { patchVnode ( elmToMove , newStartVnode , insertedVnodeQueue ) ; oldCh [ idxInOld ] = undefined as any ; api . insertBefore ( parentElm , elmToMove . elm ! , oldStartVnode . elm ! ) ; } } newStartVnode = newCh [ ++ newStartIdx ] ; } } if ( newStartIdx <= newEndIdx ) { before = newCh [ newEndIdx + 1 ] == null ? null : newCh [ newEndIdx + 1 ] . elm ; addVnodes ( parentElm , before , newCh , newStartIdx , newEndIdx , insertedVnodeQueue ) ; } if ( oldStartIdx <= oldEndIdx ) { removeVnodes ( parentElm , oldCh , oldStartIdx , oldEndIdx ) ; } } function patchVnode ( oldVnode , vnode , insertedVnodeQueue ) { const hook = vnode . data ?. hook ; hook ?. prepatch ?. ( oldVnode , vnode ) ; const elm = ( vnode . elm = oldVnode . elm ) ! ; const oldCh = oldVnode . children as VNode [ ] ; const ch = vnode . children as VNode [ ] ; if ( oldVnode === vnode ) return ; if ( vnode . data !== undefined ) { for ( let i = 0 ; i < cbs . update . length ; ++ i ) cbs . update [ i ] ( oldVnode , vnode ) ; vnode . data . hook ?. update ?. ( oldVnode , vnode ) ; } if ( isUndef ( vnode . text ) ) { if ( isDef ( oldCh ) && isDef ( ch ) ) { if ( oldCh !== ch ) updateChildren ( elm , oldCh , ch , insertedVnodeQueue ) ; } else if ( isDef ( ch ) ) { if ( isDef ( oldVnode . text ) ) api . setTextContent ( elm , "" ) ; addVnodes ( elm , null , ch , 0 , ch . length - 1 , insertedVnodeQueue ) ; } else if ( isDef ( oldCh ) ) { removeVnodes ( elm , oldCh , 0 , oldCh . length - 1 ) ; } else if ( isDef ( oldVnode . text ) ) { api . setTextContent ( elm , "" ) ; } } else if ( oldVnode . text !== vnode . text ) { if ( isDef ( oldCh ) ) { removeVnodes ( elm , oldCh , 0 , oldCh . length - 1 ) ; } api . setTextContent ( elm , vnode . text ! ) ; } hook ?. postpatch ?. ( oldVnode , vnode ) ; } return function patch ( oldVnode , vnode ) : VNode { let i , elm , parent ; const insertedVnodeQueue = [ ] ; for ( i = 0 ; i < cbs . pre . length ; ++ i ) cbs . pre [ i ] ( ) ; if ( isElement ( api , oldVnode ) ) { oldVnode = emptyNodeAt ( oldVnode ) ; } else if ( isDocumentFragment ( api , oldVnode ) ) { oldVnode = emptyDocumentFragmentAt ( oldVnode ) ; } if ( sameVnode ( oldVnode , vnode ) ) { patchVnode ( oldVnode , vnode , insertedVnodeQueue ) ; } else { elm = oldVnode . elm ! ; parent = api . parentNode ( elm ) as Node ; createElm ( vnode , insertedVnodeQueue ) ; if ( parent !== null ) { api . insertBefore ( parent , vnode . elm ! , api . nextSibling ( elm ) ) ; removeVnodes ( parent , [ oldVnode ] , 0 , 0 ) ; } } for ( i = 0 ; i < insertedVnodeQueue . length ; ++ i ) { insertedVnodeQueue [ i ] . data ! . hook ! . insert ! ( insertedVnodeQueue [ i ] ) ; } for ( i = 0 ; i < cbs . post . length ; ++ i ) cbs . post [ i ] ( ) ; return vnode ; } ; }