import * as d3 from "d3" ; import Map from "../map" ; import Utils from "../../utils/utils" ; import Node from "../models/node" ; import { Path } from "d3-path" ; /**
 * Draw the map and update it.
 */ export default class Draw { private map : Map ; param { Map } map constructor ( map ) { this . map = map ; } /**
     * Create svg and main css map properties.
     */ public create ( ) { this . map . dom . container = d3 . select ( "#" + this . map . id ) . style ( "position" , "relative" ) ; this . map . dom . svg = this . map . dom . container . append ( "svg" ) . style ( "position" , "absolute" ) . style ( "width" , "100%" ) . style ( "height" , "100%" ) . style ( "top" , 0 ) . style ( "left" , 0 ) ; this . map . dom . svg . append ( "rect" ) . attr ( "width" , "100%" ) . attr ( "height" , "100%" ) . attr ( "fill" , "white" ) . attr ( "pointer-events" , "all" ) . on ( "click" , ( ) => { this . map . nodes . deselectNode ( ) ; } ) ; this . map . dom . g = this . map . dom . svg . append ( "g" ) ; } /**
     * Update the dom of the map with the (new) nodes.
     */ public update ( ) { let nodes = this . map . nodes . getNodes ( ) , dom = { nodes : this . map . dom . g . selectAll ( "." + this . map . id + "_node" ) . data ( nodes ) , branches : this . map . dom . g . selectAll ( "." + this . map . id + "_branch" ) . data ( nodes . slice ( 1 ) ) } ; let tapedTwice = false ; let outer = dom . nodes . enter ( ) . append ( "g" ) . style ( "cursor" , "pointer" ) . attr ( "class" , this . map . id + "_node" ) . attr ( "id" , function ( node ) { node . dom = this ; return node . id ; } ) . attr ( "transform" , ( node ) => "translate(" + node . coordinates . x + "," + node . coordinates . y + ")" ) . on ( "dblclick" , ( node ) => { d3 . event . stopPropagation ( ) ; this . enableNodeNameEditing ( node ) ; } ) . on ( 'touchstart' , ( node ) => { if ( ! tapedTwice ) { tapedTwice = true ; setTimeout ( function ( ) { tapedTwice = false ; } , 300 ) ; return false ; } this . enableNodeNameEditing ( node ) ; } ) ; if ( this . map . options . drag === true ) { outer . call ( this . map . drag . getDragBehavior ( ) ) ; } else { outer . on ( "mousedown" , ( node ) => { this . map . nodes . selectNode ( node . id ) ; } ) ; } outer . insert ( "foreignObject" ) . html ( ( node ) => this . createNodeNameDOM ( node ) ) . each ( ( node ) => { this . updateNodeNameContainer ( node ) ; } ) ; outer . insert ( "path" , "foreignObject" ) . style ( "fill" , ( node ) => node . colors . background ) . style ( "stroke-width" , 3 ) . attr ( "d" , ( node ) => this . drawNodeBackground ( node ) ) ; outer . each ( ( node ) => { this . setImage ( node ) ; } ) ; dom . branches . enter ( ) . insert ( "path" , "g" ) . style ( "fill" , ( node ) => node . colors . branch ) . style ( "stroke" , ( node ) => node . colors . branch ) . attr ( "class" , this . map . id + "_branch" ) . attr ( "id" , ( node ) => node . id + "_branch" ) . attr ( "d" , ( node ) => this . drawBranch ( node ) ) ; dom . nodes . exit ( ) . remove ( ) ; dom . branches . exit ( ) . remove ( ) ; } /**
     * Remove all nodes and branches of the map.
     */ public clear ( ) { d3 . selectAll ( "." + this . map . id + "_node, ." + this . map . id + "_branch" ) . remove ( ) ; } param { Node } node returns { Path } public drawNodeBackground ( node ) { let name = node . getNameDOM ( ) , path = d3 . path ( ) ; node . dimensions . width = name . clientWidth + 45 ; node . dimensions . height = name . clientHeight + 30 ; let x = node . dimensions . width / 2 , y = node . dimensions . height / 2 , k = node . k ; path . moveTo ( - x , k / 3 ) ; path . bezierCurveTo ( - x , - y + 10 , - x + 10 , - y , k , - y ) ; path . bezierCurveTo ( x - 10 , - y , x , - y + 10 , x , k / 3 ) ; path . bezierCurveTo ( x , y - 10 , x - 10 , y , k , y ) ; path . bezierCurveTo ( - x + 10 , y , - x , y - 10 , - x , k / 3 ) ; path . closePath ( ) ; return path ; } param { Node } node returns { Path } public drawBranch ( node ) { let parent = node . parent , path = d3 . path ( ) , level = node . getLevel ( ) , width = 22 - ( level < 6 ? level : 6 ) * 3 , mx = ( parent . coordinates . x + node . coordinates . x ) / 2 , ory = parent . coordinates . y < node . coordinates . y + node . dimensions . height / 2 ? - 1 : 1 , orx = parent . coordinates . x > node . coordinates . x ? - 1 : 1 , inv = orx * ory ; path . moveTo ( parent . coordinates . x , parent . coordinates . y - width * .8 ) ; path . bezierCurveTo ( mx - width * inv , parent . coordinates . y - width / 2 , parent . coordinates . x - width / 2 * inv , node . coordinates . y + node . dimensions . height / 2 - width / 3 , node . coordinates . x - node . dimensions . width / 3 * orx , node . coordinates . y + node . dimensions . height / 2 + 3 ) ; path . bezierCurveTo ( parent . coordinates . x + width / 2 * inv , node . coordinates . y + node . dimensions . height / 2 + width / 3 , mx + width * inv , parent . coordinates . y + width / 2 , parent . coordinates . x , parent . coordinates . y + width * .8 ) ; path . closePath ( ) ; return path ; } param { Node } node public updateNodeShapes ( node ) { let background = node . getBackgroundDOM ( ) ; d3 . select ( background ) . attr ( "d" , ( node ) => < any > this . drawNodeBackground ( node ) ) ; d3 . selectAll ( "." + this . map . id + "_branch" ) . attr ( "d" , ( node ) => < any > this . drawBranch ( node ) ) ; this . updateImagePosition ( node ) ; this . updateNodeNameContainer ( node ) ; } param { Node } node public setImage ( node ) { let domImage = node . getImageDOM ( ) ; if ( ! domImage ) { domImage = document . createElementNS ( "http://www.w3.org/2000/svg" , "image" ) ; node . dom . appendChild ( domImage ) ; } if ( node . image . src !== "" ) { let image = new Image ( ) ; image . src = node . image . src ; image . onload = function ( ) { let h = node . image . size , w = ( < any > this ) . width * h / ( < any > this ) . height , y = - ( h + node . dimensions . height / 2 + 5 ) , x = - w / 2 ; domImage . setAttribute ( "href" , node . image . src ) ; domImage . setAttribute ( "height" , h . toString ( ) ) ; domImage . setAttribute ( "width" , w . toString ( ) ) ; domImage . setAttribute ( "y" , y . toString ( ) ) ; domImage . setAttribute ( "x" , x . toString ( ) ) ; } ; image . onerror = function ( ) { domImage . remove ( ) ; node . image . src = "" ; } ; } else { domImage . remove ( ) ; } } param { Node } node public updateImagePosition ( node ) { if ( node . image . src !== "" ) { let image = node . getImageDOM ( ) , y = - ( ( < any > image ) . getBBox ( ) . height + node . dimensions . height / 2 + 5 ) ; image . setAttribute ( "y" , y . toString ( ) ) ; } } param { Node } node public enableNodeNameEditing ( node ) { let name = node . getNameDOM ( ) ; Utils . focusWithCaretAtEnd ( name ) ; name . style . setProperty ( "cursor" , "auto" ) ; name . ondblclick = name . onmousedown = ( event ) => { event . stopPropagation ( ) ; } ; name . oninput = ( ) => { this . updateNodeShapes ( node ) ; } ; name . onkeydown = ( event ) => { if ( event . code === 'Escape' ) { Utils . removeAllRanges ( ) ; name . blur ( ) ; } if ( event . ctrlKey || event . metaKey ) { switch ( event . code ) { case 'KeyA' : case 'KeyC' : case 'KeyV' : case 'KeyX' : case 'KeyZ' : case 'ArrowLeft' : case 'ArrowRight' : case 'ArrowUp' : case 'ArrowDown' : case 'Backspace' : case 'Delete' : return true ; default : return false ; } } switch ( event . code ) { case 'Tab' : return false ; default : return true ; } } ; name . onpaste = ( event ) => { event . preventDefault ( ) ; let text = event . clipboardData . getData ( "text/plain" ) ; document . execCommand ( "insertHTML" , false , text ) ; } ; name . onblur = ( ) => { name . innerHTML = name . innerHTML === "<br>" ? "" : name . innerHTML ; if ( name . innerHTML !== node . name ) { this . map . nodes . updateNode ( "name" , name . innerHTML ) ; } name . ondblclick = name . onmousedown = name . onblur = name . onkeydown = name . oninput = name . onpaste = null ; name . style . setProperty ( "cursor" , "pointer" ) ; name . blur ( ) ; } ; } param { Node } node private updateNodeNameContainer ( node ) { let name = node . getNameDOM ( ) , foreignObject = < SVGForeignObjectElement > name . parentNode ; if ( name . offsetWidth !== 0 ) { foreignObject . setAttribute ( "x" , ( - name . clientWidth / 2 ) . toString ( ) ) ; foreignObject . setAttribute ( "y" , ( - name . clientHeight / 2 ) . toString ( ) ) ; foreignObject . setAttribute ( "width" , name . clientWidth . toString ( ) ) ; foreignObject . setAttribute ( "height" , name . clientHeight . toString ( ) ) ; } } param { Node } node returns { string } private createNodeNameDOM ( node ) { let div = document . createElement ( "div" ) ; div . style . setProperty ( "font-size" , node . font . size . toString ( ) + "px" ) ; div . style . setProperty ( "color" , node . colors . name ) ; div . style . setProperty ( "font-style" , node . font . style ) ; div . style . setProperty ( "font-weight" , node . font . weight ) ; div . style . setProperty ( "text-decoration" , node . font . decoration ) ; div . style . setProperty ( "display" , "inline-block" ) ; div . style . setProperty ( "white-space" , "pre" ) ; div . style . setProperty ( "font-family" , this . map . options . fontFamily ) ; div . style . setProperty ( "text-align" , "center" ) ; div . setAttribute ( "contenteditable" , "true" ) ; div . innerHTML = node . name ; return div . outerHTML ; } }