import { init , propsModule , styleModule , eventListenersModule , h , attributesModule , VNode , VNodeData } from 'snabbdom' ; function updateProps ( oldVnode , vnode ) { const element = vnode . elm as any ; let oldProps = oldVnode . data ?. props || { } ; let props = vnode . data ?. props || { } ; for ( const key of ( Object . keys ( oldProps ) ) ) { if ( props [ key ] === undefined ) { props [ key ] = null ; } } if ( element instanceof HTMLInputElement && element === document . activeElement ) { delete vnode ?. data ?. props ?. value ; } } const patch = init ( [ { update : updateProps , create : updateProps } , attributesModule , propsModule , styleModule , eventListenersModule ] ) ; function mapEvent ( event ) { if ( event instanceof KeyboardEvent ) { return { altKey : event . altKey , shiftKey : event . shiftKey , ctrlKey : event . ctrlKey , metaKey : event . metaKey , code : event . code , key : event . key , location : event . location , repeat : event . repeat , } ; } else if ( event instanceof MouseEvent ) { return { altKey : event . altKey , shiftKey : event . shiftKey , ctrlKey : event . ctrlKey , metaKey : event . metaKey , x : event . x , y : event . y , pageX : event . pageX , pageY : event . pageY , screenX : event . screenX , screenY : event . screenY , offsetX : event . offsetX , offsetY : event . offsetY , } ; } else if ( [ 'input' , 'change' ] . includes ( event . type ) ) { const target = event . target as HTMLInputElement ; if ( target . tagName . toLowerCase ( ) === 'select' ) { const selectTarget = event . target as HTMLSelectElement ; const selected = [ ... selectTarget . options ] . filter ( x => x . selected ) . map ( x => x . value ) ; return { value : selectTarget . multiple ? selected : selected [ 0 ] } ; } return target . type === 'checkbox' ? { value : target . checked } : { value : target . value } ; } } const createdElements = { } ; customElements . define ( 'drayman-element' , class extends HTMLElement { componentInstanceId : string ; previouslySerializedTree = [ ] ; events = { } ; get options ( ) { return this . getAttribute ( 'options' ) ; } set options ( value ) { this . setAttribute ( 'options' , JSON . stringify ( value ) ) ; } get component ( ) { return this . getAttribute ( 'component' ) ; } set component ( value ) { this . setAttribute ( 'component' , value ) ; } constructor ( ) { super ( ) ; } static get observedAttributes ( ) { return [ 'options' ] ; } attributeChangedCallback ( attrName , oldValue , newValue ) { if ( attrName === 'options' && window [ 'draymanConfig' ] ) { window [ 'draymanConfig' ] . connection . updateComponentInstanceProps ( { componentInstanceId : this . componentInstanceId , options : this . options } ) ; } } debounce ( eventName , wait , options ) { let timeout ; let toReject ; return ( c , d ) => { return new Promise ( async ( resolve , reject ) => { let later = async ( ) => { timeout = null ; if ( options . trailing ) { resolve ( await this . emit ( eventName , { trailing : true } , c , d ) ) ; } } ; let callNow = options . leading && ! timeout ; clearTimeout ( timeout ) ; if ( toReject ) { toReject ( ) ; } timeout = setTimeout ( later , wait ) ; toReject = reject ; if ( callNow ) { resolve ( await this . emit ( eventName , { leading : true } , c , d ) ) ; } } ) . catch ( ( ) => { } ) ; } ; } getFn ( element , eventName , elementOptions ) { if ( typeof elementOptions === 'object' ) { if ( elementOptions . debounce ) { let wait = typeof elementOptions . debounce === 'number' ? elementOptions . debounce : elementOptions . debounce . wait ; let trailing = typeof elementOptions . debounce === 'number' ? true : ! ! elementOptions . debounce ?. trailing ; let leading = ! ! elementOptions . debounce ?. leading ; const debounced = this . debounce ( eventName , wait , { trailing , leading } ) ; return debounced ; } } return async ( c , d ) => await this . emit ( eventName , null , c , d ) ; } emit = async ( eventName , info = { } , options = { } , files = [ ] ) => { const formData = new FormData ( ) ; formData . append ( 'eventName' , eventName ) ; formData . append ( 'componentInstanceId' , this . componentInstanceId ) ; formData . append ( 'options' , JSON . stringify ( { ... info , ... options } ) ) ; for ( let file of files ) { formData . append ( 'file' , file . file , file . fileName ) ; } return await window [ 'draymanConfig' ] ?. connection . postFormData ( formData ) ; } ; traverseTree ( child ) { if ( window [ 'draymanConfig' ] . elementOptions ) { for ( const [ currentElement , currentElementOption ] of Object . entries ( window [ 'draymanConfig' ] . elementOptions ) ) { if ( child . sel === currentElement ) { if ( child . sel ?. includes ( '-' ) ) { child . data . props = { ... child . data . props , ... ( currentElementOption as any ) } ; } else { const props = { } ; const events = { } ; for ( const option of Object . keys ( currentElementOption ) ) { if ( isEvent ( option ) ) { events [ option . substring ( 2 ) ] = currentElementOption [ option ] ; } else { props [ option ] = currentElementOption [ option ] ; } } child . data . props = { ... child . data . props , ... ( props as any ) } ; child . data . on = { ... child . data . on , ... ( events as any ) } ; } } } } if ( child . sel ?. includes ( '-' ) ) { for ( const option of Object . keys ( child . data . props || { } ) ) { if ( isEvent ( option ) ) { if ( ! this . events [ `${ child . key }/${ option }` ] ) { this . events [ `${ child . key }/${ option }` ] = this . getFn ( child . sel , `${ child . key }/${ option }` , child . data . props [ option ] ) ; } child . data . props [ option ] = this . events [ `${ child . key }/${ option }` ] ; } } if ( ! customElements . get ( child . sel ) && ! createdElements [ child . sel ] ) { createdElements [ child . sel ] = true ; const my_awesome_script = document . createElement ( 'script' ) ; my_awesome_script . setAttribute ( 'src' , `${ window [ 'draymanConfig' ] . elementUrl }${ child . sel }` ) ; document . head . appendChild ( my_awesome_script ) ; } return h ( child . sel , child . data , child . children . map ( x => x . sel ? this . traverseTree ( x ) : x . text ) ) ; } for ( const optionName of Object . keys ( child . data ?. on || { } ) ) { if ( ! this . events [ `${ child . key }/${ optionName }` ] ) { const options = child . data . on [ optionName ] ; this . events [ `${ child . key }/${ optionName }` ] = this . getFn ( child . sel , `${ child . key }/${ optionName }` , options ) ; } child . data . on [ optionName ] = ( x ) => { const files = [ ] ; if ( x . target ?. files ) { for ( const file of x . target . files ) { files . push ( { file : file , fileName : file . name } ) ; } } this . events [ `${ child . key }/${ optionName }` ] ( mapEvent ( x ) , files ) ; } ; } return h ( child . sel , child . data , child . children . map ( x => { return x . sel ? this . traverseTree ( x ) : x . text ; } ) ) ; } once = false ; updateId = 0 ; async connectedCallback ( ) { while ( ! window [ 'draymanConfig' ] || ! this . component ) { console . log ( `waiting for config` ) ; await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ; } const browserCommands = window [ 'draymanConfig' ] . browserCommands ?. ( ( callbackId , data ) => window [ 'draymanConfig' ] ?. connection . handleBrowserCallback ( { callbackId , data , } ) ) || { } ; let rootNode = document . createElement ( 'drayman-element-container' ) as any ; this . appendChild ( rootNode ) ; this . componentInstanceId = await window [ 'draymanConfig' ] . connection . initializeComponent ( { componentId : this . component , componentOptions : this . options , browserCommands : Object . keys ( browserCommands ) , } ) ; window [ 'draymanConfig' ] . connection . onEvent ( this . componentInstanceId , async ( { type , payload } ) => { if ( type === 'view' && payload . updateId > this . updateId ) { this . updateId = payload . updateId ; const newNode = h ( 'drayman-element-container' , { attrs : { componentInstanceId : this . componentInstanceId } } , payload . view . map ( x => this . traverseTree ( x ) ) ) ; patch ( rootNode , newNode ) ; rootNode = newNode ; } else if ( type === 'browserCommand' ) { const { data , callbackId , command , elements } = payload ; let domElements = [ ] ; if ( elements ) { domElements = elements . map ( x => document . querySelector ( `drayman-element-container[componentinstanceid="${ this . componentInstanceId }"] [ref="${ x }"]` ) ) ; } const response = await browserCommands [ command ] ( data , domElements ) ; window [ 'draymanConfig' ] ?. connection . handleBrowserCallback ( { callbackId , data : response } ) ; } } ) ; } disconnectedCallback ( ) { window [ 'draymanConfig' ] ?. connection ?. destroyComponentInstance ( { componentInstanceId : this . componentInstanceId } ) ; } } ) ; export const isEvent = ( optionName ) => optionName ?. length > 2 && optionName . slice ( 0 , 2 ) === 'on' ;