/app/app.js
JavaScript | 1048 lines | 519 code | 135 blank | 394 comment | 51 complexity | 232eeaa8a8833f6cda2573396841e1f7 MD5 | raw file
- jQuery(function($) {
- /*======================================================================================================================
- * NODE_STATES
- *
- *======================================================================================================================
- *
- * Enum Node states
- */
- var NODE_STATES = {
- DEFAULT: "default",
- EDIT: "edit",
- SELECTED: "selected",
- ADDCHILD: "addchild",
- DRAGGING: "dragging",
- COLLIDED: "collided"
- };
- /**
- /*======================================================================================================================
- * MindMapNode
- *
- *======================================================================================================================
- *
- * @class MindMapNode
- */
- var MindMapNode = Backbone.Model.extend({
- defaults: {
- content: "Nieuwe Node",
- pointX: 100,
- pointY: 100,
- parentNode: null,
- userId: null,
- state: NODE_STATES.DEFAULT,
- width: null,
- height: null
- },
- children: null,
- /**
- * @constructor
- * @method initialize
- */
- initialize: function()
- {
- this.children = new MindMapNodeList();
- },
- /**
- * Has this or any of its children the given states
- *
- * @param states NODE_STATE[]
- * @method haveStates
- */
- haveStates: function(states)
- {
- if( states.indexOf(this.get("state")) > -1 )
- {
- return true;
- }
- var i = this.children.length;
- while (i--)
- {
- var haveStates = this.children.at(i).haveStates(states);
- if (haveStates)
- {
- return haveStates;
- }
- }
- return false;
- },
- /**
- * Those node intersect other node ?
- *
- * Will return null when nothing is found.
- *
- * @param {MindMapNode} node
- * @method intersect
- * @return MindMapNode[]
- */
- intersect: function(node)
- {
- var intersected = [];
- // target lookup coordinates
- var targetLeft = node.get("pointX");
- var targetRight = targetLeft + node.get("width");
- var targetTop = node.get("pointY");
- var targetBottom = targetTop + node.get("height");
- // current node lookup coordinates
- var left = this.get("pointX");
- var right = left + this.get("width");
- var top = this.get("pointY");
- var bottom = top + this.get("height");
- // compare coordinates from target with current if they intersect,
- // the node should not be intersecting with itself.
- if (left <= targetRight && targetLeft <= right && top <= targetBottom && targetTop <= bottom && this != node)
- {
- intersected.push(this);
- }
- // foreach child if child = this x and i
- var i = this.children.length;
- while (i--)
- {
- var tmp = this.children.at(i).intersect(node);
- if (tmp != null)
- {
- intersected = intersected.concat(tmp);
- }
- }
- return intersected;
- },
- /**
- * Draw lines to other node.
- *
- * @param drawMethod Function callback
- * @param {int} x2 optional target x position
- * @param {int} y2 optional target y position
- * @method linesToChildren
- */
- linesToChildren: function(drawMethod, x2, y2)
- {
- var x = this.get("pointX") + (this.get("width") /2);
- var y = this.get("pointY") + (this.get("height") /2);
- if( x2 && y2)
- {
- drawMethod(x, y, x2, y2);
- }
- this.children.each(function(node) {
- // calc positions of child.
- var x2 = node.get("pointX") + (node.get("width") / 2);
- var y2 = node.get("pointY") + (node.get("height") / 2);
- // draw line
- drawMethod(x, y, x2, y2);
- node.children.each(function(node) {
- node.linesToChildren(drawMethod, x2, y2);
- });
- });
- },
- /**
- * Find all parents of this node
- *
- * @return MindMapNode[]
- */
- getParents: function()
- {
- var parents = [];
- if( this.get("parentNode") != null)
- {
- parents.push(this.get("parentNode"));
- parents = parents.concat(this.get("parentNode").getParents());
- }
- return parents;
- },
- /**
- * Is given node a child of this node.
- *
- * @param {MindMapNode} parent the parent node
- * @method isChild
- */
- isChildOf: function(parent)
- {
- var res = this.children.select(function(node) {
- return node.cid == parent.cid;
- });
- return res.length > 0;
- },
- /**
- * Find the nearest common ancestor of this and given node.
- *
- * @param node MindMapNode
- * @method hasCommonAncestor
- * @return MindMapNode|null
- */
- findCommonAncestor: function(node)
- {
- var parents = this.getParents();
- var parents2 = node.getParents();
- var found = null;
- for(var i = 0, j = parents.length; i < j && found == null; i++ )
- {
- for(var k = 0, l = parents2.length; k < l; k++)
- {
- if(parents[i].cid == parents2[k].cid)
- {
- found = parents2[k];
- }
- }
- }
- return found;
- }
- });
- /*======================================================================================================================
- * MindMapNodeList
- *
- *======================================================================================================================
- *
- *
- * @class MindMapNodeList
- */
- var MindMapNodeList = Backbone.Collection.extend({
- model: MindMapNode
- });
- /*======================================================================================================================
- * MindMap
- *
- *======================================================================================================================
- *
- *
- * @class MindMap
- */
- var MindMap = Backbone.Model.extend({
- root: null,
- /**
- * @method initialize
- */
- initialize: function()
- {
- this.set({"root": new MindMapNode()});
- }
- });
- /*======================================================================================================================
- * MindMapApplication
- *
- *======================================================================================================================
- *
- * MindMap application controller
- *
- * @class MindMapApplication
- */
- var MindMapApplication = Backbone.Controller.extend({
- routes: {
- "/index": "index",
- "/new": "run"
- },
- map: null,
- /**
- * @constructor
- * @method initialize
- */
- initialize: function()
- {
- this.location("/index");
- },
- /**
- * Open Index view
- * @method index
- */
- index: function()
- {
- new IndexView();
- },
- /**
- * Run application
- *
- * @method run
- */
- run: function()
- {
- this.map = new MindMapView({model: new MindMap()});
- this.map.show();
- this.location("/index");
- },
- /**
- * Set the location of the application.
- *
- * @param url
- * @method location
- */
- location: function(url)
- {
- location.hash = "#" + url;
- },
- /**
- * Delegate the collided.
- *
- * @param node
- * @method handleIntersects
- */
- handleIntersects: function(node)
- {
- var intersected = window.application.map.model.get("root").intersect(node);
- for( var i = 0, j = intersected.length; i < j; i++ )
- {
- intersected[i].set({
- "state": NODE_STATES.COLLIDED
- });
- }
- }
- });
- /*======================================================================================================================
- * IndexView
- *
- *======================================================================================================================
- *
- * Index view > Index page
- *
- * class IndexView
- */
- var IndexView = Backbone.View.extend({
- el: $("body"),
- events: {
- "click #new-map": "newMap"
- },
- /**
- * @method newMap
- */
- newMap: function()
- {
- application.run();
- }
- });
- /*======================================================================================================================
- * MindMapView
- *
- *======================================================================================================================
- *
- * View the mindMap (Generates the root node)
- *
- * @class MindMapView
- */
- var MindMapView = Backbone.View.extend({
- id: "map",
- /**
- * @method initialize
- */
- initialize: function()
- {
- var self = this;
- $(window).resize(function() {
- self.render();
- self.drawLinesToChildren();
- });
- },
- /**
- * Draw line on canvas
- *
- * @param {int} x
- * @param {int} y
- * @param {int} x2
- * @param {int} y2
- * @method drawLine
- */
- drawLine: function(x, y, x2, y2)
- {
- var canvas = document.getElementById("canvas");
- // Make sure we don't execute when canvas isn't supported
- if (canvas.getContext)
- {
- var ctx = canvas.getContext('2d');
- // draw the line
- ctx.beginPath();
- ctx.moveTo(x, y);
- ctx.lineTo(x2, y2);
- ctx.stroke();
- ctx.closePath();
- }
- },
- /**
- * Draw lines all children of the root element
- *
- * @method drawLinesToChildren
- */
- drawLinesToChildren: function()
- {
- var canvas = document.getElementById("canvas");
- // Make sure we don't execute when canvas isn't supported
- if (canvas.getContext)
- {
- // use getContext to use the canvas for drawing
- var ctx = canvas.getContext('2d');
- ctx.clearRect(0, 0, $(canvas).width(), $(canvas).height());
- // draw lines to all children of the root node
- this.model.get("root").linesToChildren(window.application.map.drawLine);
- }
- },
- /**
- * render root node
- *
- * @method render
- */
- render: function()
- {
- var width = $(document).width();
- var height = $(document).height();
- var canvas = document.getElementById("canvas");
- canvas.width = width;
- canvas.height = height;
- $(this.el).css("width", width);
- $(this.el).css("height", height);
- // render nodeView for root node.
- var nodeView = new MindMapNodeView({model: this.model.get("root")});
- // we need to append it first so the dimensions of a node are known.
- $(this.el).append(
- $(nodeView.el).addClass("root")
- );
- nodeView.render();
- return this;
- },
- /**
- * Show the mindMapView (add it to the document) by replacing the element (by id)
- *
- * @method show
- */
- show: function()
- {
- $("#" + this.id).replaceWith(this.render().el);
- }
- });
- /*======================================================================================================================
- * DraggableView
- *
- *======================================================================================================================
- *
- * Make a view draggable, by using the optional .el parameter you can make
- * every view a draggable view.
- *
- * This view uses jquery.ui.draggable (1.8+ tested)
- *
- * @class DraggableView
- */
- var DraggableView = Backbone.View.extend({
- /**
- * This functions is empty by default, and can be overridden by giving a callback parameter
- *
- * Will be called on Drag start.
- *
- * Callback will have arguments:
- * event
- * ui
- *
- * @method onStart
- * @param {Function} callback
- * @method onStart
- */
- onStart: function(callback)
- {
- this.onStart = callback;
- },
- /**
- * This functions is empty by default, and can be overridden by giving a callback parameter
- *
- * Callback will have arguments:
- * event
- * ui
- *
- * Will be called on Drag stop.
- *
- * @method onStop
- * @param callback Function
- * @method onStop
- */
- onStop: function(callback)
- {
- this.onStop = callback;
- },
- /**
- * This functions is empty by default, and can be overridden by giving a callback parameter
- *
- * Callback will have arguments:
- * event
- * ui
- *
- * Will be called on Drag.
- *
- * @method onDrag
- * @param callback Function
- * @method onDrag
- */
- onDrag: function(callback)
- {
- this.onDrag = callback;
- },
- /**
- * Stop dragging will destroy the drag functionality and call the onStop event.
- *
- * @param {Event} event
- * @param {Object} ui
- * @method stopDrag
- */
- stopDrag: function(event, ui)
- {
- this.onStop(event, ui);
- //remove draggable
- $(this.el).draggable("destroy");
- },
- /**
- * Render will make the element (.el) from this view draggable,
- * so by providing a .el option to the view it will make that element draggable.
- *
- * @method render
- */
- render: function()
- {
- // make sure the events (callbacks) are executed in the view scope.
- var self = this;
- $(this.el).draggable({
- stop: function(event, ui) {
- self.stopDrag(event, ui);
- },
- start: function(event, ui) {
- self.onStart(event, ui);
- },
- drag: function(event, ui) {
- self.onDrag(event, ui);
- },
- disabled: false,
- grid: [10, 10],
- scroll: false,
- snap: true
- });
- }
- });
- /*======================================================================================================================
- * EditableMindMapNodeView
- *
- *======================================================================================================================
- *
- * requires model argument (instance of MindMapNode)
- *
- * @class EditableMindMapNodeView
- */
- var EditableMindMapNodeView = Backbone.View.extend({
- tagName: "input",
- events: {
- "keypress": "keyPressInput",
- "blur": "stopEdit"
- },
- /**
- * Stop editing, will update the given model with the new value.
- *
- * @method stopEdit
- */
- stopEdit: function()
- {
- $(this.el).parent().removeClass(NODE_STATES.SELECTED);
- this.model.set({"content": this.getEditValue(), "state": NODE_STATES.DEFAULT});
- return this;
- },
- /**
- * Find out if enter is hit, if so then stop editing
- *
- * @param {Event} e
- * @method keyPressInput
- */
- keyPressInput: function(e)
- {
- // If you hit `enter`, we"re through editing the item.
- if (e.keyCode === 13)
- {
- this.stopEdit();
- }
- },
- /**
- * Get value edited.
- *
- * @method getEditValue
- * @return String
- */
- getEditValue: function()
- {
- return $(this.el).val();
- },
- /**
- * Render view
- *
- * @method render
- * @override
- * return EditableMindMapNodeView
- */
- render: function()
- {
- $(this.el).val(this.model.escape("content"));
- return this;
- }
- });
- /*======================================================================================================================
- * MindMapNodeAddButtonView
- *
- *======================================================================================================================
- *
- * MindMap add button to add a child
- *
- * @class MindMapNodeAddButtonView
- */
- var MindMapNodeAddButtonView = Backbone.View.extend({
- tagName: "button",
- events: {
- "click": "click"
- },
- /**
- * On click set state of node to 'addchild'
- *
- * @method click
- */
- click: function()
- {
- this.hide();
- this.model.set({"state": NODE_STATES.ADDCHILD});
- },
- /**
- * Show the button
- *
- * @method show
- */
- show: function()
- {
- if( this.isShown() )
- {
- return;
- }
- $(this.render().el).fadeIn(250);
- // hide button after timeout
- var self = this;
- setTimeout(function() {
- self.hide();
- }, 2000);
- },
- /**
- * Hide the button
- *
- * @param [timeout] default 250
- * @method hide
- */
- hide: function(timeout)
- {
- $(this.el).fadeOut(timeout || 250);
- },
- /**
- * Is button shown?
- *
- * @method isShown
- * @return String
- */
- isShown: function()
- {
- return $(this.el).is(":visible");
- },
- /**
- * View render method appends the button the the body
- *
- * @method render
- * @return MindMapNodeAddButtonView
- */
- render: function()
- {
- $(this.el)
- .css({
- position: 'absolute',
- top: this.model.get("pointY") + "px",
- left: (this.model.get("pointX") + this.model.get("width") + 10) + "px",
- display: "none"
- }).text("+").appendTo('body');
- return this;
- }
- });
- /*======================================================================================================================
- * MindMapNodeView
- *
- *======================================================================================================================
- *
- * MindMapNode
- *
- * @class EditableMindMapNodeView
- */
- var MindMapNodeView = Backbone.View.extend({
- className: "node",
- events: {
- "click": "click",
- "dblclick": "doubleClick",
- "mouseover": "mouseOver"
- },
- /**
- * subviews
- */
- addButtonView: null,
- editView: null,
- /**
- * @constructor
- * @method initialize
- */
- initialize: function()
- {
- var self = this;
- // render when model is changed
- this.model.bind("change", function()
- {
- if(this.hasChanged("pointX") || this.hasChanged("pointY"))
- {
- // find new coalitions
- if(this.get("state") != NODE_STATES.DRAGGING)
- {
- window.application.handleIntersects(this);
- }
- self.render.call(self);
- }
- else if (this.hasChanged("content"))
- {
- self.render.call(self);
- }
- else if ( this.hasChanged("state") )
- {
- self.handleState(self.model.get("state"));
- }
- });
- // keys
- $(document).keydown(function(e) {
- if( $(self.el).hasClass(NODE_STATES.SELECTED) ) // only allow f2 when node is selected.
- {
- if (e.which == 113) // F2
- {
- self.model.set({"state": NODE_STATES.EDIT});
- }
- }
- });
- },
- /**
- * Hide add button view
- *
- * @param timeout
- * @method hideAddButton
- */
- hideAddButton: function(timeout)
- {
- if( this.addButtonView != null )
- {
- this.addButtonView.hide(timeout);
- }
- },
- /**
- * Mouse over event handler
- *
- * handles the add new node button.
- * @method mouseOver
- */
- mouseOver: function()
- {
- // When one node has state edit or dragging then don't add any add button view
- if (window.application.map.model.get("root").haveStates([NODE_STATES.EDIT, NODE_STATES.DRAGGING]))
- {
- return;
- }
- //make sure the dimensions of this view are set.
- this.setDimensions();
- // create a button and show it.
- if( this.addButtonView == null )
- {
- this.addButtonView = new MindMapNodeAddButtonView({model: this.model});
- this.addButtonView.render();
- }
- this.addButtonView.show();
- },
- /**
- * Click event handler
- *
- * On click this node will be made draggable.
- *
- * @method click
- */
- click: function()
- {
- if (this.model.get("state") == NODE_STATES.EDIT) // don't do mouse move stuff when editing.)
- {
- return;
- }
- $(this.el).addClass(NODE_STATES.SELECTED);
- this.makeDraggable();
- },
- /**
- * Make Node draggable by using a draggableView
- *
- * @method makeDraggable
- */
- makeDraggable: function()
- {
- // make dragView of the node
- var self = this;
- var dragView = new DraggableView({el: this.el});
- dragView.onStart(function() {
- self.model.set({"state": NODE_STATES.DRAGGING});
- });
- dragView.onStop(function() {
- self.model.set({"state": NODE_STATES.DEFAULT});
- window.application.handleIntersects(self.model);
- });
- dragView.onDrag(function(e, ui) {
- self.model.set({
- "state": NODE_STATES.DRAGGING,
- "pointX": ui.position.left,
- "pointY": ui.position.top
- });
- });
- dragView.render();
- },
- /**
- * Double click event handler
- *
- * Turn edit mode on.
- *
- * @method doubleClick
- */
- doubleClick: function()
- {
- this.model.set({"state": NODE_STATES.EDIT});
- },
- /**
- * Handles node state
- *
- * @method handleState
- * @param state NODE_STATES property of 'enum'
- */
- handleState: function(state)
- {
- switch(state)
- {
- case NODE_STATES.ADDCHILD:
- var node = new MindMapNode();
- var pointX = this.model.get("pointX") + this.model.get("width") + 50;
- var pointY = this.model.get("pointY");
- var lastNode = this.model.children.last();
- if( lastNode !== undefined )
- {
- pointX = lastNode.get("pointX");
- pointY = lastNode.get("pointY") + lastNode.get("height") + 10;
- }
- node.set({
- "content": "child: " + this.model.cid + " " + this.model.children.length,
- "pointX": pointX,
- "pointY": pointY,
- "parentNode": this.model
- });
- var nodeView = new MindMapNodeView({model: node});
- // we need to append it first so the dimensions of a node are known.
- $(window.application.map.el).append(nodeView.el);
- nodeView.render();
- // we add the node as child after it is rendered so coalition detection can be done.
- this.model.children.add(node);
- // handle intersects
- window.application.handleIntersects(node);
- // we need to paint it again after handleIntersect could have changed any positions of it's children.
- nodeView.render();
- this.model.set({"state": NODE_STATES.DEFAULT});
- break;
- case NODE_STATES.EDIT:
- this.editView = new EditableMindMapNodeView({model: this.model});
- $(this.el).html(this.editView.render().el);
- $(this.editView.el).focus();
- break;
- case NODE_STATES.DRAGGING:
- // dragging stop editing.
- if( this.editView != null)
- {
- this.editView.stopEdit().remove();
- }
- // remove add button on dragging
- this.hideAddButton(0);
- break;
- case NODE_STATES.COLLIDED:
- var intersects = window.application.map.model.get("root").intersect(this.model);
- if (intersects.length > 0)
- {
- console.log(this.model.isChildOf(intersects[0]));
- console.log(this.model.findCommonAncestor(intersects[0]));
- intersects[0].set({
- "state": NODE_STATES.DEFAULT,
- "pointX": this.model.get("pointX"),
- "pointY": this.model.get("pointY") + this.model.get("height") + 10
- });
- }
- else
- {
- this.model.set({"state": NODE_STATES.DEFAULT});
- }
- break;
- case NODE_STATES.DEFAULT:
- this.render();
- break;
- }
- },
- /**
- * Sets the dimensions on of the node on the model, this doesn't trigger a model change event.
- *
- * @method setDimensions
- */
- setDimensions: function()
- {
- // save the new dimensions of the node.
- this.model.set({
- 'width': $(this.el).outerWidth(),
- 'height': $(this.el).outerHeight()
- }, {silent: true});
- },
- /**
- * Renders the position and content of the node. (Will reset every change in the node content)
- *
- * @method render
- * @return MindMapNodeView
- */
- render: function()
- {
- // generate positioned node
- $(this.el)
- .attr("id", this.model.cid)
- .css({
- top: this.model.get("pointY") + "px",
- left: this.model.get("pointX") + "px"
- })
- .html(this.model.escape("content"));
- //make sure the dimensions of this view are set.
- this.setDimensions();
- // draw lines between children.
- window.application.map.drawLinesToChildren();
- return this;
- }
- });
- window.application = new MindMapApplication();
- Backbone.history.start();
- });