import * as React from 'react' ; import { DiagramView } from '../diagram/view' ; import { Element , Link } from '../diagram/elements' ; import { EventObserver , Unsubscribe } from '../viewUtils/events' ; import { PaperWidgetProps } from '../diagram/paperArea' ; import { boundsOf , computePolyline , computePolylineLength , getPointAlongPolyline , Vector , } from '../diagram/geometry' ; import { DraggableHandle } from '../workspace/draggableHandle' ; const DEFAULT_WIDTH = 300 ; const DEFAULT_HEIGHT = 300 ; const MIN_WIDTH = 250 ; const MIN_HEIGHT = 250 ; const MAX_WIDTH = 800 ; const MAX_HEIGHT = 800 ; const ELEMENT_OFFSET = 40 ; const LINK_OFFSET = 20 ; const FOCUS_OFFSET = 20 ; const CLASS_NAME = 'graph-explorer-dialog' ; export interface Props extends PaperWidgetProps { view : DiagramView ; target : Element | Link ; size ? : { width : number ; height : number ; } ; caption ? : string ; offset ? : Vector ; calculatePosition ? : ( ) => Vector ; onClose : ( ) => void ; } export interface State { width ? : number ; height ? : number ; } export class Dialog extends React . Component < Props , State > { private static defaultProps : Partial < Props > = { size : { width : DEFAULT_WIDTH , height : DEFAULT_HEIGHT } , } ; private unsubscribeFromTarget : Unsubscribe | undefined = undefined ; private readonly handler = new EventObserver ( ) ; private updateAll = ( ) => this . forceUpdate ( ) ; constructor ( props ) { super ( props ) ; this . state = { } ; } componentDidMount ( ) { this . listenToTarget ( this . props . target ) ; this . focusOn ( ) ; } componentDidUpdate ( nextProps ) { if ( nextProps . target !== this . props . target ) { this . listenToTarget ( nextProps . target ) ; } } componentWillUnmount ( ) { this . listenToTarget ( undefined ) ; } private listenToTarget ( target ? ) { if ( this . unsubscribeFromTarget ) { this . unsubscribeFromTarget ( ) ; this . unsubscribeFromTarget = undefined ; } if ( target ) { const { view } = this . props ; if ( target instanceof Element ) { this . listenToElement ( target ) ; } else if ( target instanceof Link ) { this . listenToLink ( target ) ; } this . handler . listen ( view . events , 'changeLanguage' , this . updateAll ) ; this . unsubscribeFromTarget = ( ) => { this . handler . stopListening ( ) ; } ; } } private listenToElement ( element ) { this . handler . listen ( element . events , 'changePosition' , this . updateAll ) ; this . handler . listen ( element . events , 'changeSize' , this . updateAll ) ; } private listenToLink ( link ) { const { view } = this . props ; const source = view . model . getElement ( link . sourceId ) ; const target = view . model . getElement ( link . targetId ) ; this . listenToElement ( source ) ; this . listenToElement ( target ) ; this . handler . listen ( link . events , 'changeVertices' , this . updateAll ) ; this . handler . listen ( link . events , 'changeLabelBounds' , this . updateAll ) ; } private calculatePositionForElement ( element ) { const { paperArea , size } = this . props ; const bbox = boundsOf ( element ) ; const { y : y0 } = paperArea . paperToScrollablePaneCoords ( bbox . x , bbox . y ) ; const { x : x1 , y : y1 } = paperArea . paperToScrollablePaneCoords ( bbox . x + bbox . width , bbox . y + bbox . height ) ; return { x : x1 + ELEMENT_OFFSET , y : ( y0 + y1 ) / 2 - size . height / 2 , } ; } private calculatePositionForLink ( link ) { const { view , paperArea } = this . props ; const source = view . model . getElement ( link . sourceId ) ; const target = view . model . getElement ( link . targetId ) ; if ( ! source || ! target ) { throw new Error ( 'Source and target are not specified' ) ; } const route = view . getRouting ( link . id ) ; const verticesDefinedByUser = link . vertices || [ ] ; const vertices = route ? route . vertices : verticesDefinedByUser ; const polyline = computePolyline ( source , target , vertices ) ; const polylineLength = computePolylineLength ( polyline ) ; const targetPoint = getPointAlongPolyline ( polyline , polylineLength / 2 ) ; const { x , y } = paperArea . paperToScrollablePaneCoords ( targetPoint . x , targetPoint . y ) ; return { y : y + LINK_OFFSET , x : x + LINK_OFFSET } ; } private calculatePosition ( ) { const { target , paperArea , offset = { x : 0 , y : 0 } , calculatePosition , } = this . props ; if ( calculatePosition ) { const pos = calculatePosition ( ) ; const { x , y } = paperArea . paperToScrollablePaneCoords ( pos . x , pos . y ) ; return { x : x + offset . x , y : y + offset . y } ; } if ( target instanceof Element ) { return this . calculatePositionForElement ( target ) ; } else if ( target instanceof Link ) { return this . calculatePositionForLink ( target ) ; } throw new Error ( 'Unknown target type' ) ; } private getViewPortScrollablePoints ( ) { const { paperArea } = this . props ; const paperAreaMetrix = paperArea . getAreaMetrics ( ) ; const min = paperArea . clientToScrollablePaneCoords ( 0 , 0 ) ; const max = paperArea . clientToScrollablePaneCoords ( paperAreaMetrix . clientWidth , paperAreaMetrix . clientHeight ) ; return { min , max } ; } private getDialogScrollablePoints ( ) { const { size } = this . props ; const { x , y } = this . calculatePosition ( ) ; const min = { x : x - FOCUS_OFFSET , y : y - FOCUS_OFFSET , } ; const max = { x : min . x + size . width + FOCUS_OFFSET * 2 , y : min . y + size . height + FOCUS_OFFSET * 2 , } ; return { min , max } ; } private focusOn ( ) { const { paperArea } = this . props ; const { min : viewPortMin , max : viewPortMax , } = this . getViewPortScrollablePoints ( ) ; const { min , max } = this . getDialogScrollablePoints ( ) ; let xOffset = 0 ; if ( min . x < viewPortMin . x ) { xOffset = min . x - viewPortMin . x ; } else if ( max . x > viewPortMax . x ) { xOffset = max . x - viewPortMax . x ; } let yOffset = 0 ; if ( min . y < viewPortMin . y ) { yOffset = min . y - viewPortMin . y ; } else if ( max . y > viewPortMax . y ) { yOffset = max . y - viewPortMax . y ; } const curScrollableCenter = { x : viewPortMin . x + ( viewPortMax . x - viewPortMin . x ) / 2 , y : viewPortMin . y + ( viewPortMax . y - viewPortMin . y ) / 2 , } ; const newScrollabalCenter = { x : curScrollableCenter . x + xOffset , y : curScrollableCenter . y + yOffset , } ; const paperCenter = paperArea . scrollablePaneToPaperCoords ( newScrollabalCenter . x , newScrollabalCenter . y ) ; paperArea . centerTo ( paperCenter ) ; } private startSize : Vector ; private onStartDragging = ( e ) => { this . preventSelection ( ) ; const { size } = this . props ; this . startSize = { x : this . state . width || size . width , y : this . state . height || size . height , } ; } ; private calculateHeight ( height ) { const { size } = this . props ; const minHeight = Math . min ( size . height , MIN_HEIGHT ) ; const maxHeight = Math . max ( size . height , MAX_HEIGHT ) ; return Math . max ( minHeight , Math . min ( maxHeight , height ) ) ; } private calculateWidth ( width ) { const { size } = this . props ; const minWidth = Math . min ( size . width , MIN_WIDTH ) ; const maxWidth = Math . max ( size . width , MAX_WIDTH ) ; return Math . max ( minWidth , Math . min ( maxWidth , width ) ) ; } private onDragHandleBottom = ( e , dx , dy ) => { const height = this . calculateHeight ( this . startSize . y + dy ) ; this . setState ( { height } ) ; } ; private onDragHandleRight = ( e , dx ) => { const width = this . calculateWidth ( this . startSize . x + dx ) ; this . setState ( { width } ) ; } ; private onDragHandleBottomRight = ( e , dx , dy ) => { const width = this . calculateWidth ( this . startSize . x + dx ) ; const height = this . calculateHeight ( this . startSize . y + dy ) ; this . setState ( { width , height } ) ; } ; private preventSelection = ( ) => { const onMouseUp = ( ) => { document . body . classList . remove ( 'graph-explorer--unselectable' ) ; document . removeEventListener ( 'mouseup' , onMouseUp ) ; } ; document . addEventListener ( 'mouseup' , onMouseUp ) ; document . body . classList . add ( 'graph-explorer--unselectable' ) ; } ; render ( ) { const { size , caption } = this . props ; const { x , y } = this . calculatePosition ( ) ; const width = this . state . width || size . width ; const height = this . state . height || size . height ; const style = { top : y , left : x , width , height , } ; return ( < div className = { CLASS_NAME } style = { style } > < button className = { `${ CLASS_NAME }__close-button` } onClick = { ( ) => this . props . onClose ( ) } /> { caption ? ( < div className = "graph-explorer-dialog__caption" > { caption } < /div>) : null} { this . props . children } < DraggableHandle className = { `${ CLASS_NAME }__bottom-handle` } onBeginDragHandle = { this . onStartDragging } onDragHandle = { this . onDragHandleBottom } > < /DraggableHandle> < DraggableHandle className = { `${ CLASS_NAME }__right-handle` } onBeginDragHandle = { this . onStartDragging } onDragHandle = { this . onDragHandleRight } > < /DraggableHandle> < DraggableHandle className = { `${ CLASS_NAME }__bottom-right-handle` } onBeginDragHandle = { this . onStartDragging } onDragHandle = { this . onDragHandleBottomRight } > < /DraggableHandle> < /div>); } }