import { EventForDotNet , UIEventArgs } from './EventForDotNet' ; import { EventFieldInfo } from './EventFieldInfo' ; const nonBubblingEvents = toLookup ( [ 'abort' , 'blur' , 'change' , 'error' , 'focus' , 'load' , 'loadend' , 'loadstart' , 'mouseenter' , 'mouseleave' , 'progress' , 'reset' , 'scroll' , 'submit' , 'unload' , 'DOMNodeInsertedIntoDocument' , 'DOMNodeRemovedFromDocument' , ] ) ; const disableableEventNames = toLookup ( [ 'click' , 'dblclick' , 'mousedown' , 'mousemove' , 'mouseup' ] ) ; export interface OnEventCallback { ( event , eventHandlerId , eventArgs , eventFieldInfo ) : void ; } export class EventDelegator { private static nextEventDelegatorId = 0 ; private readonly eventsCollectionKey : string ; private readonly afterClickCallbacks : ( ( event : MouseEvent ) => void ) [ ] = [ ] ; private eventInfoStore : EventInfoStore ; constructor ( private onEvent ) { const eventDelegatorId = ++ EventDelegator . nextEventDelegatorId ; this . eventsCollectionKey = `_blazorEvents_${ eventDelegatorId }` ; this . eventInfoStore = new EventInfoStore ( this . onGlobalEvent . bind ( this ) ) ; } public setListener ( element , eventName , eventHandlerId , renderingComponentId ) { const infoForElement = this . getEventHandlerInfosForElement ( element , true ) ! ; const existingHandler = infoForElement . getHandler ( eventName ) ; if ( existingHandler ) { this . eventInfoStore . update ( existingHandler . eventHandlerId , eventHandlerId ) ; } else { const newInfo = { element , eventName , eventHandlerId , renderingComponentId } ; this . eventInfoStore . add ( newInfo ) ; infoForElement . setHandler ( eventName , newInfo ) ; } } public removeListener ( eventHandlerId ) { const info = this . eventInfoStore . remove ( eventHandlerId ) ; if ( info ) { const element = info . element ; const elementEventInfos = this . getEventHandlerInfosForElement ( element , false ) ; if ( elementEventInfos ) { elementEventInfos . removeHandler ( info . eventName ) ; } } } public notifyAfterClick ( callback ) { this . afterClickCallbacks . push ( callback ) ; this . eventInfoStore . addGlobalListener ( 'click' ) ; } public setStopPropagation ( element , eventName , value ) { const infoForElement = this . getEventHandlerInfosForElement ( element , true ) ! ; infoForElement . stopPropagation ( eventName , value ) ; } public setPreventDefault ( element , eventName , value ) { const infoForElement = this . getEventHandlerInfosForElement ( element , true ) ! ; infoForElement . preventDefault ( eventName , value ) ; } private onGlobalEvent ( evt ) { if ( ! ( evt . target instanceof Element ) ) { return ; } let candidateElement = evt . target as Element | null ; let eventArgs = null ; const eventIsNonBubbling = nonBubblingEvents . hasOwnProperty ( evt . type ) ; let stopPropagationWasRequested = false ; while ( candidateElement ) { const handlerInfos = this . getEventHandlerInfosForElement ( candidateElement , false ) ; if ( handlerInfos ) { const handlerInfo = handlerInfos . getHandler ( evt . type ) ; if ( handlerInfo && ! eventIsDisabledOnElement ( candidateElement , evt . type ) ) { if ( ! eventArgs ) { eventArgs = EventForDotNet . fromDOMEvent ( evt ) ; } const eventFieldInfo = EventFieldInfo . fromEvent ( handlerInfo . renderingComponentId , evt ) ; this . onEvent ( evt , handlerInfo . eventHandlerId , eventArgs , eventFieldInfo ) ; } if ( handlerInfos . stopPropagation ( evt . type ) ) { stopPropagationWasRequested = true ; } if ( handlerInfos . preventDefault ( evt . type ) ) { evt . preventDefault ( ) ; } } candidateElement = ( eventIsNonBubbling || stopPropagationWasRequested ) ? null : candidateElement . parentElement ; } if ( evt . type === 'click' ) { this . afterClickCallbacks . forEach ( callback => callback ( evt as MouseEvent ) ) ; } } private getEventHandlerInfosForElement ( element , createIfNeeded ) { if ( element . hasOwnProperty ( this . eventsCollectionKey ) ) { return element [ this . eventsCollectionKey ] ; } else if ( createIfNeeded ) { return ( element [ this . eventsCollectionKey ] = new EventHandlerInfosForElement ( ) ) ; } else { return null ; } } } class EventInfoStore { private infosByEventHandlerId : { [ eventHandlerId ] : EventHandlerInfo ; } = { } ; private countByEventName : { [ eventName ] : number ; } = { } ; constructor ( private globalListener ) { } public add ( info ) { if ( this . infosByEventHandlerId [ info . eventHandlerId ] ) { throw new Error ( `Event ${ info . eventHandlerId } is already tracked` ) ; } this . infosByEventHandlerId [ info . eventHandlerId ] = info ; this . addGlobalListener ( info . eventName ) ; } public addGlobalListener ( eventName ) { if ( this . countByEventName . hasOwnProperty ( eventName ) ) { this . countByEventName [ eventName ] ++ ; } else { this . countByEventName [ eventName ] = 1 ; const useCapture = nonBubblingEvents . hasOwnProperty ( eventName ) ; document . addEventListener ( eventName , this . globalListener , useCapture ) ; } } public update ( oldEventHandlerId , newEventHandlerId ) { if ( this . infosByEventHandlerId . hasOwnProperty ( newEventHandlerId ) ) { throw new Error ( `Event ${ newEventHandlerId } is already tracked` ) ; } const info = this . infosByEventHandlerId [ oldEventHandlerId ] ; delete this . infosByEventHandlerId [ oldEventHandlerId ] ; info . eventHandlerId = newEventHandlerId ; this . infosByEventHandlerId [ newEventHandlerId ] = info ; } public remove ( eventHandlerId ) { const info = this . infosByEventHandlerId [ eventHandlerId ] ; if ( info ) { delete this . infosByEventHandlerId [ eventHandlerId ] ; const eventName = info . eventName ; if ( -- this . countByEventName [ eventName ] === 0 ) { delete this . countByEventName [ eventName ] ; document . removeEventListener ( eventName , this . globalListener ) ; } } return info ; } } class EventHandlerInfosForElement { private handlers : { [ eventName ] : EventHandlerInfo ; } = { } ; private preventDefaultFlags : { [ eventName ] : boolean ; } | null = null ; private stopPropagationFlags : { [ eventName ] : boolean ; } | null = null ; public getHandler ( eventName ) { return this . handlers . hasOwnProperty ( eventName ) ? this . handlers [ eventName ] : null ; } public setHandler ( eventName , handler ) { this . handlers [ eventName ] = handler ; } public removeHandler ( eventName ) { delete this . handlers [ eventName ] ; } public preventDefault ( eventName , setValue ? ) { if ( setValue !== undefined ) { this . preventDefaultFlags = this . preventDefaultFlags || { } ; this . preventDefaultFlags [ eventName ] = setValue ; } return this . preventDefaultFlags ? this . preventDefaultFlags [ eventName ] : false ; } public stopPropagation ( eventName , setValue ? ) { if ( setValue !== undefined ) { this . stopPropagationFlags = this . stopPropagationFlags || { } ; this . stopPropagationFlags [ eventName ] = setValue ; } return this . stopPropagationFlags ? this . stopPropagationFlags [ eventName ] : false ; } } interface EventHandlerInfo { element : Element ; eventName : string ; eventHandlerId : number ; renderingComponentId : number ; } function toLookup ( items ) { const result = { } ; items . forEach ( value => { result [ value ] = true ; } ) ; return result ; } function eventIsDisabledOnElement ( element , eventName ) { return ( element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement ) && disableableEventNames . hasOwnProperty ( eventName ) && element . disabled ; }