import { RenderBatch , ArrayBuilderSegment , RenderTreeEdit , RenderTreeFrame , EditType , FrameType , ArrayValues } from './RenderBatch/RenderBatch' ; import { EventDelegator } from './EventDelegator' ; import { EventForDotNet , UIEventArgs , EventArgsType } from './EventForDotNet' ; import { LogicalElement , PermutationListEntry , toLogicalElement , insertLogicalChild , removeLogicalChild , getLogicalParent , getLogicalChild , createAndInsertLogicalContainer , isSvgElement , getLogicalChildrenArray , getLogicalSiblingEnd , permuteLogicalChildren , getClosestDomElement } from './LogicalElements' ; import { applyCaptureIdToElement } from './ElementReferenceCapture' ; import { EventFieldInfo } from './EventFieldInfo' ; import { dispatchEvent } from './RendererEventDispatcher' ; import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager' ; const selectValuePropname = '_blazorSelectValue' ; const sharedTemplateElemForParsing = document . createElement ( 'template' ) ; const sharedSvgElemForParsing = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'g' ) ; const preventDefaultEvents = { submit : true } ; const rootComponentsPendingFirstRender = { } ; const internalAttributeNamePrefix = '__internal_' ; const eventPreventDefaultAttributeNamePrefix = 'preventDefault_' ; const eventStopPropagationAttributeNamePrefix = 'stopPropagation_' ; export class BrowserRenderer { private eventDelegator : EventDelegator ; private childComponentLocations : { [ componentId ] : LogicalElement ; } = { } ; private browserRendererId : number ; public constructor ( browserRendererId ) { this . browserRendererId = browserRendererId ; this . eventDelegator = new EventDelegator ( ( event , eventHandlerId , eventArgs , eventFieldInfo ) => { raiseEvent ( event , this . browserRendererId , eventHandlerId , eventArgs , eventFieldInfo ) ; } ) ; attachNavigationManagerToEventDelegator ( this . eventDelegator ) ; } public attachRootComponentToLogicalElement ( componentId , element ) { this . attachComponentToElement ( componentId , element ) ; rootComponentsPendingFirstRender [ componentId ] = element ; } public updateComponent ( batch , componentId , edits , referenceFrames ) { const element = this . childComponentLocations [ componentId ] ; if ( ! element ) { throw new Error ( `No element is currently associated with component ${ componentId }` ) ; } const rootElementToClear = rootComponentsPendingFirstRender [ componentId ] ; if ( rootElementToClear ) { const rootElementToClearEnd = getLogicalSiblingEnd ( rootElementToClear ) ; delete rootComponentsPendingFirstRender [ componentId ] ; if ( ! rootElementToClearEnd ) { clearElement ( rootElementToClear as unknown as Element ) ; } else { clearBetween ( rootElementToClear as unknown as Node , rootElementToClearEnd as unknown as Comment ) ; } } const ownerDocument = getClosestDomElement ( element ) . ownerDocument ; const activeElementBefore = ownerDocument && ownerDocument . activeElement ; this . applyEdits ( batch , componentId , element , 0 , edits , referenceFrames ) ; if ( ( activeElementBefore instanceof HTMLElement ) && ownerDocument && ownerDocument . activeElement !== activeElementBefore ) { activeElementBefore . focus ( ) ; } } public disposeComponent ( componentId ) { delete this . childComponentLocations [ componentId ] ; } public disposeEventHandler ( eventHandlerId ) { this . eventDelegator . removeListener ( eventHandlerId ) ; } private attachComponentToElement ( componentId , element ) { this . childComponentLocations [ componentId ] = element ; } private applyEdits ( batch , componentId , parent , childIndex , edits , referenceFrames ) { let currentDepth = 0 ; let childIndexAtCurrentDepth = childIndex ; let permutationList ; const arrayBuilderSegmentReader = batch . arrayBuilderSegmentReader ; const editReader = batch . editReader ; const frameReader = batch . frameReader ; const editsValues = arrayBuilderSegmentReader . values ( edits ) ; const editsOffset = arrayBuilderSegmentReader . offset ( edits ) ; const editsLength = arrayBuilderSegmentReader . count ( edits ) ; const maxEditIndexExcl = editsOffset + editsLength ; for ( let editIndex = editsOffset ; editIndex < maxEditIndexExcl ; editIndex ++ ) { const edit = batch . diffReader . editsEntry ( editsValues , editIndex ) ; const editType = editReader . editType ( edit ) ; switch ( editType ) { case EditType . prependFrame : { const frameIndex = editReader . newTreeIndex ( edit ) ; const frame = batch . referenceFramesEntry ( referenceFrames , frameIndex ) ; const siblingIndex = editReader . siblingIndex ( edit ) ; this . insertFrame ( batch , componentId , parent , childIndexAtCurrentDepth + siblingIndex , referenceFrames , frame , frameIndex ) ; break ; } case EditType . removeFrame : { const siblingIndex = editReader . siblingIndex ( edit ) ; removeLogicalChild ( parent , childIndexAtCurrentDepth + siblingIndex ) ; break ; } case EditType . setAttribute : { const frameIndex = editReader . newTreeIndex ( edit ) ; const frame = batch . referenceFramesEntry ( referenceFrames , frameIndex ) ; const siblingIndex = editReader . siblingIndex ( edit ) ; const element = getLogicalChild ( parent , childIndexAtCurrentDepth + siblingIndex ) ; if ( element instanceof Element ) { this . applyAttribute ( batch , componentId , element , frame ) ; } else { throw new Error ( 'Cannot set attribute on non-element child' ) ; } break ; } case EditType . removeAttribute : { const siblingIndex = editReader . siblingIndex ( edit ) ; const element = getLogicalChild ( parent , childIndexAtCurrentDepth + siblingIndex ) ; if ( element instanceof HTMLElement ) { const attributeName = editReader . removedAttributeName ( edit ) ! ; if ( ! this . tryApplySpecialProperty ( batch , element , attributeName , null ) ) { element . removeAttribute ( attributeName ) ; } } else { throw new Error ( 'Cannot remove attribute from non-element child' ) ; } break ; } case EditType . updateText : { const frameIndex = editReader . newTreeIndex ( edit ) ; const frame = batch . referenceFramesEntry ( referenceFrames , frameIndex ) ; const siblingIndex = editReader . siblingIndex ( edit ) ; const textNode = getLogicalChild ( parent , childIndexAtCurrentDepth + siblingIndex ) ; if ( textNode instanceof Text ) { textNode . textContent = frameReader . textContent ( frame ) ; } else { throw new Error ( 'Cannot set text content on non-text child' ) ; } break ; } case EditType . updateMarkup : { const frameIndex = editReader . newTreeIndex ( edit ) ; const frame = batch . referenceFramesEntry ( referenceFrames , frameIndex ) ; const siblingIndex = editReader . siblingIndex ( edit ) ; removeLogicalChild ( parent , childIndexAtCurrentDepth + siblingIndex ) ; this . insertMarkup ( batch , parent , childIndexAtCurrentDepth + siblingIndex , frame ) ; break ; } case EditType . stepIn : { const siblingIndex = editReader . siblingIndex ( edit ) ; parent = getLogicalChild ( parent , childIndexAtCurrentDepth + siblingIndex ) ; currentDepth ++ ; childIndexAtCurrentDepth = 0 ; break ; } case EditType . stepOut : { parent = getLogicalParent ( parent ) ! ; currentDepth -- ; childIndexAtCurrentDepth = currentDepth === 0 ? childIndex : 0 ; break ; } case EditType . permutationListEntry : { permutationList = permutationList || [ ] ; permutationList . push ( { fromSiblingIndex : childIndexAtCurrentDepth + editReader . siblingIndex ( edit ) , toSiblingIndex : childIndexAtCurrentDepth + editReader . moveToSiblingIndex ( edit ) , } ) ; break ; } case EditType . permutationListEnd : { permuteLogicalChildren ( parent , permutationList ! ) ; permutationList = undefined ; break ; } default : { const unknownType = editType ; throw new Error ( `Unknown edit type: ${ unknownType }` ) ; } } } } private insertFrame ( batch , componentId , parent , childIndex , frames , frame , frameIndex ) { const frameReader = batch . frameReader ; const frameType = frameReader . frameType ( frame ) ; switch ( frameType ) { case FrameType . element : this . insertElement ( batch , componentId , parent , childIndex , frames , frame , frameIndex ) ; return 1 ; case FrameType . text : this . insertText ( batch , parent , childIndex , frame ) ; return 1 ; case FrameType . attribute : throw new Error ( 'Attribute frames should only be present as leading children of element frames.' ) ; case FrameType . component : this . insertComponent ( batch , parent , childIndex , frame ) ; return 1 ; case FrameType . region : return this . insertFrameRange ( batch , componentId , parent , childIndex , frames , frameIndex + 1 , frameIndex + frameReader . subtreeLength ( frame ) ) ; case FrameType . elementReferenceCapture : if ( parent instanceof Element ) { applyCaptureIdToElement ( parent , frameReader . elementReferenceCaptureId ( frame ) ! ) ; return 0 ; } else { throw new Error ( 'Reference capture frames can only be children of element frames.' ) ; } case FrameType . markup : this . insertMarkup ( batch , parent , childIndex , frame ) ; return 1 ; default : const unknownType = frameType ; throw new Error ( `Unknown frame type: ${ unknownType }` ) ; } } private insertElement ( batch , componentId , parent , childIndex , frames , frame , frameIndex ) { const frameReader = batch . frameReader ; const tagName = frameReader . elementName ( frame ) ! ; const newDomElementRaw = tagName === 'svg' || isSvgElement ( parent ) ? document . createElementNS ( 'http://www.w3.org/2000/svg' , tagName ) : document . createElement ( tagName ) ; const newElement = toLogicalElement ( newDomElementRaw ) ; insertLogicalChild ( newDomElementRaw , parent , childIndex ) ; const descendantsEndIndexExcl = frameIndex + frameReader . subtreeLength ( frame ) ; for ( let descendantIndex = frameIndex + 1 ; descendantIndex < descendantsEndIndexExcl ; descendantIndex ++ ) { const descendantFrame = batch . referenceFramesEntry ( frames , descendantIndex ) ; if ( frameReader . frameType ( descendantFrame ) === FrameType . attribute ) { this . applyAttribute ( batch , componentId , newDomElementRaw , descendantFrame ) ; } else { this . insertFrameRange ( batch , componentId , newElement , 0 , frames , descendantIndex , descendantsEndIndexExcl ) ; break ; } } if ( newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw ) { const selectValue = newDomElementRaw [ selectValuePropname ] ; newDomElementRaw . value = selectValue ; delete newDomElementRaw [ selectValuePropname ] ; } } private insertComponent ( batch , parent , childIndex , frame ) { const containerElement = createAndInsertLogicalContainer ( parent , childIndex ) ; const childComponentId = batch . frameReader . componentId ( frame ) ; this . attachComponentToElement ( childComponentId , containerElement ) ; } private insertText ( batch , parent , childIndex , textFrame ) { const textContent = batch . frameReader . textContent ( textFrame ) ! ; const newTextNode = document . createTextNode ( textContent ) ; insertLogicalChild ( newTextNode , parent , childIndex ) ; } private insertMarkup ( batch , parent , childIndex , markupFrame ) { const markupContainer = createAndInsertLogicalContainer ( parent , childIndex ) ; const markupContent = batch . frameReader . markupContent ( markupFrame ) ; const parsedMarkup = parseMarkup ( markupContent , isSvgElement ( parent ) ) ; let logicalSiblingIndex = 0 ; while ( parsedMarkup . firstChild ) { insertLogicalChild ( parsedMarkup . firstChild , markupContainer , logicalSiblingIndex ++ ) ; } } private applyAttribute ( batch , componentId , toDomElement , attributeFrame ) { const frameReader = batch . frameReader ; const attributeName = frameReader . attributeName ( attributeFrame ) ! ; const eventHandlerId = frameReader . attributeEventHandlerId ( attributeFrame ) ; if ( eventHandlerId ) { const eventName = stripOnPrefix ( attributeName ) ; this . eventDelegator . setListener ( toDomElement , eventName , eventHandlerId , componentId ) ; return ; } if ( ! this . tryApplySpecialProperty ( batch , toDomElement , attributeName , attributeFrame ) ) { toDomElement . setAttribute ( attributeName , frameReader . attributeValue ( attributeFrame ) ! ) ; } } private tryApplySpecialProperty ( batch , element , attributeName , attributeFrame ) { switch ( attributeName ) { case 'value' : return this . tryApplyValueProperty ( batch , element , attributeFrame ) ; case 'checked' : return this . tryApplyCheckedProperty ( batch , element , attributeFrame ) ; default : { if ( attributeName . startsWith ( internalAttributeNamePrefix ) ) { this . applyInternalAttribute ( batch , element , attributeName . substring ( internalAttributeNamePrefix . length ) , attributeFrame ) ; return true ; } return false ; } } } private applyInternalAttribute ( batch , element , internalAttributeName , attributeFrame ) { const attributeValue = attributeFrame ? batch . frameReader . attributeValue ( attributeFrame ) : null ; if ( internalAttributeName . startsWith ( eventStopPropagationAttributeNamePrefix ) ) { const eventName = stripOnPrefix ( internalAttributeName . substring ( eventStopPropagationAttributeNamePrefix . length ) ) ; this . eventDelegator . setStopPropagation ( element , eventName , attributeValue !== null ) ; } else if ( internalAttributeName . startsWith ( eventPreventDefaultAttributeNamePrefix ) ) { const eventName = stripOnPrefix ( internalAttributeName . substring ( eventPreventDefaultAttributeNamePrefix . length ) ) ; this . eventDelegator . setPreventDefault ( element , eventName , attributeValue !== null ) ; } else { throw new Error ( `Unsupported internal attribute '${ internalAttributeName }'` ) ; } } private tryApplyValueProperty ( batch , element , attributeFrame ) { const frameReader = batch . frameReader ; if ( element . tagName === 'INPUT' && element . getAttribute ( 'type' ) === 'time' && ! element . getAttribute ( 'step' ) ) { const timeValue = attributeFrame ? frameReader . attributeValue ( attributeFrame ) : null ; if ( timeValue ) { element [ 'value' ] = timeValue . substring ( 0 , 5 ) ; return true ; } } switch ( element . tagName ) { case 'INPUT' : case 'SELECT' : case 'TEXTAREA' : { const value = attributeFrame ? frameReader . attributeValue ( attributeFrame ) : null ; ( element as any ) . value = value ; if ( element . tagName === 'SELECT' ) { element [ selectValuePropname ] = value ; } return true ; } case 'OPTION' : { const value = attributeFrame ? frameReader . attributeValue ( attributeFrame ) : null ; if ( value ) { element . setAttribute ( 'value' , value ) ; } else { element . removeAttribute ( 'value' ) ; } const selectElem = this . findClosestAncestorSelectElement ( element ) ; if ( selectElem && ( selectValuePropname in selectElem ) && selectElem [ selectValuePropname ] === value ) { this . tryApplyValueProperty ( batch , selectElem , attributeFrame ) ; delete selectElem [ selectValuePropname ] ; } return true ; } default : return false ; } } private tryApplyCheckedProperty ( batch , element , attributeFrame ) { if ( element . tagName === 'INPUT' ) { const value = attributeFrame ? batch . frameReader . attributeValue ( attributeFrame ) : null ; ( element as any ) . checked = value !== null ; return true ; } else { return false ; } } private findClosestAncestorSelectElement ( element ) { while ( element ) { if ( element instanceof HTMLSelectElement ) { return element ; } else { element = element . parentElement ; } } return null ; } private insertFrameRange ( batch , componentId , parent , childIndex , frames , startIndex , endIndexExcl ) { const origChildIndex = childIndex ; for ( let index = startIndex ; index < endIndexExcl ; index ++ ) { const frame = batch . referenceFramesEntry ( frames , index ) ; const numChildrenInserted = this . insertFrame ( batch , componentId , parent , childIndex , frames , frame , index ) ; childIndex += numChildrenInserted ; index += countDescendantFrames ( batch , frame ) ; } return ( childIndex - origChildIndex ) ; } } export interface ComponentDescriptor { start : Node ; end : Node ; } export interface EventDescriptor { browserRendererId : number ; eventHandlerId : number ; eventArgsType : EventArgsType ; eventFieldInfo : EventFieldInfo | null ; } function parseMarkup ( markup , isSvg ) { if ( isSvg ) { sharedSvgElemForParsing . innerHTML = markup || ' ' ; return sharedSvgElemForParsing ; } else { sharedTemplateElemForParsing . innerHTML = markup || ' ' ; return sharedTemplateElemForParsing . content ; } } function countDescendantFrames ( batch , frame ) { const frameReader = batch . frameReader ; switch ( frameReader . frameType ( frame ) ) { case FrameType . component : case FrameType . element : case FrameType . region : return frameReader . subtreeLength ( frame ) - 1 ; default : return 0 ; } } function raiseEvent ( event , browserRendererId , eventHandlerId , eventArgs , eventFieldInfo ) { if ( preventDefaultEvents [ event . type ] ) { event . preventDefault ( ) ; } const eventDescriptor = { browserRendererId , eventHandlerId , eventArgsType : eventArgs . type , eventFieldInfo : eventFieldInfo , } ; dispatchEvent ( eventDescriptor , eventArgs . data ) ; } function clearElement ( element ) { let childNode ; while ( childNode = element . firstChild ) { element . removeChild ( childNode ) ; } } function clearBetween ( start , end ) { const logicalParent = getLogicalParent ( start as unknown as LogicalElement ) ; if ( ! logicalParent ) { throw new Error ( "Can't clear between nodes. The start node does not have a logical parent." ) ; } const children = getLogicalChildrenArray ( logicalParent ) ; const removeStart = children . indexOf ( start as unknown as LogicalElement ) + 1 ; const endIndex = children . indexOf ( end as unknown as LogicalElement ) ; for ( let i = removeStart ; i <= endIndex ; i ++ ) { removeLogicalChild ( logicalParent , removeStart ) ; } start . textContent = '!' ; } function stripOnPrefix ( attributeName ) { if ( attributeName . startsWith ( 'on' ) ) { return attributeName . substring ( 2 ) ; } throw new Error ( `Attribute should be an event name, but doesn't start with 'on'. Value: '${ attributeName }'` ) ; }