import * as React from 'react' ; import { findDOMNode } from 'react-dom' ; import { hcl } from 'd3-color' ; import { Property , ElementTypeIri , PropertyTypeIri } from '../data/model' ; import { TemplateProps } from '../customization/props' ; import { Debouncer } from '../viewUtils/async' ; import { EventObserver } from '../viewUtils/events' ; import { PropTypes } from '../viewUtils/react' ; import { KeyedObserver , observeElementTypes , observeProperties } from '../viewUtils/keyedObserver' ; import { setElementExpanded } from './commands' ; import { Element } from './elements' ; import { DiagramView , RenderingLayer , IriClickIntent } from './view' ; export interface Props { view : DiagramView ; group ? : string ; style : React . CSSProperties ; } interface State { readonly elementStates ? : ReadonlyMap < string , ElementState > ; } interface ElementState { element : Element ; templateProps : TemplateProps ; blurred : boolean ; } enum RedrawFlags { None = 0 , Render = 1 , RecomputeTemplate = 1 | 2 , RecomputeBlurred = 1 | 4 } interface RedrawBatch { requests : Map < string , RedrawFlags > ; forAll : RedrawFlags ; } interface SizeUpdateRequest { element : Element ; node : HTMLDivElement ; } export class ElementLayer extends React . Component < Props , State > { private readonly listener = new EventObserver ( ) ; private redrawBatch : RedrawBatch = { requests : new Map < string , RedrawFlags > ( ) , forAll : RedrawFlags . None , } ; private delatedRedraw = new Debouncer ( ) ; private sizeRequests = new Map < string , SizeUpdateRequest > ( ) ; private delayedUpdateSizes = new Debouncer ( ) ; private layer : HTMLDivElement ; constructor ( props , context ) { super ( props , context ) ; const { view , group } = this . props ; this . state = { elementStates : applyRedrawRequests ( view , group , this . redrawBatch , new Map < string , ElementState > ( ) ) } ; } render ( ) { const { view , style } = this . props ; const { elementStates } = this . state ; const elementsToRender = [ ] ; for ( const { id } of view . model . elements ) { const state = elementStates . get ( id ) ; if ( state ) { elementsToRender . push ( state ) ; } } return < div className = 'ontodia-element-layer' ref = { this . onMount } style = { style } > { elementsToRender . map ( state => { const overlayElement = ( < OverlayedElement key = { state . element . id } state = { state } view = { view } onInvalidate = { this . requestRedraw } onResize = { this . requestSizeUpdate } / > ) ; const elementDecorator = view . _decorateElement ( state . element ) ; if ( elementDecorator ) { return ( < div key = { state . element . id } > { overlayElement } { elementDecorator } < /div>); } return overlayElement ; } ) } < /div>; } private onMount = ( layer ) => { this . layer = layer ; } ; componentDidMount ( ) { const { view } = this . props ; this . listener . listen ( view . model . events , 'changeCells' , e => { if ( e . updateAll ) { this . requestRedrawAll ( RedrawFlags . None ) ; } else { if ( e . changedElement ) { this . requestRedraw ( e . changedElement , RedrawFlags . None ) ; } } } ) ; this . listener . listen ( view . events , 'changeLanguage' , ( ) => { this . requestRedrawAll ( RedrawFlags . RecomputeTemplate ) ; } ) ; this . listener . listen ( view . events , 'changeHighlight' , ( ) => { this . requestRedrawAll ( RedrawFlags . RecomputeBlurred ) ; } ) ; this . listener . listen ( view . model . events , 'elementEvent' , ( { data } ) => { const invalidatesTemplate = data . changeData || data . changeExpanded || data . changeElementState ; if ( invalidatesTemplate ) { this . requestRedraw ( invalidatesTemplate . source , RedrawFlags . RecomputeTemplate ) ; } const invalidatesRender = data . changePosition || data . requestedRedraw ; if ( invalidatesRender ) { this . requestRedraw ( invalidatesRender . source , RedrawFlags . Render ) ; } } ) ; this . listener . listen ( view . events , 'syncUpdate' , ( { layer } ) => { if ( layer === RenderingLayer . Element ) { this . delatedRedraw . runSynchronously ( ) ; } else if ( layer === RenderingLayer . ElementSize ) { this . delayedUpdateSizes . runSynchronously ( ) ; } } ) ; } componentWillReceiveProps ( nextProps ) { if ( this . props . group !== nextProps . group ) { this . setState ( ( state ) : State => ( { elementStates : applyRedrawRequests ( nextProps . view , nextProps . group , this . redrawBatch , state . elementStates ) } ) ) ; } } componentWillUnmount ( ) { this . listener . stopListening ( ) ; this . delatedRedraw . dispose ( ) ; this . delayedUpdateSizes . dispose ( ) ; } private requestRedraw = ( element , request ) => { const flagsWithForAll = this . redrawBatch . forAll | request ; if ( flagsWithForAll === this . redrawBatch . forAll ) { return ; } const existing = this . redrawBatch . requests . get ( element . id ) ; this . redrawBatch . requests . set ( element . id , existing | request ) ; this . delatedRedraw . call ( this . redrawElements ) ; } ; private requestRedrawAll ( request ) { this . redrawBatch . forAll |= request ; this . delatedRedraw . call ( this . redrawElements ) ; } private redrawElements = ( ) => { const props = this . props ; this . setState ( ( state ) : State => ( { elementStates : applyRedrawRequests ( props . view , props . group , this . redrawBatch , state . elementStates ) } ) ) ; } ; private requestSizeUpdate = ( element , node ) => { this . sizeRequests . set ( element . id , { element , node } ) ; this . delayedUpdateSizes . call ( this . recomputeQueuedSizes ) ; } ; private recomputeQueuedSizes = ( ) => { const batch = this . sizeRequests ; this . sizeRequests = new Map < string , SizeUpdateRequest > ( ) ; batch . forEach ( ( { element , node } ) => { const { clientWidth , clientHeight } = node ; element . setSize ( { width : clientWidth , height : clientHeight } ) ; } ) ; } ; } function applyRedrawRequests ( view , targetGroup , batch , previous ) { const computed = new Map < string , ElementState > ( ) ; for ( const element of view . model . elements ) { if ( element . group !== targetGroup ) { continue ; } const elementId = element . id ; if ( previous . has ( elementId ) ) { let state = previous . get ( elementId ) ; const request = ( batch . requests . get ( elementId ) || RedrawFlags . None ) | batch . forAll ; if ( request & RedrawFlags . Render ) { state = { element , templateProps : ( request & RedrawFlags . RecomputeTemplate ) === RedrawFlags . RecomputeTemplate ? computeTemplateProps ( state . element , view ) : state . templateProps , blurred : ( request & RedrawFlags . RecomputeBlurred ) === RedrawFlags . RecomputeBlurred ? computeIsBlurred ( state . element , view ) : state . blurred , } ; } computed . set ( elementId , state ) ; batch . requests . delete ( elementId ) ; } else { computed . set ( element . id , { element , templateProps : computeTemplateProps ( element , view ) , blurred : computeIsBlurred ( element , view ) , } ) ; } } batch . forAll = RedrawFlags . None ; return computed ; } interface OverlayedElementProps { state : ElementState ; view : DiagramView ; onInvalidate : ( model : Element , request : RedrawFlags ) => void ; onResize : ( model : Element , node : HTMLDivElement ) => void ; } export interface ElementContextWrapper { ontodiaElement : ElementContext ; } export const ElementContextTypes = { ontodiaElement : PropTypes . anything , } ; export interface ElementContext { element : Element ; } class OverlayedElement extends React . Component < OverlayedElementProps , { } > { static childContextTypes = ElementContextTypes ; private readonly listener = new EventObserver ( ) ; private disposed = false ; private typesObserver : KeyedObserver < ElementTypeIri > ; private propertiesObserver : KeyedObserver < PropertyTypeIri > ; getChildContext ( ) { const ontodiaElement = { element : this . props . state . element , } ; return { ontodiaElement } ; } private rerenderTemplate = ( ) => { if ( this . disposed ) { return ; } this . props . onInvalidate ( this . props . state . element , RedrawFlags . RecomputeTemplate ) ; } ; render ( ) { const { state : { element , blurred } } = this . props ; if ( element . temporary ) { return < div />; } const { x = 0 , y = 0 } = element . position ; const transform = `translate(${ x }px,${ y }px)` ; const className = ( `ontodia-overlayed-element ${ blurred ? 'ontodia-overlayed-element--blurred' : '' }` ) ; return < div className = { className } data - element - id = { element . id } style = { { position : 'absolute' , transform } } tabIndex = { 0 } ref = { this . onMount } onLoad = { this . onLoadOrErrorEvent } onError = { this . onLoadOrErrorEvent } onClick = { this . onClick } onDoubleClick = { this . onDoubleClick } > < TemplatedElement { ... this . props } / > < /div>; } private onMount = ( node ) => { if ( ! node ) { return ; } const { state , onResize } = this . props ; onResize ( state . element , node ) ; } ; private onLoadOrErrorEvent = ( ) => { const { state , onResize } = this . props ; onResize ( state . element , findDOMNode ( this ) as HTMLDivElement ) ; } ; private onClick = ( e ) => { if ( e . target instanceof HTMLElement && e . target . localName === 'a' ) { const anchor = e . target as HTMLAnchorElement ; const { view , state } = this . props ; const clickIntent = e . target . getAttribute ( 'data-iri-click-intent' ) === IriClickIntent . OpenEntityIri ? IriClickIntent . OpenEntityIri : IriClickIntent . OpenOtherIri ; view . onIriClick ( decodeURI ( anchor . href ) , state . element , clickIntent , e ) ; } } ; private onDoubleClick = ( e ) => { e . preventDefault ( ) ; e . stopPropagation ( ) ; const { view , state : { element } } = this . props ; view . model . history . execute ( setElementExpanded ( element , ! element . isExpanded ) ) ; } ; componentDidMount ( ) { const { state , view } = this . props ; this . listener . listen ( state . element . events , 'requestedFocus' , ( ) => { const element = findDOMNode ( this ) as HTMLElement ; if ( element ) { element . focus ( ) ; } } ) ; this . typesObserver = observeElementTypes ( view . model , 'changeLabel' , this . rerenderTemplate ) ; this . propertiesObserver = observeProperties ( view . model , 'changeLabel' , this . rerenderTemplate ) ; this . observeTypes ( ) ; } componentWillUnmount ( ) { this . listener . stopListening ( ) ; this . typesObserver . stopListening ( ) ; this . propertiesObserver . stopListening ( ) ; this . disposed = true ; } shouldComponentUpdate ( nextProps ) { return this . props . state !== nextProps . state ; } componentDidUpdate ( ) { this . observeTypes ( ) ; this . props . onResize ( this . props . state . element , findDOMNode ( this ) as HTMLDivElement ) ; } private observeTypes ( ) { const { state : { element } } = this . props ; this . typesObserver . observe ( element . data . types ) ; this . propertiesObserver . observe ( Object . keys ( element . data . properties ) as PropertyTypeIri [ ] ) ; } } class TemplatedElement extends React . Component < OverlayedElementProps , { } > { private cachedTemplateClass : React . ComponentClass < TemplateProps > ; private cachedTemplateProps : TemplateProps ; render ( ) { const { state , view } = this . props ; const { element , templateProps } = state ; const templateClass = view . getElementTemplate ( element . data . types ) ; this . cachedTemplateClass = templateClass ; this . cachedTemplateProps = templateProps ; return React . createElement ( templateClass , templateProps ) ; } shouldComponentUpdate ( nextProps ) { const templateClass = nextProps . view . getElementTemplate ( nextProps . state . element . data . types ) ; return ! ( this . cachedTemplateClass === templateClass && this . cachedTemplateProps === nextProps . state . templateProps ) ; } } function computeTemplateProps ( model , view ) { const types = model . data . types . length > 0 ? view . getElementTypeString ( model . data ) : 'Thing' ; const label = view . formatLabel ( model . data . label . values , model . iri ) ; const { color , icon } = computeStyleFor ( model , view ) ; const propsAsList = computePropertyTable ( model , view ) ; return { elementId : model . id , data : model . data , iri : model . iri , types , label , color , iconUrl : icon , imgUrl : model . data . image , isExpanded : model . isExpanded , props : model . data . properties , propsAsList , } ; } function computePropertyTable ( model , view ) { if ( ! model . data . properties ) { return [ ] ; } const propertyIris = Object . keys ( model . data . properties ) as PropertyTypeIri [ ] ; const propTable = propertyIris . map ( key => { const property = view . model . createProperty ( key ) ; const name = view . formatLabel ( property . label , key ) ; return { id : key , name : name , property : model . data . properties [ key ] , } ; } ) ; propTable . sort ( ( a , b ) => { const aLabel = ( a . name || a . id ) . toLowerCase ( ) ; const bLabel = ( b . name || b . id ) . toLowerCase ( ) ; return aLabel . localeCompare ( bLabel ) ; } ) ; return propTable ; } function computeStyleFor ( model , view ) { const { color : { h , c , l } , icon } = view . getTypeStyle ( model . data . types ) ; return { icon , color : hcl ( h , c , l ) . toString ( ) , } ; } function computeIsBlurred ( element , view ) { return view . highlighter && ! view . highlighter ( element ) ; }