1 // TODO take container as argument,c reate drawing area dynamically. remove on 2 // clear();, recreate on init() 3 4 /** 5 * Creates a new CanvasView. This is the base class for all canvas view 6 * implementations. 7 * 8 * @constructor 9 */ 10 mindmaps.CanvasView = function() { 11 /** 12 * Returns the element that used to draw the map upon. 13 * 14 * @returns {jQuery} 15 */ 16 this.$getDrawingArea = function() { 17 return $("#drawing-area"); 18 }; 19 20 /** 21 * Returns the element that contains the drawing area. 22 * 23 * @returns {jQuery} 24 */ 25 this.$getContainer = function() { 26 return $("#canvas-container"); 27 }; 28 29 /** 30 * Scrolls the container to show the center of the drawing area. 31 */ 32 this.center = function() { 33 var c = this.$getContainer(); 34 var area = this.$getDrawingArea(); 35 var w = area.width() - c.width(); 36 var h = area.height() - c.height(); 37 this.scroll(w / 2, h / 2); 38 }; 39 40 /** 41 * Scrolls the container. 42 * 43 * @param {Number} x 44 * @param {Number} y 45 */ 46 this.scroll = function(x, y) { 47 var c = this.$getContainer(); 48 c.scrollLeft(x).scrollTop(y); 49 }; 50 51 /** 52 * Changes the size of the drawing area to match with with the new zoom 53 * factor and scrolls the container to adjust the view port. 54 */ 55 this.applyViewZoom = function() { 56 var delta = this.zoomFactorDelta; 57 // console.log(delta); 58 59 var c = this.$getContainer(); 60 var sl = c.scrollLeft(); 61 var st = c.scrollTop(); 62 63 var cw = c.width(); 64 var ch = c.height(); 65 var cx = cw / 2 + sl; 66 var cy = ch / 2 + st; 67 68 cx *= this.zoomFactorDelta; 69 cy *= this.zoomFactorDelta; 70 71 sl = cx - cw / 2; 72 st = cy - ch / 2; 73 // console.log(sl, st); 74 75 var drawingArea = this.$getDrawingArea(); 76 var width = drawingArea.width(); 77 var height = drawingArea.height(); 78 drawingArea.width(width * delta).height(height * delta); 79 80 // scroll only after drawing area's width was set. 81 this.scroll(sl, st); 82 83 // adjust background size 84 var backgroundSize = parseFloat(drawingArea.css("background-size")); 85 if (isNaN(backgroundSize)) { 86 // parsing could possibly fail in the future. 87 console.warn("Could not set background-size!"); 88 } 89 drawingArea.css("background-size", backgroundSize * delta); 90 }; 91 92 /** 93 * Applies the new size according to current zoom factor. 94 * 95 * @param {Integer} width 96 * @param {Integer} height 97 */ 98 this.setDimensions = function(width, height) { 99 width = width * this.zoomFactor; 100 height = height * this.zoomFactor; 101 102 var drawingArea = this.$getDrawingArea(); 103 drawingArea.width(width).height(height); 104 }; 105 106 /** 107 * Sets the new zoom factor and stores the delta to the old one. 108 * 109 * @param {Number} zoomFactor 110 */ 111 this.setZoomFactor = function(zoomFactor) { 112 this.zoomFactorDelta = zoomFactor / (this.zoomFactor || 1); 113 this.zoomFactor = zoomFactor; 114 }; 115 }; 116 117 /** 118 * Should draw the mind map onto the drawing area. 119 * 120 * @param {mindmaps.MindMap} map 121 */ 122 mindmaps.CanvasView.prototype.drawMap = function(map) { 123 throw new Error("Not implemented"); 124 }; 125 126 /** 127 * Creates a new DefaultCanvasView. This is the reference implementation for 128 * drawing mind maps. 129 * 130 * @extends mindmaps.CanvasView 131 * @constructor 132 */ 133 mindmaps.DefaultCanvasView = function() { 134 var self = this; 135 var nodeDragging = false; 136 var creator = new Creator(this); 137 var captionEditor = new CaptionEditor(this); 138 var textMetrics = new TextMetrics(this); 139 140 captionEditor.commit = function(text) { 141 self.nodeCaptionEditCommitted(text); 142 }; 143 144 /** 145 * Enables dragging of the map with the mouse. 146 */ 147 function makeDraggable() { 148 self.$getContainer().dragscrollable({ 149 dragSelector : "#drawing-area, canvas.line-canvas", 150 acceptPropagatedEvent : false, 151 delegateMode : true, 152 preventDefault : true 153 }); 154 } 155 156 function $getNodeCanvas(node) { 157 return $("#node-canvas-" + node.id); 158 } 159 160 function $getNode(node) { 161 return $("#node-" + node.id); 162 } 163 164 function $getNodeCaption(node) { 165 return $("#node-caption-" + node.id); 166 } 167 168 /** 169 * Draws the line connection (the branch) between two nodes onto the canvas 170 * object. 171 * 172 * @param {jQuery} $canvas 173 * @param {Integer} depth 174 * @param {Number} offsetX 175 * @param {Number} offsetY 176 * @param {jQuery} $node 177 * @param {jQuery} $parent 178 * @param {String} color 179 */ 180 function drawLineCanvas($canvas, depth, offsetX, offsetY, $node, $parent, 181 color) { 182 var zoomFactor = self.zoomFactor; 183 offsetX = offsetX * zoomFactor; 184 offsetY = offsetY * zoomFactor; 185 186 var pw = $parent.width(); 187 var nw = $node.width(); 188 var pih = $parent.innerHeight(); 189 var nih = $node.innerHeight(); 190 191 // line is drawn from node to parent 192 // draw direction 193 var leftToRight, topToBottom; 194 195 // node overlaps with parent above or delow 196 var overlap = false; 197 198 // canvas attributes 199 var left, top, width, height; 200 var bColor; 201 202 // position relative to parent 203 var nodeLeft = offsetX + nw / 2 < pw / 2; 204 if (nodeLeft) { 205 var aOffsetX = Math.abs(offsetX); 206 if (aOffsetX > nw) { 207 // normal left 208 209 // make it one pixel too wide to fix firefox rounding issues 210 width = aOffsetX - nw + 1; 211 left = nw; 212 leftToRight = true; 213 214 //bColor = "red"; 215 } else { 216 // left overlap 217 left = -offsetX; 218 width = nw + offsetX; 219 leftToRight = false; 220 overlap = true; 221 222 //bColor = "orange"; 223 } 224 } else { 225 if (offsetX > pw) { 226 // normal right 227 228 // make it one pixel too wide to fix firefox rounding issues 229 width = offsetX - pw + 1; 230 left = pw - offsetX; 231 leftToRight = false; 232 233 //bColor = "green"; 234 } else { 235 // right overlap 236 width = pw - offsetX; 237 left = 0; 238 leftToRight = true; 239 overlap = true; 240 241 //bColor = "yellow"; 242 } 243 } 244 245 246 var lineWidth = self.getLineWidth(depth); 247 var halfLineWidth = lineWidth / 2; 248 249 // avoid zero widths 250 if (width < lineWidth) { 251 width = lineWidth; 252 } 253 254 var nodeAbove = offsetY + nih < pih; 255 if (nodeAbove) { 256 top = nih; 257 height = $parent.outerHeight() - offsetY - top; 258 259 topToBottom = true; 260 } else { 261 top = pih - offsetY; 262 height = $node.outerHeight() - top; 263 264 topToBottom = false; 265 } 266 267 // position canvas 268 $canvas.attr({ 269 width : width, 270 height : height 271 }).css({ 272 left : left, 273 top : top 274 // ,border: "1px solid " + bColor 275 }); 276 277 // determine start and end coordinates 278 var startX, startY, endX, endY; 279 if (leftToRight) { 280 startX = 0; 281 endX = width; 282 } else { 283 startX = width; 284 endX = 0; 285 } 286 287 // calculate difference in line width to parent node 288 // and position line vertically centered to parent line 289 var pLineWidth = self.getLineWidth(depth - 1); 290 var diff = (pLineWidth - lineWidth)/2; 291 292 if (topToBottom) { 293 startY = 0 + halfLineWidth; 294 endY = height - halfLineWidth - diff; 295 } else { 296 startY = height - halfLineWidth; 297 endY = 0 + halfLineWidth + diff; 298 } 299 300 // calculate bezier points 301 if (!overlap) { 302 var cp2x = startX > endX ? startX / 5 : endX - (endX / 5); 303 var cp2y = endY; 304 305 var cp1x = Math.abs(startX - endX) / 2; 306 var cp1y = startY; 307 } else { 308 // node overlaps with parent 309 310 // take left and right a bit away so line fits fully in canvas 311 if (leftToRight) { 312 startX += halfLineWidth; 313 endX -= halfLineWidth; 314 } else { 315 startX -= halfLineWidth; 316 endX += halfLineWidth; 317 } 318 319 // reversed bezier for overlap 320 var cp1x = startX; 321 var cp1y = Math.abs(startY - endY) / 2; 322 323 var cp2x = endX; 324 var cp2y = startY > endY ? startY / 5 : endY - (endY / 5); 325 } 326 327 // draw 328 var canvas = $canvas[0]; 329 var ctx = canvas.getContext("2d"); 330 ctx.lineWidth = lineWidth; 331 ctx.strokeStyle = color; 332 ctx.fillStyle = color; 333 334 ctx.beginPath(); 335 ctx.moveTo(startX, startY); 336 ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY); 337 ctx.stroke(); 338 339 var drawControlPoints = false; 340 if (drawControlPoints) { 341 // control points 342 ctx.beginPath(); 343 ctx.fillStyle = "red"; 344 ctx.arc(cp1x, cp1y, 4, 0, Math.PI * 2); 345 ctx.fill(); 346 ctx.beginPath(); 347 ctx.fillStyle = "green"; 348 ctx.arc(cp2x, cp2y, 4, 0, Math.PI * 2); 349 ctx.fill(); 350 } 351 } 352 353 354 this.init = function() { 355 makeDraggable(); 356 this.center(); 357 358 var $drawingArea = this.$getDrawingArea(); 359 $drawingArea.addClass("mindmap"); 360 361 // setup delegates 362 $drawingArea.delegate("div.node-caption", "mousedown", function(e) { 363 var node = $(this).parent().data("node"); 364 if (self.nodeMouseDown) { 365 self.nodeMouseDown(node); 366 } 367 }); 368 369 $drawingArea.delegate("div.node-caption", "mouseup", function(e) { 370 var node = $(this).parent().data("node"); 371 if (self.nodeMouseUp) { 372 self.nodeMouseUp(node); 373 } 374 }); 375 376 $drawingArea.delegate("div.node-caption", "dblclick", function(e) { 377 var node = $(this).parent().data("node"); 378 if (self.nodeDoubleClicked) { 379 self.nodeDoubleClicked(node); 380 } 381 }); 382 383 $drawingArea.delegate("div.node-container", "mouseover", function(e) { 384 if (e.target === this) { 385 var node = $(this).data("node"); 386 if (self.nodeMouseOver) { 387 self.nodeMouseOver(node); 388 } 389 } 390 return false; 391 }); 392 393 $drawingArea.delegate("div.node-caption", "mouseover", function(e) { 394 if (e.target === this) { 395 var node = $(this).parent().data("node"); 396 if (self.nodeCaptionMouseOver) { 397 self.nodeCaptionMouseOver(node); 398 } 399 } 400 return false; 401 }); 402 403 // mouse wheel listener 404 this.$getContainer().bind("mousewheel", function(e, delta) { 405 if (self.mouseWheeled) { 406 self.mouseWheeled(delta); 407 } 408 }); 409 }; 410 411 /** 412 * Clears the drawing area. 413 */ 414 this.clear = function() { 415 var drawingArea = this.$getDrawingArea(); 416 drawingArea.children().remove(); 417 drawingArea.width(0).height(0); 418 }; 419 420 /** 421 * Calculates the width of a branch for a node for the given depth 422 * 423 * @param {Integer} depth the depth of the node 424 * @returns {Number} 425 */ 426 this.getLineWidth = function(depth) { 427 // var width = this.zoomFactor * (10 - depth); 428 var width = this.zoomFactor * (12 - depth * 2); 429 430 if (width < 2) { 431 width = 2; 432 } 433 434 return width; 435 }; 436 437 /** 438 * Draws the complete map onto the drawing area. This should only be called 439 * once for a mind map. 440 */ 441 this.drawMap = function(map) { 442 var now = new Date().getTime(); 443 var $drawingArea = this.$getDrawingArea(); 444 445 // clear map first 446 $drawingArea.children().remove(); 447 448 var root = map.root; 449 450 // 1.5. do NOT detach for now since DIV dont have widths and heights, 451 // and loading maps draws wrong canvases (or create nodes and then draw 452 // canvases) 453 454 var detach = false; 455 if (detach) { 456 // detach drawing area during map creation to avoid unnecessary DOM 457 // repaint events. (binary7 is 3 times faster) 458 var $parent = $drawingArea.parent(); 459 $drawingArea.detach(); 460 self.createNode(root, $drawingArea); 461 $drawingArea.appendTo($parent); 462 } else { 463 self.createNode(root, $drawingArea); 464 } 465 466 console.debug("draw map ms: ", new Date().getTime() - now); 467 }; 468 469 /** 470 * Inserts a new node including all of its children into the DOM. 471 * 472 * @param {mindmaps.Node} node - The model of the node. 473 * @param {jQuery} [$parent] - optional jquery parent object the new node is 474 * appended to. Usually the parent node. If argument is omitted, 475 * the getParent() method of the node is called to resolve the 476 * parent. 477 * @param {Integer} [depth] - Optional. The depth of the tree relative to 478 * the root. If argument is omitted the getDepth() method of the 479 * node is called to resolve the depth. 480 */ 481 this.createNode = function(node, $parent, depth) { 482 var parent = node.getParent(); 483 var $parent = $parent || $getNode(parent); 484 var depth = depth || node.getDepth(); 485 var offsetX = node.offset.x; 486 var offsetY = node.offset.y; 487 488 // div node container 489 var $node = $("<div/>", { 490 id : "node-" + node.id, 491 "class" : "node-container" 492 }).data({ 493 node : node 494 }).css({ 495 "font-size" : node.text.font.size 496 }); 497 $node.appendTo($parent); 498 499 if (node.isRoot()) { 500 var w = this.getLineWidth(depth); 501 $node.css("border-bottom-width", w); 502 } 503 504 if (!node.isRoot()) { 505 // draw border and position manually only non-root nodes 506 var bThickness = this.getLineWidth(depth); 507 var bColor = node.branchColor; 508 var bb = bThickness + "px solid " + bColor; 509 510 $node.css({ 511 left : this.zoomFactor * offsetX, 512 top : this.zoomFactor * offsetY, 513 "border-bottom" : bb 514 }); 515 516 // node drag behaviour 517 /** 518 * Only attach the drag handler once we mouse over it. this speeds 519 * up loading of big maps. 520 */ 521 $node.one("mouseenter", function() { 522 $node.draggable({ 523 // could be set 524 // revert: true, 525 // revertDuration: 0, 526 handle : "div.node-caption:first", 527 start : function() { 528 nodeDragging = true; 529 }, 530 drag : function(e, ui) { 531 // reposition and draw canvas while dragging 532 var offsetX = ui.position.left / self.zoomFactor; 533 var offsetY = ui.position.top / self.zoomFactor; 534 var color = node.branchColor; 535 var $canvas = $getNodeCanvas(node); 536 537 drawLineCanvas($canvas, depth, offsetX, offsetY, $node, 538 $parent, color); 539 540 // fire dragging event 541 if (self.nodeDragging) { 542 self.nodeDragging(); 543 } 544 }, 545 stop : function(e, ui) { 546 nodeDragging = false; 547 var pos = new mindmaps.Point(ui.position.left 548 / self.zoomFactor, ui.position.top 549 / self.zoomFactor); 550 551 // fire dragged event 552 if (self.nodeDragged) { 553 self.nodeDragged(node, pos); 554 } 555 } 556 }); 557 }); 558 } 559 560 // text caption 561 var font = node.text.font; 562 var $text = $("<div/>", { 563 id : "node-caption-" + node.id, 564 "class" : "node-caption node-text-behaviour", 565 text : node.text.caption 566 }).css({ 567 "color" : font.color, 568 "font-size" : this.zoomFactor * 100 + "%", 569 "font-weight" : font.weight, 570 "font-style" : font.style, 571 "text-decoration" : font.decoration 572 }).appendTo($node); 573 574 var metrics = textMetrics.getTextMetrics(node); 575 $text.css(metrics); 576 577 // create fold button for parent if he hasn't one already 578 var parentAlreadyHasFoldButton = $parent.data("foldButton"); 579 var nodeOrParentIsRoot = node.isRoot() || parent.isRoot(); 580 if (!parentAlreadyHasFoldButton && !nodeOrParentIsRoot) { 581 this.createFoldButton(parent); 582 } 583 584 if (!node.isRoot()) { 585 // toggle visibility 586 if (parent.foldChildren) { 587 $node.hide(); 588 } else { 589 $node.show(); 590 } 591 592 // draw canvas to parent if node is not a root 593 var $canvas = $("<canvas/>", { 594 id : "node-canvas-" + node.id, 595 "class" : "line-canvas" 596 }); 597 598 // position and draw connection 599 drawLineCanvas($canvas, depth, offsetX, offsetY, $node, $parent, 600 node.branchColor); 601 $canvas.appendTo($node); 602 } 603 604 if (node.isRoot()) { 605 $node.children().andSelf().addClass("root"); 606 } 607 608 // draw child nodes 609 node.forEachChild(function(child) { 610 self.createNode(child, $node, depth + 1); 611 }); 612 }; 613 614 /** 615 * Removes a node from the view and with it all its children and the branch 616 * leading to the parent. 617 * 618 * @param {mindmaps.Node} node 619 */ 620 this.deleteNode = function(node) { 621 // detach creator first, we need still him 622 // creator.detach(); 623 624 // delete all DOM below 625 var $node = $getNode(node); 626 $node.remove(); 627 }; 628 629 /** 630 * Highlights a node to show that it is selected. 631 * 632 * @param {mindmaps.Node} node 633 */ 634 this.highlightNode = function(node) { 635 var $text = $getNodeCaption(node); 636 $text.addClass("selected"); 637 }; 638 639 /** 640 * Removes the hightlight of a node. 641 * 642 * @param {mindmaps.Node} node 643 */ 644 this.unhighlightNode = function(node) { 645 var $text = $getNodeCaption(node); 646 $text.removeClass("selected"); 647 }; 648 649 /** 650 * Hides all children of a node. 651 * 652 * @param {mindmaps.Node} node 653 */ 654 this.closeNode = function(node) { 655 var $node = $getNode(node); 656 $node.children(".node-container").hide(); 657 658 var $foldButton = $node.children(".button-fold").first(); 659 $foldButton.removeClass("open").addClass("closed"); 660 }; 661 662 /** 663 * Shows all children of a node. 664 * 665 * @param {mindmaps.Node} node 666 */ 667 this.openNode = function(node) { 668 var $node = $getNode(node); 669 $node.children(".node-container").show(); 670 671 var $foldButton = $node.children(".button-fold").first(); 672 $foldButton.removeClass("closed").addClass("open"); 673 }; 674 675 /** 676 * Creates the fold button for a node that shows/hides its children. 677 * 678 * @param {mindmaps.Node} node 679 */ 680 this.createFoldButton = function(node) { 681 var position = node.offset.x > 0 ? " right" : " left"; 682 var openClosed = node.foldChildren ? " closed" : " open"; 683 var $foldButton = $("<div/>", { 684 "class" : "button-fold no-select" + openClosed + position 685 }).click(function(e) { 686 // fire event 687 if (self.foldButtonClicked) { 688 self.foldButtonClicked(node); 689 } 690 691 e.preventDefault(); 692 return false; 693 }); 694 695 // remember that foldButton was set and attach to node 696 var $node = $getNode(node); 697 $node.data({ 698 foldButton : true 699 }).append($foldButton); 700 }; 701 702 /** 703 * Removes the fold button. 704 * 705 * @param {mindmaps.Node} node 706 */ 707 this.removeFoldButton = function(node) { 708 var $node = $getNode(node); 709 $node.data({ 710 foldButton : false 711 }).children(".button-fold").remove(); 712 }; 713 714 /** 715 * Goes into edit mode for a node. 716 * 717 * @param {mindmaps.Node} node 718 */ 719 this.editNodeCaption = function(node) { 720 captionEditor.edit(node, this.$getDrawingArea()); 721 }; 722 723 /** 724 * Stops the current edit mode. 725 */ 726 this.stopEditNodeCaption = function() { 727 captionEditor.stop(); 728 }; 729 730 /** 731 * Updates the text caption for a node. 732 * 733 * @param {mindmaps.Node} node 734 * @param {String} value 735 */ 736 this.setNodeText = function(node, value) { 737 var $text = $getNodeCaption(node); 738 var metrics = textMetrics.getTextMetrics(node, value); 739 $text.css(metrics).text(value); 740 }; 741 742 /** 743 * Get a reference to the creator tool. 744 * 745 * @returns {Creator} 746 */ 747 this.getCreator = function() { 748 return creator; 749 }; 750 751 /** 752 * Returns whether a node is currently being dragged. 753 * 754 * @returns {Boolean} 755 */ 756 this.isNodeDragging = function() { 757 return nodeDragging; 758 }; 759 760 /** 761 * Redraws a node's branch to its parent. 762 * 763 * @param {mindmaps.Node} node 764 */ 765 function drawNodeCanvas(node) { 766 var parent = node.getParent(); 767 var depth = node.getDepth(); 768 var offsetX = node.offset.x; 769 var offsetY = node.offset.y; 770 var color = node.branchColor; 771 772 var $node = $getNode(node); 773 var $parent = $getNode(parent); 774 var $canvas = $getNodeCanvas(node); 775 776 drawLineCanvas($canvas, depth, offsetX, offsetY, $node, $parent, color); 777 } 778 779 /** 780 * Redraws all branches that a node is connected to. 781 * 782 * @param {mindmaps.Node} node 783 */ 784 this.redrawNodeConnectors = function(node) { 785 786 // redraw canvas to parent 787 if (!node.isRoot()) { 788 drawNodeCanvas(node); 789 } 790 791 // redraw all child canvases 792 if (!node.isLeaf()) { 793 node.forEachChild(function(child) { 794 drawNodeCanvas(child); 795 }); 796 } 797 }; 798 799 /** 800 * Does a complete visual update of a node to reflect all of its attributes. 801 * 802 * @param {mindmaps.Node} node 803 */ 804 this.updateNode = function(node) { 805 var $node = $getNode(node); 806 var $text = $getNodeCaption(node); 807 var font = node.text.font; 808 $node.css({ 809 "font-size" : font.size, 810 "border-bottom-color" : node.branchColor 811 }); 812 813 var metrics = textMetrics.getTextMetrics(node); 814 815 $text.css({ 816 "color" : font.color, 817 "font-weight" : font.weight, 818 "font-style" : font.style, 819 "text-decoration" : font.decoration 820 }).css(metrics); 821 822 this.redrawNodeConnectors(node); 823 }; 824 825 /** 826 * Moves the node a new position. 827 * 828 * @param {mindmaps.Node} node 829 */ 830 this.positionNode = function(node) { 831 var $node = $getNode(node); 832 // TODO try animate 833 // position 834 $node.css({ 835 left : this.zoomFactor * node.offset.x, 836 top : this.zoomFactor * node.offset.y 837 }); 838 839 // redraw canvas to parent 840 drawNodeCanvas(node); 841 }; 842 843 /** 844 * Redraws the complete map to adapt to a new zoom factor. 845 */ 846 this.scaleMap = function() { 847 var zoomFactor = this.zoomFactor; 848 var $root = this.$getDrawingArea().children().first(); 849 var root = $root.data("node"); 850 851 var w = this.getLineWidth(0); 852 $root.css("border-bottom-width", w); 853 854 // handle root differently 855 var $text = $getNodeCaption(root); 856 var metrics = textMetrics.getTextMetrics(root); 857 $text.css({ 858 "font-size" : zoomFactor * 100 + "%", 859 "left" : zoomFactor * -TextMetrics.ROOT_CAPTION_MIN_WIDTH / 2 860 }).css(metrics); 861 862 root.forEachChild(function(child) { 863 scale(child, 1); 864 }); 865 866 function scale(node, depth) { 867 var $node = $getNode(node); 868 869 // draw border and position manually 870 var bWidth = self.getLineWidth(depth); 871 872 $node.css({ 873 left : zoomFactor * node.offset.x, 874 top : zoomFactor * node.offset.y, 875 "border-bottom-width" : bWidth 876 }); 877 878 var $text = $getNodeCaption(node); 879 $text.css({ 880 "font-size" : zoomFactor * 100 + "%" 881 }); 882 883 var metrics = textMetrics.getTextMetrics(node); 884 $text.css(metrics); 885 886 // redraw canvas to parent 887 drawNodeCanvas(node); 888 889 // redraw all child canvases 890 if (!node.isLeaf()) { 891 node.forEachChild(function(child) { 892 scale(child, depth + 1); 893 }); 894 } 895 } 896 }; 897 898 /** 899 * Creates a new CaptionEditor. This tool offers an inline editor component 900 * to change a node's caption. 901 * 902 * @constructor 903 * @param {mindmaps.CanvasView} view 904 */ 905 function CaptionEditor(view) { 906 var self = this; 907 var attached = false; 908 909 // text input for node edits. 910 var $editor = $("<textarea/>", { 911 id : "caption-editor", 912 "class" : "node-text-behaviour" 913 }).bind("keydown", "esc", function() { 914 self.stop(); 915 }).bind("keydown", "return", function() { 916 if (self.commit) { 917 self.commit($editor.val()); 918 } 919 }).mousedown(function(e) { 920 // avoid premature canceling 921 e.stopPropagation(); 922 }).blur(function() { 923 self.stop(); 924 }).bind("input", function() { 925 var metrics = textMetrics.getTextMetrics(self.node, $editor.val()); 926 $editor.css(metrics); 927 928 // slightly defer execution for better performance on slow browsers 929 setTimeout(function() { 930 view.redrawNodeConnectors(self.node); 931 }, 1); 932 }); 933 934 /** 935 * Attaches the textarea to the node and temporarily removes the 936 * original node caption. 937 * 938 * @param {mindmaps.Node} node 939 * @param {jQuery} $cancelArea 940 */ 941 this.edit = function(node, $cancelArea) { 942 if (attached) { 943 return; 944 } 945 this.node = node; 946 attached = true; 947 948 // TODO put text into span and hide() 949 this.$text = $getNodeCaption(node); 950 this.$cancelArea = $cancelArea; 951 952 this.text = this.$text.text(); 953 954 this.$text.css({ 955 width : "auto", 956 height : "auto" 957 }).empty().addClass("edit"); 958 959 $cancelArea.bind("mousedown.editNodeCaption", function(e) { 960 self.stop(); 961 }); 962 963 var metrics = textMetrics.getTextMetrics(self.node, this.text); 964 $editor.attr({ 965 value : this.text 966 }).css(metrics).appendTo(this.$text).select(); 967 968 }; 969 970 /** 971 * Removes the editor from the node and restores its old text value. 972 */ 973 this.stop = function() { 974 if (attached) { 975 attached = false; 976 this.$text.removeClass("edit"); 977 $editor.detach(); 978 this.$cancelArea.unbind("mousedown.editNodeCaption"); 979 view.setNodeText(this.node, this.text); 980 } 981 982 }; 983 } 984 985 /** 986 * Creates a new Creator. This tool is used to drag out new branches to 987 * create new nodes. 988 * 989 * @constructor 990 * @param {mindmaps.CanvasView} view 991 * @returns {Creator} 992 */ 993 function Creator(view) { 994 var self = this; 995 var dragging = false; 996 997 this.node = null; 998 this.lineColor = null; 999 1000 var $wrapper = $("<div/>", { 1001 id : "creator-wrapper" 1002 }).bind("remove", function(e) { 1003 // detach the creator when some removed the node or opened a new map 1004 self.detach(); 1005 // and avoid removing from DOM 1006 e.stopImmediatePropagation(); 1007 1008 console.debug("creator detached."); 1009 return false; 1010 }); 1011 1012 // red dot creator element 1013 var $nub = $("<div/>", { 1014 id : "creator-nub" 1015 }).appendTo($wrapper); 1016 1017 var $fakeNode = $("<div/>", { 1018 id : "creator-fakenode" 1019 }).appendTo($nub); 1020 1021 // canvas used by the creator tool to draw new lines 1022 var $canvas = $("<canvas/>", { 1023 id : "creator-canvas", 1024 "class" : "line-canvas" 1025 }).hide().appendTo($wrapper); 1026 1027 // make it draggable 1028 $wrapper.draggable({ 1029 revert : true, 1030 revertDuration : 0, 1031 start : function() { 1032 dragging = true; 1033 // show creator canvas 1034 $canvas.show(); 1035 if (self.dragStarted) { 1036 self.lineColor = self.dragStarted(self.node); 1037 } 1038 }, 1039 drag : function(e, ui) { 1040 // update creator canvas 1041 var offsetX = ui.position.left / view.zoomFactor; 1042 var offsetY = ui.position.top / view.zoomFactor; 1043 1044 // set depth+1 because we are drawing the canvas for the child 1045 var $node = $getNode(self.node); 1046 drawLineCanvas($canvas, self.depth + 1, offsetX, offsetY, 1047 $fakeNode, $node, self.lineColor); 1048 }, 1049 stop : function(e, ui) { 1050 dragging = false; 1051 // remove creator canvas, gets replaced by real canvas 1052 $canvas.hide(); 1053 if (self.dragStopped) { 1054 var $wp = $wrapper.position(); 1055 var $wpLeft = $wp.left / view.zoomFactor; 1056 var $wpTop = $wp.top / view.zoomFactor; 1057 var nubLeft = ui.position.left / view.zoomFactor; 1058 var nubTop = ui.position.top / view.zoomFactor; 1059 1060 var distance = mindmaps.Util.distance($wpLeft - nubLeft, 1061 $wpTop - nubTop); 1062 self.dragStopped(self.node, nubLeft, nubTop, distance); 1063 } 1064 1065 // remove any positioning that the draggable might have caused 1066 $wrapper.css({ 1067 left : "", 1068 top : "" 1069 }); 1070 } 1071 }); 1072 1073 /** 1074 * Attaches the tool to a node. 1075 * 1076 * @param {mindmaps.Node} node 1077 */ 1078 this.attachToNode = function(node) { 1079 if (this.node === node) { 1080 return; 1081 } 1082 this.node = node; 1083 1084 // position the nub correctly 1085 $wrapper.removeClass("left right"); 1086 if (node.offset.x > 0) { 1087 $wrapper.addClass("right"); 1088 } else if (node.offset.x < 0) { 1089 $wrapper.addClass("left"); 1090 } 1091 1092 // set border on our fake node for correct line drawing 1093 this.depth = node.getDepth(); 1094 var w = view.getLineWidth(this.depth + 1); 1095 $fakeNode.css("border-bottom-width", w); 1096 1097 var $node = $getNode(node); 1098 $wrapper.appendTo($node); 1099 }; 1100 1101 /** 1102 * Removes the tool from the current node. 1103 */ 1104 this.detach = function() { 1105 $wrapper.detach(); 1106 this.node = null; 1107 }; 1108 1109 /** 1110 * Returns whether the tool is currently in use being dragged. 1111 * 1112 * @returns {Boolean} 1113 */ 1114 this.isDragging = function() { 1115 return dragging; 1116 }; 1117 } 1118 1119 /** 1120 * Utitility object that calculates how much space a text would take up in a 1121 * node. This is done through a dummy div that has the same formatting as 1122 * the node and gets the text injected. 1123 * 1124 * @constructor 1125 * @param {mindmaps.CanvasView} view 1126 */ 1127 function TextMetrics(view) { 1128 var $div = $("<div/>", { 1129 id : "text-metrics-dummy", 1130 "class" : "node-text-behaviour" 1131 }).css({ 1132 position : "absolute", 1133 visibility : "hidden", 1134 height : "auto", 1135 width : "auto" 1136 }).appendTo(view.$getContainer()); 1137 1138 /** 1139 * Calculates the width and height a node would have to provide to show 1140 * the text. 1141 * 1142 * @param {mindmaps.Node} node the node whose text is to be measured. 1143 * @param {mindmaps.Node} [text] use this instead of the text of node 1144 * @returns {Object} object with properties width and height. 1145 */ 1146 this.getTextMetrics = function(node, text) { 1147 text = text || node.getCaption(); 1148 var font = node.text.font; 1149 var minWidth = node.isRoot() ? TextMetrics.ROOT_CAPTION_MIN_WIDTH 1150 : TextMetrics.NODE_CAPTION_MIN_WIDTH; 1151 var maxWidth = TextMetrics.NODE_CAPTION_MAX_WIDTH; 1152 1153 $div.css({ 1154 "font-size" : view.zoomFactor * font.size, 1155 "min-width" : view.zoomFactor * minWidth, 1156 "max-width" : view.zoomFactor * maxWidth, 1157 "font-weight" : font.weight 1158 }).text(text); 1159 1160 // add some safety pixels for firefox, otherwise it doesnt render 1161 // right on textareas 1162 var w = $div.width() + 2; 1163 var h = $div.height() + 2; 1164 1165 return { 1166 width : w, 1167 height : h 1168 }; 1169 }; 1170 } 1171 /** 1172 * @constant 1173 */ 1174 TextMetrics.ROOT_CAPTION_MIN_WIDTH = 100; 1175 1176 /** 1177 * @constant 1178 */ 1179 TextMetrics.NODE_CAPTION_MIN_WIDTH = 70; 1180 1181 /** 1182 * @constant 1183 */ 1184 TextMetrics.NODE_CAPTION_MAX_WIDTH = 150; 1185 }; 1186 1187 // inherit from base canvas view 1188 mindmaps.DefaultCanvasView.prototype = new mindmaps.CanvasView();