import * as O from "./observable/observables" ; import { SVG_TAGS } from "./special-casing" ; export type HarmajaComponent = ( props : HarmajaProps ) => HarmajaOutput ; export type JSXElementType = string | HarmajaComponent ; export type HarmajaProps = Record < string , any > ; export type HarmajaChild = HarmajaObservableChild | DOMNode | string | number | null ; export type HarmajaChildren = ( HarmajaChild | HarmajaChildren ) [ ] ; export type HarmajaChildOrChildren = HarmajaChild | HarmajaChildren ; export interface HarmajaObservableChild extends O . Property < HarmajaChildOrChildren > { } export type HarmajaStaticOutput = DOMNode | DOMNode [ ] ; export type HarmajaOutput = DOMNode | HarmajaDynamicOutput | HarmajaOutput [ ] ; export interface HarmajaDynamicOutput extends O . Property < HarmajaOutput > { } export type DOMNode = ChildNode ; export type DomElementType < K extends keyof JSX . IntrinsicElements > = JSX . IntrinsicElements [ K ] extends JSX . DetailedHTMLProps < any , infer H > ? H : JSX . IntrinsicElements [ K ] extends JSX . SVGProps < infer S > ? S : never ; export type RefType < K extends keyof JSX . IntrinsicElements > = DomElementType < K > | null ; let transientStateStack = [ ] ; type ContextMap = Map < Context < any > , any > ; type ContextFn = ( e : DOMNode ) => void ; type TransientState = { mountCallbacks : Callback [ ] ; mountE : O . EventStream < void > | undefined ; unmountCallbacks : Callback [ ] ; unmountE : O . EventStream < void > | undefined ; scope : O . Scope | undefined ; mountsController : NodeController | undefined ; contextFns : ContextFn [ ] ; } ; const EMPTY_OBJECT = Object . freeze ( { } ) ; const EMPTY_ARRAY = [ ] ; Object . freeze ( EMPTY_ARRAY ) ; function emptyTransientState ( ) { return { mountCallbacks : EMPTY_ARRAY as Callback [ ] , mountE : undefined , unmountCallbacks : EMPTY_ARRAY as Callback [ ] , unmountE : undefined , scope : undefined , mountsController : undefined , contextFns : EMPTY_ARRAY as ContextFn [ ] , } ; } /**
 *  Element constructor used by JSX.
 */ export function createElement ( type , props ? , ... children ) { const flattenedChildren = children . flatMap ( flattenChildren ) ; if ( ! props ) { props = EMPTY_OBJECT ; } else if ( props . children ) { delete props . children ; } if ( typeof type == "function" ) { const constructor = type as HarmajaComponent ; transientStateStack . push ( emptyTransientState ( ) ) ; const result = constructor ( { ... props , children : flattenedChildren } ) ; const transientState = transientStateStack . pop ( ) ! ; if ( O . isProperty ( result ) ) { if ( transientState . contextFns . length > 0 ) { throw Error ( "setContext/onContext supported only for components that return a single static element (not a Property)" ) ; } return createController ( [ placeholders . create ( ) ] , composeControllers ( handleMounts ( transientState ) , startUpdatingNodes ( result as HarmajaObservableChild ) ) ) ; } else if ( transientState . unmountCallbacks . length > 0 || transientState . mountCallbacks . length > 0 || transientState . contextFns . length > 0 || transientState . scope ) { if ( Array . isArray ( result ) && transientState . contextFns . length > 0 ) { throw Error ( "setContext/onContext supported only for components that return a single static element (not a Fragment)" ) ; } return createController ( toDOMNodes ( render ( result ) ) , handleMounts ( transientState ) ) ; } else { return result ; } } else if ( typeof type == "string" ) { return renderElement ( type , props , flattenedChildren ) ; } else { console . error ( "Unexpected createElement call with arguments" , arguments ) ; throw Error ( `Cannot create element of type ${ type }` ) ; } } function composeControllers ( c1 , c2 ) { return ( controller ) => { const unsub1 = c1 ( controller ) ; const unsub2 = c2 ( controller ) ; return ( ) => { unsub2 ( ) ; unsub1 ( ) ; } ; } ; } const handleMounts = ( transientState ) => ( controller ) => { if ( transientState . scope ) { transientState . mountsController = controller ; } transientState . contextFns . forEach ( ( fn ) => fn ( controller . currentElements [ 0 ] ) ) ; for ( const callback of transientState . mountCallbacks ) { callback ( ) ; } return ( ) => { for ( const callback of transientState . unmountCallbacks ) { callback ( ) ; } } ; } ; export function Fragment ( { children , } ) { return children . flatMap ( flattenChildren ) . flatMap ( render ) ; } function flattenChildren ( child ) { if ( child instanceof Array ) return child . flatMap ( flattenChildren ) ; return [ child ] ; } function renderElement ( type , props , children ) { const el = SVG_TAGS . has ( type ) ? document . createElementNS ( "http://www.w3.org/2000/svg" , type ) : document . createElement ( type ) ; let contentEditable = false ; const props_ = props || EMPTY_OBJECT ; for ( let key of Object . keys ( props_ ) ) { const value = props_ [ key ] ; if ( key === "ref" ) { setRefProp ( el , key , value ) ; continue ; } if ( key === "contentEditable" && value !== false && value !== "false" ) { contentEditable = true ; } if ( O . isProperty ( value ) ) { attachOnMount ( el , ( ) => { let previousValue = undefined ; const unsub = O . forEach ( value , ( nextValue ) => { setProp ( el , key , nextValue , previousValue ) ; previousValue = nextValue ; } ) ; attachOnUnmount ( el , unsub ) ; } ) ; } else { setProp ( el , key , value , undefined ) ; } } if ( contentEditable ) { addContentEditableController ( el as HTMLElement , children ) ; } else { ; ( children || EMPTY_ARRAY ) . map ( render ) . flatMap ( toDOMNodes ) . forEach ( ( node ) => el . appendChild ( node ) ) ; } return el ; } function addContentEditableController ( el , children ) { if ( ! children || children . length == 0 ) { return ; } if ( children . length != 1 ) { throw Error ( "contentEditable elements expected to contain zero to one child" ) ; } const child = children [ 0 ] ; if ( ! O . isProperty ( child ) ) { throw Error ( "contentEditable element must have an Observable<string> as child" ) ; } const observable = child as O . Property < string > ; createController ( [ el ] , ( controller ) : Callback => { return O . forEach ( observable , ( nextValue ) => { if ( typeof nextValue !== "string" ) { throw Error ( `Value for contentEditable is not string: ${ nextValue } is a ${ typeof nextValue }.` ) ; } if ( nextValue !== el . textContent ) { el . textContent = nextValue ; } } ) ; } ) ; } const placeholders = ( function ( ) { let counter = 1 ; return { create : ( ) : Text => { const placeholder = document . createTextNode ( "" ) ; ( placeholder as any ) . _h_id = counter ++ ; return placeholder ; } , isPlaceholder : ( node ) : boolean => ( node as any ) . _h_id !== undefined , getId : ( node ) : number | undefined => ( node as any ) . _h_id , } ; } ) ( ) ; function render ( child ) { if ( child instanceof Array ) { return child . flatMap ( render ) ; } if ( typeof child === "string" || typeof child === "number" ) { return document . createTextNode ( child . toString ( ) ) ; } if ( child === null || child === false ) { return placeholders . create ( ) ; } if ( O . isProperty ( child ) ) { return createController ( [ placeholders . create ( ) ] , startUpdatingNodes ( child as HarmajaObservableChild ) ) ; } if ( isDOMElement ( child ) ) { return child ; } throw Error ( child + " is not a valid element" ) ; } const startUpdatingNodes = ( observable ) => ( controller ) : Callback => { return O . forEach ( observable , ( nextChildren ) => { let oldElements = controller . currentElements . slice ( ) ; let newNodes = flattenChildren ( nextChildren ) . flatMap ( render ) . flatMap ( toDOMNodes ) ; if ( newNodes . length === 0 ) { newNodes = [ placeholders . create ( ) ] ; } replaceAll ( controller , oldElements , newNodes ) ; } ) ; } ; function isDOMElement ( child ) { return child instanceof Element || child instanceof Text ; } function setRefProp ( el , key , value ) { if ( O . isAtom ( value ) ) { O . set ( value , null ) ; attachOnMount ( el , ( ) => O . set ( value , el ) ) ; attachOnUnmount ( el , ( ) => O . set ( value , null ) ) ; } else if ( typeof value === "function" ) { const refFn = value as Function ; attachOnMount ( el , ( ) => refFn ( el ) ) ; } else { throw Error ( "Expecting ref prop to be an atom or a function, got " + value ) ; } return ; } function setProp ( el , key , value , previousValue ) { if ( key . startsWith ( "on" ) ) { key = key . toLowerCase ( ) ; key = key === "ondoubleclick" ? "ondblclick" : key ; ( el as any ) [ key ] = value ; } else if ( key === "style" ) { setStyleProp ( el as HTMLElement , value , previousValue ) ; } else if ( key === "className" ) { el . setAttribute ( "class" , value ) ; } else if ( el . namespaceURI === "http://www.w3.org/2000/svg" ) { el . setAttribute ( key , value ) ; } else if ( key in el ) { ; ( el as any ) [ key ] = value ; } else { el . setAttribute ( key , value ) ; } } type StyleProp = Record < string , string > | undefined ; function setStyleProp ( el , value , oldValue ) { if ( oldValue ) { for ( let name in oldValue ) { if ( ! ( value && name in value ) ) { setStyle ( el . style , name , "" ) ; } } } if ( value ) { for ( let name in value ) { if ( ! oldValue || value [ name ] !== oldValue [ name ] ) { setStyle ( el . style , name , value [ name ] ) ; } } } } function setStyle ( style , key , value ) { if ( key [ 0 ] === "-" ) { style . setProperty ( key , value ) ; } else if ( value === null || value === undefined ) { style [ key as any ] = "" ; } else { style [ key as any ] = value ; } } function getTransientState ( forMethod ) { if ( transientStateStack . length === 0 ) { throw Error ( `Illegal ${ forMethod } call outside component constructor call` ) ; } return transientStateStack [ transientStateStack . length - 1 ] ; } export type Callback = ( ) => void ; type NodeState = { mounted : boolean ; unmounted : boolean ; onUnmounts : Callback [ ] ; onMounts : Callback [ ] ; controllers : NodeController [ ] ; contextMap : ContextMap | undefined ; } ; export type NodeController = { unsub ? : Callback ; currentElements : DOMNode [ ] ; } & NodeControllerOptions ; type NodeControllerOptions = { onReplace ? : ( oldNodes : DOMNode [ ] , newNodes : DOMNode [ ] ) => void ; } ; type NodeControllerFn = ( controller : NodeController ) => Callback ; const nodeState = ( function ( ) { return { getIfExists ( node ) { return ( node as any ) . __h ; } , getOrInstantiate ( node ) { let currentNodeState = ( node as any ) . __h ; if ( currentNodeState === undefined ) { currentNodeState = { mounted : false , unmounted : false , onMounts : EMPTY_ARRAY , onUnmounts : EMPTY_ARRAY , controllers : EMPTY_ARRAY , contextMap : undefined , } ; ( node as any ) . __h = currentNodeState ; } return currentNodeState ; } , } ; } ) ( ) ; /**
 *  Mounts the given element to the document, replacing the given root element.
 *
 *  - Causes the component to be activated, i.e. to start listening to observables
 *  - `onMount` callbacks will be called
 *  - `onMountEvent` will be triggered
 */ export function mount ( harmajaElement , root ) { const rendered = render ( harmajaElement ) ; replaceAll ( null , [ root ] , rendered ) ; return rendered ; } /**
 *  Unmounts the given element, removing it from the DOM.
 *
 *  - Causes the component to be deactivated, i.e. to stop listening to observables
 *  - `onUnmount` callbacks will be called
 *  - `onUnmountEvent` will be triggered
 */ export function unmount ( harmajaElement ) { if ( O . isProperty ( harmajaElement ) ) { unmount ( O . get ( harmajaElement ) ) ; } else if ( ( harmajaElement as any ) instanceof Array ) { ; ( harmajaElement as Array < any > ) . forEach ( unmount ) ; } else { removeNode ( null , 0 , harmajaElement ) ; } } /**
 *  Add onMount callback. Called once after the component has been mounted on the document.
 *  NOTE: Call only in component constructors. Otherwise will not do anything useful.
 */ export function onMount ( callback ) { const transientState = getTransientState ( "onMount" ) ; if ( transientState . mountCallbacks === EMPTY_ARRAY ) transientState . mountCallbacks = [ ] ; transientState . mountCallbacks . push ( callback ) ; } /**
 *  Add onUnmount callback. Called once after the component has been unmounted from the document.
 *  NOTE: Call only in component constructors. Otherwise will not do anything useful.
 */ export function onUnmount ( callback ) { const transientState = getTransientState ( "onUnmount" ) ; if ( transientState . unmountCallbacks === EMPTY_ARRAY ) transientState . unmountCallbacks = [ ] ; transientState . unmountCallbacks . push ( callback ) ; } /**
 *  The onMount event as EventStream, emitting a value after the component has been mounted to the document.
 *  NOTE: Call only in component constructors. Otherwise will not do anything useful.
 */ export function mountEvent ( ) { const transientState = getTransientState ( "mountEvent" ) ; if ( ! transientState . mountE ) { const event = O . bus < void > ( ) ; onMount ( ( ) => { O . pushAndEnd ( event , undefined ) ; } ) ; transientState . mountE = event ; } return transientState . mountE ! as O . NativeEventStream < void > ; } /**
 *  The onUnmount event as EventStream, emitting a value after the component has been unmounted from the document.
 *  NOTE: Call only in component constructors. Otherwise will not do anything useful.
 */ export function unmountEvent ( ) { const transientState = getTransientState ( "unmountEvent" ) ; if ( ! transientState . unmountE ) { const event = O . bus < void > ( ) ; onUnmount ( ( ) => { O . pushAndEnd ( event , undefined ) ; } ) ; transientState . unmountE = event ; } return transientState . unmountE ! as O . NativeEventStream < void > ; } export function componentScope ( ) { const transientState = getTransientState ( "unmountEvent" ) ; if ( ! transientState . scope ) { const unmountE = unmountEvent ( ) ; const mountE = mountEvent ( ) ; transientState . scope = O . mkScope ( ( onIn ) => { let unsub = null ; O . forEach ( unmountE , ( ) => { if ( unsub ) unsub ( ) ; } ) ; if ( transientState . mountsController ) { const state = nodeState . getOrInstantiate ( transientState . mountsController . currentElements [ 0 ] ) ; if ( state . mounted ) { unsub = onIn ( ) ; return ; } } O . forEach ( mountE , ( ) => { unsub = onIn ( ) ; } ) ; } ) ; } return transientState . scope ; } export function callOnMounts ( element ) { let state = nodeState . getOrInstantiate ( element ) ; if ( state . mounted ) { return ; } if ( state . unmounted ) { throw new Error ( "Component re-mount not supported" ) ; } state . mounted = true ; for ( const sub of state . onMounts ) { sub ( ) ; } for ( const child of element . childNodes ) { callOnMounts ( child ) ; } } function callOnUnmounts ( element ) { let state = nodeState . getOrInstantiate ( element ) ; if ( ! state . mounted ) { return ; } for ( const unsub of state . onUnmounts ) { unsub ( ) ; } for ( const child of element . childNodes ) { callOnUnmounts ( child ) ; } state . mounted = false ; state . unmounted = true ; } function attachOnMount ( element , onMount ) { if ( typeof onMount !== "function" ) { throw Error ( "not a function: " + onMount ) ; } let state = nodeState . getOrInstantiate ( element ) ; if ( state . onMounts === EMPTY_ARRAY ) { state . onMounts = [ ] ; } state . onMounts . push ( onMount ) ; } function attachOnUnmount ( element , onUnmount ) { if ( typeof onUnmount !== "function" ) { throw Error ( "not a function: " + onUnmount ) ; } let state = nodeState . getOrInstantiate ( element ) ; if ( state . onUnmounts === EMPTY_ARRAY ) { state . onUnmounts = [ ] ; } if ( state . onUnmounts . includes ( onUnmount ) ) { return ; } state . onUnmounts . push ( onUnmount ) ; } function detachOnUnmount ( element , onUnmount ) { let state = nodeState . getIfExists ( element ) ; if ( state === undefined || state . onUnmounts === EMPTY_ARRAY ) { return ; } for ( let i = 0 ; i < state . onUnmounts . length ; i ++ ) { if ( state . onUnmounts [ i ] === onUnmount ) { state . onUnmounts . splice ( i , 1 ) ; return ; } } } function detachController ( oldElements , controller ) { for ( const el of oldElements ) { const state = nodeState . getOrInstantiate ( el ) ; const index = state . controllers . indexOf ( controller ) ; if ( index === - 1 ) { throw Error ( "Controller not attached to " + el ) ; } if ( controller . unsub ) detachOnUnmount ( el , controller . unsub ) ; } } function createController ( elements , bootstrap , options ? ) { const controller = { ... options , currentElements : elements , } ; attachController ( elements , controller , bootstrap ) ; return elements ; } function attachController ( elements , controller , bootstrap ? , skipAttachControllerUnsub ? ) { for ( let i = 0 ; i < elements . length ; i ++ ) { let el = elements [ i ] ; const state = nodeState . getOrInstantiate ( el ) ; if ( state . controllers === EMPTY_ARRAY ) { state . controllers = [ controller ] ; } else if ( state . controllers . includes ( controller ) ) { } else { state . controllers . push ( controller ) ; } if ( i == 0 ) { if ( bootstrap ) { if ( state . mounted ) { throw Error ( "Unexpected: Component already mounted" ) ; } else { attachOnMount ( el , ( ) => { const unsub = bootstrap ( controller ) ; controller . unsub = unsub ; el = controller . currentElements [ 0 ] ; attachOnUnmount ( el , controller . unsub ) ; } ) ; } } if ( controller . unsub && ! skipAttachControllerUnsub ) { attachOnUnmount ( el , controller . unsub ) ; } } } } function replacedExternally ( controller , oldNodes , newNodes ) { let firstIndex = - 1 ; let lastIndex = - 1 ; if ( oldNodes . length === 0 ) throw Error ( "Empty list of nodes" ) ; for ( const oldNode of oldNodes ) { const index = controller . currentElements . indexOf ( oldNode ) ; if ( index < 0 ) { throw Error ( `Element not found: ${ debug ( oldNode ) }` ) ; } if ( lastIndex >= 0 && index != lastIndex + 1 ) { throw Error ( "Non-consecutive nodes " + oldNodes ) ; } if ( firstIndex < 0 ) { firstIndex = index ; } lastIndex = index ; } if ( firstIndex < 0 || lastIndex < 0 ) throw Error ( "Assertion failed" ) ; if ( controller . onReplace ) { controller . onReplace ( oldNodes , newNodes ) ; } detachController ( oldNodes , controller ) ; controller . currentElements = [ ... controller . currentElements . slice ( 0 , firstIndex ) , ... newNodes , ... controller . currentElements . slice ( lastIndex + 1 ) , ] ; attachController ( controller . currentElements , controller ) ; } function replacedByController ( controller , oldNodes , newNodes ) { if ( ! controller ) return ; const controllers = nodeState . getOrInstantiate ( oldNodes [ 0 ] ) . controllers ; const index = controllers . indexOf ( controller ) ; const parentControllers = controllers . slice ( index + 1 ) ; for ( let i = 1 ; i < oldNodes . length ; i ++ ) { const controllersHere = nodeState . getOrInstantiate ( oldNodes [ i ] ) . controllers ; const indexHere = controllersHere . indexOf ( controller ) ; if ( indexHere < 0 ) { throw new Error ( `Controller ${ controller } not found in ${ debug ( oldNodes [ i ] ) }` ) ; } const parentControllersHere = controllersHere . slice ( indexHere + 1 ) ; if ( ! arrayEq ( parentControllers , parentControllersHere ) ) { throw new Error ( `Assertion failed: controller array of ${ debug ( oldNodes [ i ] ) } (${ controllersHere }, replacer index ${ indexHere }) not equal to that of ${ debug ( oldNodes [ 0 ] ) } (${ controllers }).` ) ; } } for ( let parentController of parentControllers ) { replacedExternally ( parentController , oldNodes , newNodes ) ; } } function appendedByController ( controller , cursorNode , newNode ) { if ( ! controller ) return ; const controllers = nodeState . getOrInstantiate ( cursorNode ) . controllers ; const index = controllers . indexOf ( controller ) ; if ( index < 0 ) { throw new Error ( `Controller ${ controller } not found in ${ debug ( cursorNode ) }` ) ; } const parentControllers = controllers . slice ( index + 1 ) ; for ( let parentController of parentControllers ) { const indexForCursor = parentController . currentElements . indexOf ( cursorNode ) ; if ( indexForCursor < 0 ) { throw new Error ( `Element ${ debug ( cursorNode ) } not found in node list of Controller ${ parentController } not found in ` ) ; } parentController . currentElements . splice ( indexForCursor + 1 , 0 , newNode ) ; attachController ( [ newNode ] , parentController ) ; } } function arrayEq < A > ( xs , ys ) { if ( xs . length !== ys . length ) return false ; for ( let i = 0 ; i < xs . length ; i ++ ) { if ( xs [ i ] !== ys [ i ] ) return false ; } return true ; } function replaceNode ( controller , index , newNode ) { const oldNode = controller . currentElements [ index ] ; controller . currentElements [ index ] = newNode ; detachController ( [ oldNode ] , controller ) ; attachController ( [ newNode ] , controller ) ; let wasMounted = nodeState . getIfExists ( oldNode ) ?. mounted ; if ( wasMounted ) { callOnUnmounts ( oldNode ) ; } if ( ! oldNode . parentElement ) { return ; } oldNode . parentElement . replaceChild ( newNode , oldNode ) ; replacedByController ( controller , [ oldNode ] , [ newNode ] ) ; if ( wasMounted ) { callOnMounts ( newNode ) ; } } function identicalNodes ( oldNodes , newNodes ) { if ( oldNodes . length !== newNodes . length ) { return false ; } for ( let i = 0 ; i < oldNodes . length ; i ++ ) { if ( ! ( oldNodes [ i ] instanceof Text && newNodes [ i ] instanceof Text && oldNodes [ i ] . textContent && oldNodes [ i ] . textContent === newNodes [ i ] . textContent ) ) { return false ; } } return true ; } function replaceAll ( controller , oldContent , newContent ) { const oldNodes = toDOMNodes ( oldContent ) ; const newNodes = toDOMNodes ( newContent ) ; if ( identicalNodes ( oldNodes , newNodes ) ) { return ; } if ( controller ) { controller . currentElements = newNodes ; detachController ( oldNodes , controller ) ; attachController ( newNodes , controller ) ; } if ( oldNodes . length === 0 ) throw new Error ( "Cannot replace zero nodes" ) ; if ( newNodes . length === 0 ) throw new Error ( "Cannot replace with zero nodes" ) ; oldNodes [ 0 ] . parentElement ! . replaceChild ( newNodes [ 0 ] , oldNodes [ 0 ] ) ; for ( let i = 1 ; i < oldNodes . length ; i ++ ) { oldNodes [ i ] . remove ( ) ; } for ( let i = 1 ; i < newNodes . length ; i ++ ) { newNodes [ i - 1 ] . after ( newNodes [ i ] ) ; } replacedByController ( controller , oldNodes , newNodes ) ; for ( let node of oldNodes ) { callOnUnmounts ( node ) ; } for ( let node of newNodes ) { callOnMounts ( node ) ; } } function replaceAllInPlace ( controller , oldNodes , newNodes , createdNodes , deletedNodes ) { if ( oldNodes . length === 0 ) throw new Error ( "Cannot replace zero nodes" ) ; if ( newNodes . length === 0 ) throw new Error ( "Cannot replace with zero nodes" ) ; const oldFirst = controller . currentElements [ 0 ] ; controller . currentElements = newNodes ; detachController ( deletedNodes , controller ) ; attachController ( createdNodes , controller , undefined , true ) ; if ( controller . unsub ) { if ( deletedNodes . includes ( oldFirst ) ) { attachOnUnmount ( newNodes [ 0 ] , controller . unsub ) ; } else if ( newNodes [ 0 ] !== oldNodes [ 0 ] ) { detachOnUnmount ( oldNodes [ 0 ] , controller . unsub ) ; attachOnUnmount ( newNodes [ 0 ] , controller . unsub ) ; } } if ( newNodes [ 0 ] !== oldNodes [ 0 ] ) { oldNodes [ 0 ] . parentElement ?. replaceChild ( newNodes [ 0 ] , oldNodes [ 0 ] ) ; } deletedNodes . forEach ( ( node ) => node . remove ( ) ) ; for ( let i = 1 ; i < newNodes . length ; i ++ ) { const thisOne = newNodes [ i - 1 ] ; const nextOne = newNodes [ i ] ; if ( thisOne . nextSibling !== nextOne ) { thisOne . after ( nextOne ) ; } } replacedByController ( controller , oldNodes , newNodes ) ; deletedNodes . forEach ( callOnUnmounts ) ; createdNodes . forEach ( callOnMounts ) ; } function appendNode ( controller , next ) { const lastNode = controller . currentElements [ controller . currentElements . length - 1 ] ; controller . currentElements . push ( next ) ; attachController ( [ next ] , controller ) ; lastNode . after ( next ) ; appendedByController ( controller , lastNode , next ) ; callOnMounts ( next ) ; } function toDOMNodes ( elements ) { if ( elements instanceof Array ) return elements . flatMap ( toDOMNodes ) ; return [ elements ] ; } function removeNode ( controller , index , oldNode ) { if ( oldNode instanceof Array ) { if ( controller ) throw Error ( "Only single node supported with controller option" ) ; for ( const node of oldNode ) { removeNode ( controller , index , node ) ; } } else { if ( controller ) { controller . currentElements . splice ( index , 1 ) ; detachController ( [ oldNode ] , controller ) ; } callOnUnmounts ( oldNode ) ; oldNode . remove ( ) ; replacedByController ( controller , [ oldNode ] , [ ] ) ; } } declare const contextBrand ; export type Context < T > = { [ contextBrand ] : true ; name : string ; } ; export function createContext < T > ( name ) { return { name , } as Context < T > ; } const nop = ( ) => { } ; export function setContext < T > ( ctx , value ) { const transientState = getTransientState ( "setContext" ) ; if ( transientState . contextFns === EMPTY_ARRAY ) transientState . contextFns = [ ] ; transientState . contextFns . push ( ( el ) => { const ns = nodeState . getOrInstantiate ( el ) ; if ( ! ns . contextMap ) ns . contextMap = new Map ( ) ; if ( ns . contextMap . has ( ctx ) ) { throw Error ( `Context ${ ctx . name } already set` ) ; } ns . contextMap . set ( ctx , value ) ; return nop ; } ) ; } export function onContext < T > ( ctx , callback ) { const transientState = getTransientState ( "setContext" ) ; if ( transientState . contextFns === EMPTY_ARRAY ) transientState . contextFns = [ ] ; function getFrom ( el ) { const ns = nodeState . getIfExists ( el ) ; if ( ns && ns . contextMap && ns . contextMap . has ( ctx ) ) { const value = ns . contextMap . get ( ctx ) ; callback ( value ) ; return nop ; } if ( el . parentNode ) { return getFrom ( el . parentNode ) ; } throw Error ( `Context value ${ ctx . name } not set` ) ; } transientState . contextFns . push ( getFrom ) ; } export function debug ( element ) { if ( element instanceof Array ) { return element . map ( debug ) . join ( "," ) || "[]" ; } else if ( element instanceof Element ) { return element . outerHTML ; } else { return ( element . textContent || ( placeholders . isPlaceholder ( element ) ? `<placeholder ${ placeholders . getId ( element ) }>` : "<empty text node>" ) ) ; } } export const LowLevelApi = { createPlaceholder : placeholders . create , attachOnMount , attachOnUnmount , createController , removeNode , appendNode , replaceNode , replaceAll , replaceAllInPlace , toDOMNodes , render , } ;