Passed
Push — master ( 1713a6...bcb549 )
by Peter
02:05
created

FuzzEd/static/script/node.js   F

Complexity

Total Complexity 124
Complexity/F 1.63

Size

Lines of Code 1176
Function Count 76

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
dl 0
loc 1176
rs 2.4
c 0
b 0
f 0
wmc 124
nc 32
mnd 2
bc 91
fnc 76
bpm 1.1973
cpm 1.6315
noi 37

How to fix   Complexity   

Complexity

Complex classes like FuzzEd/static/script/node.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
define(['factory', 'property', 'mirror', 'canvas', 'class', 'config', 'jquery', 'jsplumb'],
2
function(Factory, Property, Mirror, Canvas, Class, Config) {
3
    /**
4
     * Package: Base
5
     */
6
7
    /**
8
     * Abstract Class: Node
9
     *      This class models the abstract base class for all nodes. It provides basic functionality for CRUD
10
     *      operations, setting up visual representation, dragging, selection, <Mirrors>, <Properties> and defines basic
11
     *      connection rules. Other classes, like e.g. <Editor> and <Graph>, rely on the interface provided by this
12
     *      class. It is therefore strongly recommended to inherit from <Node> and to add custom behaviour.
13
     *
14
     */
15
    return Class.extend({
16
        /**
17
         * Group: Members
18
         *      {DOMElement}    container           - The DOM element that contains all other visual DOM elements of the node
19
         *                                            such as its image, mirrors, ...
20
         *      {<Graph>}       graph               - The Graph this node belongs to.
21
         *      {Number}        id                  - A client-side generated id to uniquely identify the node in the
22
         *                                            frontend. It does NOT correlate with database ids in the backend.
23
         *                                            Introduced to save round-trips and to later allow for an offline
24
         *                                            mode.
25
         *      {Array[<Edge>]} incomingEdges       - An enumeration of all edges linking TO this node (this node is the
26
         *                                            target of the edge).
27
         *      {Array[<Edge>]} outgoingEdges       - An enumeration of all edges linking FROM this node (this node is
28
         *                                            the source of the edge).
29
         *      {DOMElement} _nodeImage             - DOM element that contains the actual image/svg of the node.
30
         *      {DOMElement} _badge                 - DOM element that contains the badge that can be used to display
31
         *                                            additional information on a node.
32
         *      {DOMElement} _nodeImageContainer    - A wrapper for the node image which is necessary to get position
33
         *                                            calculation working in Firefox.
34
         *      {DOMElement} _connectionHandle      - DOM element containing the visual representation of the handle
35
         *                                            where one can pull out new edges.
36
         */
37
        container:           undefined,
38
        graph:               undefined,
39
        id:                  undefined,
40
        nodegroups:          undefined,
41
        incomingEdges:       undefined,
42
        outgoingEdges:       undefined,
43
44
        _nodeImage:          undefined,
45
        _badge:              undefined,
46
        _nodeImageContainer: undefined,
47
        _connectionHandle:   undefined,
48
49
        /**
50
         * Group: Initialization
51
         */
52
53
        /**
54
         * Constructor: init
55
         *      The constructor of the abstract node class. It will merge the state of the definition, assign a
56
         *      client-side id, setup the visual representation and enable interaction via mouse and keyboard. Calling
57
         *      the constructor as-is, will result in an exception.
58
         *
59
         * Parameters:
60
         *      {Object}     definition                - An object containing default values for the node's definition.
61
         *                                               E.g.: {x: 1, y: 20, name: 'foo'}. The values will be merged
62
         *                                               into the node recursively, creating deep copies of complex
63
         *                                               structures like arrays or other objects. Mainly required for
64
         *                                               restoring the state of a node when loading a graph from the
65
         *                                               backend.
66
         *      {Array[String]} propertiesDisplayOrder - An enumeration of property names, sorted by the order in which
67
         *                                               the property with the respective name shall appear in the
68
         *                                               property menu. May contain names of properties that the node
69
         *                                               does not have.
70
         */
71
        init: function(definition) {
72
            // merge all presets of the configuration and data from the backend into this object
73
            jQuery.extend(true, this, definition);
74
75
            this.nodegroups    = {};
76
            this.incomingEdges = [];
77
            this.outgoingEdges = [];
78
79
            if (typeof this.id === 'undefined') this.id = this.graph.createId();
80
81
            // visuals
82
            jsPlumb.extend(this.connector, jsPlumb.Defaults.PaintStyle);
83
            this._setupVisualRepresentation()
84
                // Additional visual operations - cannot go to _setupVisualRepresentation since they require the
85
                // container to be already in the DOM/
86
                ._setupNodeImage()
87
                ._moveContainerToPixel(Canvas.toPixel(this.x, this.y))
88
                ._setupConnectionHandle()
89
                ._setupEndpoints()
90
				.__setupZIndex()
91
                // Interaction
92
                ._setupDragging()
93
                ._setupMouse()
94
                ._setupSelection()
95
                ._setupProperties()
96
				._setupEditable()
97
				._setupResizable()
98
                // Events
99
                ._registerEventHandlers();
100
101
            // call home
102
            jQuery(document).trigger(Factory.getModule('Config').Events.NODE_ADDED, [
103
                this.id,
104
                this.kind,
105
                this.x,
106
                this.y,
107
                this.toDict().properties
108
            ]);
109
        },
110
111
        /**
112
         * Method: _setupVisualRepresentation
113
         *      This method is used in the constructor to set up the visual representation of the node from its kind
114
         *      string. First, the node's image is created by cloning the corresponding thumbnail from the shape menu
115
         *      and formatting it. Then, it is appended to a freshly created div element becoming the node's
116
         *      <Node::container> element.
117
         *
118
         * Returns:
119
         *      This {<Node>} instance for chaining.
120
         */
121
        _setupVisualRepresentation: function() {
122
            // get the thumbnail, clone it and wrap it with a container (for labels)
123
            this._nodeImage = jQuery('#' + Factory.getModule('Config').IDs.SHAPES_MENU + ' #' + this.kind)
124
                .clone()
125
                // cleanup the thumbnail's specific properties
126
                .removeClass('ui-draggable')
127
                .removeAttr('id')
128
                // add new classes for the actual node
129
                .addClass(Factory.getModule('Config').Classes.NODE_IMAGE);
130
				
131
            this._badge = jQuery('<span class="badge"></span>')
132
                .hide();
133
134
            this._nodeImageContainer = jQuery('<div>')
135
                .append(this._nodeImage)
136
                .append(this._badge);
137
138
            this.container = jQuery('<div>')
139
                .attr('id', this.kind + this.id)
140
                .addClass(Factory.getModule('Config').Classes.NODE)
141
                .css('position', 'absolute')
142
                .data(Factory.getModule('Config').Keys.NODE, this)
143
                .append(this._nodeImageContainer);
144
			
145
            this.container.appendTo(Canvas.container);
146
147
            return this;
148
        },
149
		
150
        /**
151
         * Method: __setupZIndex
152
         *      This method implements special treatment of z-Index for sticky notes. Sticky notes are normally
153
         *      displayed behind all other nodes, as well as node connections with a z-Index value of "-1". If a sticky
154
         *      note gets selected it will be brought in front of all other nodes/connections with a z-Index value of
155
         *      "101". If a sticky note gets unselected it will be brought in the background again.
156
		 *
157
         * Returns:
158
         *      This {<Node>} instance for chaining.
159
         */
160
		__setupZIndex: function(){
161
			if (this.kind != 'stickyNote') return this;
162
163
			jQuery(this.container).css('z-index', '-1');
164
165
			jQuery(document).on(Factory.getModule('Config').Events.NODE_SELECTED, function(event, ui) {
166
				// bring only the sticky note in front that is selected
167
				if (jQuery(this.container).hasClass('ui-selected'))
168
					jQuery(this.container).css('z-index', '101');
169
            }.bind(this));
170
			
171
			jQuery(document).on(Factory.getModule('Config').Events.NODE_UNSELECTED, function() {
172
				jQuery(this.container).css('z-index', '-1')
173
            }.bind(this));
174
175
			return this;
176
		},
177
178
        /**
179
         * Method: _setupNodeImage
180
         *      Helper method used in the constructor to setup the actual image of the node. First off it scales the
181
         *      cloned thumbnail of the node from the shape menu up to the grid size (requires a number SVG of
182
         *      transformations on all groups) and sets the according image margins to snap to the grid. After that the
183
         *      <Node::_nodeImage> member is enriched with additional convenience values (primitives, groups, xCenter,
184
         *      yCenter) to allow faster processing in other methods.
185
         *
186
         * Returns:
187
         *      This {<Node>} instance for chaining.
188
         */
189
        _setupNodeImage: function() {
190
            // calculate the scale factor
191
            var marginOffset = this._nodeImage.outerWidth(true) - this._nodeImage.width();
192
            var scaleFactor  = (Canvas.gridSize - marginOffset) / this._nodeImage.height();
193
194
            // resize the svg and the groups
195
            this._nodeImage.attr('width',  this._nodeImage.width()  * scaleFactor);
196
            this._nodeImage.attr('height', this._nodeImage.height() * scaleFactor);
197
198
            var newTransform = 'scale(' + scaleFactor + ')';
199
            var groups = this._nodeImage.find('g');
200
201
            if (groups.attr('transform')) newTransform += ' ' + groups.attr('transform');
202
            groups.attr('transform', newTransform);
203
204
            // XXX: In Webkit browsers the container div does not resize properly. This should fix it.
205
            this.container.width(this._nodeImage.width());
206
            this.container.height(this._nodeImage.height());
207
208
            // cache center of the image
209
            // XXX: We need to use the node image's container's position because Firefox fails otherwise
210
            this._nodeImage.xCenter = this._nodeImageContainer.position().left + this._nodeImage.outerWidth(true)  / 2;
211
            this._nodeImage.yCenter = this._nodeImageContainer.position().top  + this._nodeImage.outerHeight(true) / 2;
212
213
            return this;
214
        },
215
216
        /**
217
         * Method: _setupConnectionHandle
218
         *      This initialization method is called in the constructor to setup the connection handles of the node.
219
         *      These are the small plus icons where one can drag edges out of. In the default implementation connectors
220
         *      are located at the bottom of the image centered.
221
         *
222
         * Returns:
223
         *      This {<Node>} instance for chaining.
224
         */
225
        _setupConnectionHandle: function() {
226
            if (this.numberOfOutgoingConnections != 0) {
227
                this._connectionHandle = jQuery('<i class="fa fa-plus"></i>')
228
                    .addClass(Factory.getModule('Config').Classes.NODE_HALO_CONNECT + ' ' + Factory.getModule('Config').Classes.NO_PRINT)
229
                    .css({
230
                        'top':  this._nodeImage.yCenter + this._nodeImage.outerHeight(true) / 2,
231
                        'left': this._nodeImage.xCenter
232
                    })
233
                    .appendTo(this.container);
234
235
                if (this.readOnly) this._connectionHandle.hide();
236
            }
237
238
            return this;
239
        },
240
241
        /**
242
         * Method: _setupEndpoints
243
         *      This initialization method does the setup work for endpoints. Endpoints are virtual entities that
244
         *      function as source as well as target of edges dragged from/to them. They define how many edges and
245
         *      coming from which node type may be connected to this node. This method serves as a dispatcher to the
246
         *      sub-methods <Node::_setupIncomingEndpoint> and <Node::_setupOutgoingEndpoint>.
247
         *
248
         * Returns:
249
         *      This {<Node>} instance for chaining.
250
         */
251
        _setupEndpoints: function() {
252
            var anchors = this._connectorAnchors();
253
            var offset  = this._connectorOffset();
254
255
            this._setupIncomingEndpoint(anchors.in,  offset.in)
256
                ._setupOutgoingEndpoint(anchors.out, offset.out);
257
258
            return this;
259
        },
260
261
        /**
262
         * Method: _setupIncomingEndpoint
263
         *      This method sets up the endpoint for incoming connections - i.e. the target - for a node. Therefore it
264
         *      uses the jsPlumb makeTarget function on the node's container. Nodes that disallow incoming connection do
265
         *      NOT have any endpoint target.
266
         *
267
         * Parameters:
268
         *      {Array[Number]} anchors          - jsPlumb tuple anchor definition as returned by <connectorAnchors>.
269
         *      {Object}        connectionOffset - Precise pixel offset of the endpoint in relation to the relative
270
         *                                         anchor position of the anchor parameter. See also <connectorOffset>.
271
         *
272
         * Returns:
273
         *      This {<Node>} instance for chaining.
274
         */
275
        _setupIncomingEndpoint: function(anchors, connectionOffset) {
276
            if (this.numberOfIncomingConnections === 0) return this;
277
278
            jsPlumb.makeTarget(this.container, {
279
                // position of the anchor
280
                anchor:         anchors.concat([connectionOffset.x, connectionOffset.y]),
281
                maxConnections: this.numberOfIncomingConnections,
282
                dropOptions: {
283
                    // Define which node can connect to the target...
284
                    accept: function(draggable) {
285
                        var elid = draggable.attr('elid');
286
                        // ...DOM elements without elid (generated by jsPlumb) are disallowed...
287
                        if (typeof elid === 'undefined') return false;
288
289
                        // ...as well as nodes without a node object representing it.
290
                        var sourceNode = jQuery('.' + Factory.getModule('Config').Classes.NODE + ':has(#' + elid + ')').data('node');
291
                        if (typeof sourceNode === 'undefined') return false;
292
293
                        // Ask the source node if it can connect to us.
294
                        return sourceNode.allowsConnectionsTo(this);
295
                    }.bind(this),
296
                    activeClass: Factory.getModule('Config').Classes.NODE_DROP_ACTIVE
297
                }
298
            });
299
300
            return this;
301
        },
302
303
        /**
304
         * Method: _setupOutgoingEndpoint
305
         *      This initialization method sets up the endpoint for outgoing connections - i.e. the source - for edges
306
         *      of this node. The source is in contrary of a target not the whole node, but only the connection handle
307
         *      of the node (see: <Node::_setupConnectionHandle>). The functionality is provided by the jsPlumb
308
         *      makeSource call. Nodes that do no allow for outgoing connections do NOT have any source endpoint. In
309
         *      this method you will also find the callbacks that are responsible for fading out target nodes that do
310
         *      not allow connection from this node.
311
         *
312
         * Parameters:
313
         *      {Array[Number]} anchors          - jsPlumb tuple anchor definition as returned by <connectorAnchors>.
314
         *      {Object}        connectionOffset - Precise pixel offset of the endpoint in relation to the relative
315
         *                                         anchor position of the anchor parameter. See also <connectorOffset>.
316
         *
317
         * Returns:
318
         *      This {<Node>} instance for chaining.
319
         */
320
        _setupOutgoingEndpoint: function(anchors, connectionOffset) {
321
            if (this.numberOfOutgoingConnections == 0) return this;
322
323
            // small flag for the drag callback, explanation below
324
            var highlight     = true;
325
            var inactiveNodes = '.' + Factory.getModule('Config').Classes.NODE + ':not(.'+ Factory.getModule('Config').Classes.NODE_DROP_ACTIVE + ')';
326
327
            jsPlumb.makeSource(this._connectionHandle, {
328
                parent:         this.container,
329
                anchor:         anchors.concat([connectionOffset.x, connectionOffset.y]),
330
                maxConnections: this.numberOfOutgoingConnections,
331
                connectorStyle: this.connector,
332
                dragOptions: {
333
                    cursor: Factory.getModule('Config').Dragging.CURSOR_EDGE,
334
                    // XXX: have to use drag callback here instead of start
335
                    // The activeClass assigned in <Node::_setupIncomingEndpoint> is unfortunately assigned only AFTER
336
                    // the execution of the start callback by jsPlumb.
337
                    drag: function() {
338
                        // using the highlight flag here to simulate only-once-behaviour (no re-computation of node set)
339
                        if (!highlight) return;
340
                        // disable all nodes that can not be targeted
341
                        jQuery(inactiveNodes).each(function(index, node){
342
                            jQuery(node).data(Factory.getModule('Config').Keys.NODE).disable();
343
                        }.bind(this));
344
                        highlight = false;
345
                    }.bind(this),
346
                    stop: function() {
347
                        // re-enable disabled nodes
348
                        jQuery(inactiveNodes).each(function(index, node){
349
                            jQuery(node).data(Factory.getModule('Config').Keys.NODE).enable();
350
                        }.bind(this));
351
                        // release the flag, to allow fading out nodes again
352
                        highlight = true;
353
                    }.bind(this)
354
                }
355
            });
356
357
            return this;
358
        },
359
360
        /**
361
         * Method: _setupDragging
362
         *      This initialization method is called in the constructor and is responsible for setting up the node's
363
         *      dragging functionality. A user is not able to position nodes freely but can only let nodes snap to grid,
364
         *      simulating checkered graph paper. The functionality below is multi select aware, meaning a user can drag
365
         *      multiple node at once.
366
         *
367
         * Returns:
368
         *      This {<Node>} instance for chaining.
369
         */
370
        _setupDragging: function() {
371
            if (this.readOnly) return this;
372
373
            var initialPositions = {};
374
            // using jsPlumb draggable and not jQueryUI to allow that edges move together with nodes
375
            jsPlumb.draggable(this.container, {
376
                // stay in the canvas
377
                containment: Canvas.container,
378
                // become a little bit opaque when dragged
379
                opacity:     Factory.getModule('Config').Dragging.OPACITY,
380
                // show a cursor with four arrows
381
                cursor:      Factory.getModule('Config').Dragging.CURSOR,
382
                // stick to the checkered paper
383
                grid:        [Canvas.gridSize, Canvas.gridSize],
384
                // when dragged a node is send to front, overlaying other nodes and edges
385
                stack:       '.' + Factory.getModule('Config').Classes.NODE + ', .' + Factory.getModule('Config').Classes.JSPLUMB_CONNECTOR,
386
387
                // start dragging callback
388
                start: function(event) {
389
                    // XXX: add dragged node to selection
390
                    // This uses the jQuery.ui.selectable internal functions.
391
                    // We need to trigger them manually because jQuery.ui.draggable doesn't propagate these events.
392
                    if (!this.container.hasClass(Factory.getModule('Config').Classes.SELECTED)) {
393
                        Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStart(event);
394
                        Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStop(event);
395
                    }
396
397
                    // capture the original positions of all (multi) selected nodes and save them
398
                    jQuery('.' + Factory.getModule('Config').Classes.SELECTED).each(function(index, node) {
399
                        var nodeInstance = jQuery(node).data(Factory.getModule('Config').Keys.NODE);
400
                        // if this DOM element does not have an associated node object, do nothing
401
                        if (typeof nodeInstance === 'undefined') return;
402
403
                        initialPositions[nodeInstance.id] = nodeInstance.container.position();
404
                    }.bind(this));
405
                }.bind(this),
406
407
                drag: function(event, ui) {
408
                    // enlarge canvas
409
					Canvas.enlarge({
410
                        x: ui.offset.left + ui.helper.width(),
411
                        y: ui.offset.top  + ui.helper.height()
412
                    });
413
414
                    // determine by how many pixels we moved from our original position (see: start callback)
415
                    var xOffset = ui.position.left - initialPositions[this.id].left;
416
                    var yOffset = ui.position.top  - initialPositions[this.id].top;
417
418
                    // tell all selected nodes to move as well, except this node; the user already dragged it
419
                    jQuery('.' + Factory.getModule('Config').Classes.SELECTED).not(this.container).each(function(index, node) {
420
                        var nodeInstance = jQuery(node).data(Factory.getModule('Config').Keys.NODE);
421
                        // if this DOM element does not have an associated node object, do nothing
422
                        if (typeof nodeInstance === 'undefined') return;
423
424
                        // move the other selectee by the dragging offset, do NOT report to the backend yet
425
                        nodeInstance._moveContainerToPixel({
426
                            'x': initialPositions[nodeInstance.id].left + xOffset + nodeInstance._nodeImage.xCenter,
427
                            'y': initialPositions[nodeInstance.id].top  + yOffset + nodeInstance._nodeImage.yCenter
428
                        });
429
                    }.bind(this));
430
                    jQuery(document).trigger(Factory.getModule('Config').Events.NODES_MOVED);
431
                }.bind(this),
432
433
                // stop dragging callback
434
                stop: function(event, ui) {
435
                    // ... calculate the final amount of pixels we moved...
436
                    var xOffset = ui.position.left - initialPositions[this.id].left;
437
                    var yOffset = ui.position.top  - initialPositions[this.id].top;
438
439
                    jQuery('.' + Factory.getModule('Config').Classes.SELECTED).each(function(index, node) {
440
                        var nodeInstance = jQuery(node).data(Factory.getModule('Config').Keys.NODE);
441
                        // if this DOM element does not have an associated node object, do nothing
442
                        if (typeof nodeInstance === 'undefined') return;
443
444
                        // ... and report to the backend this time because dragging ended
445
                        nodeInstance.moveToPixel({
446
                            'x': initialPositions[nodeInstance.id].left + xOffset + nodeInstance._nodeImage.xCenter,
447
                            'y': initialPositions[nodeInstance.id].top  + yOffset + nodeInstance._nodeImage.yCenter
448
                        });
449
                    }.bind(this));
450
451
                    // forget the initial position of the nodes to allow new dragging
452
                    initialPositions = {};
453
                    jQuery(document).trigger(Factory.getModule('Config').Events.NODE_DRAG_STOPPED);
454
                }.bind(this)
455
            });
456
457
            return this;
458
        },
459
460
        /**
461
         * Method: _setupMouse
462
         *      Small helper method used in the constructor for setting up mouse hover highlighting (highlight on hover,
463
         *      unhighlight on mouse out).
464
         *
465
         * Returns:
466
         *      This {<Node>} instance for chaining.
467
         */
468
        _setupMouse: function() {
469
            if (this.readOnly) return this;
470
            // hovering over a node
471
            this.container.hover(
472
                // mouse in
473
                this.highlight.bind(this),
474
                // mouse out
475
                this.unhighlight.bind(this)
476
            );
477
478
            return this;
479
        },
480
481
        /**
482
         * Method: _setupSelection
483
         *      This method sets up multi-select functionality for nodes.
484
         *
485
         * Returns:
486
         *      This {<Node>} instance for chaining.
487
         */
488
        _setupSelection: function() {
489
            if (this.readOnly) return this;
490
491
            //XXX: select a node on click
492
            // This uses the jQuery.ui.selectable internal functions.
493
            // We need to trigger them manually because only jQuery.ui.draggable gets the mouseDown events on nodes.
494
            this.container.click(function(event) {
495
                Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStart(event);
496
                Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStop(event);
497
            }.bind(this));
498
499
            return this;
500
        },
501
502
        /**
503
         * Method: _setupProperties
504
         *      Creates the node's properties instances sorted by the passed display order. If a property passed in the
505
         *      display order is not present in the node it is skipped silently. When a property in the node's
506
         *      definition is set to null the property is not created and eventually removed if inherited from its
507
         *      parent.
508
         *
509
         * Returns:
510
         *      This {<Node>} instance for chaining.
511
         */
512
        _setupProperties: function() {
513
            _.each(Factory.getNotation().propertiesDisplayOrder, function(propertyName) {
514
                var property = this.properties[propertyName];
515
516
                if (typeof property === 'undefined') {
517
                    return;
518
                } else if (property === null) {
519
                    delete this.properties[propertyName];
520
                    return;
521
                }
522
523
                property.name = propertyName;
524
                this.properties[propertyName] = Factory.getModule('Property').from(this, [ this ], property);
525
            }.bind(this));
526
527
            return this;
528
        },
529
		
530
		/**
531
		 * Method: _setupResizable
532
         *      This initialization method is called in the constructor and is responsible for setting up the resizing
533
         *      of a node. Until now the resizing functionality is intended only for use in resizing sticky notes on the
534
         *      canvas.
535
		 *
536
         * Returns:
537
         *      This {<Node>} instance for chaining.
538
		 */
539
		_setupResizable: function() {
540
			if (!this.resizable) return this;
541
			// adapt size of container div to inner resizable div
542
			this.container.width('auto').height('auto');
543
			
544
			var properties = this.properties;
545
			var _nodeImage = this._nodeImage;
546
			
547
			var height = properties.height.value;
548
			var width  = properties.width.value;
549
		
550
			var resizable = this.container.find('.' + Factory.getModule('Config').Classes.RESIZABLE).resizable();
551
			// setup resizable with width/height values stored in the backend
552
			jQuery(resizable).height(height).width(width);
553
			
554
			// if resizable gets resized update width/height attribute		
555
			jQuery(resizable).on('resizestop',function() {
556
				var newWidth = jQuery(this).width();
557
				var newHeight= jQuery(this).height();
558
			
559
				properties.width.setValue(newWidth);
560
				properties.height.setValue(newHeight);
561
				// update central point after resizing
562
	            _nodeImage.xCenter = jQuery(this).outerWidth() / 2;
563
	            _nodeImage.yCenter = jQuery(this).outerHeight() / 2;
564
			});
565
			
566
			jQuery(resizable).on('resize',function(event, ui) {
567
                // enlarge canvas if resizable is resized out of the canvas
568
				Canvas.enlarge({
569
                    x: ui.helper.offset().left + ui.helper.width(),
570
                    y: ui.helper.offset().top  + ui.helper.height()
571
                });
572
				
573
				// scroll canvas if resizable is resized out of the visible part of the canvas
574
				var scrollable    = jQuery('body');
575
				var screenWidth   = jQuery(window).width();
576
				var screenHeight  = jQuery(window).height();
577
				var rightScrolled = scrollable.scrollLeft();
578
				var downScrolled  = scrollable.scrollTop();
579
				var scrollOffset  = Factory.getModule('Config').Resizable.SCROLL_OFFSET;
580
				
581
				// resize to the right	-> scroll right			
582
				if (event.clientX > screenWidth - scrollOffset)
583
					scrollable.scrollLeft(rightScrolled + Canvas.gridSize);
584
				// resize to the left	-> scroll left
585
				if (event.clientX <= scrollOffset)
586
					scrollable.scrollLeft(rightScrolled - Canvas.gridSize);
587
				
588
				// resize downwards -> scroll downwards
589
				if (event.clientY > screenHeight - scrollOffset)
590
					scrollable.scrollTop(downScrolled + Canvas.gridSize);
591
				// resize upwards  -> scroll upwards
592
				if (event.clientY <= scrollOffset)
593
					scrollable.scrollTop(downScrolled - Canvas.gridSize);
594
			}.bind(this));
595
			
596
			return this;
597
		},
598
		
599
		/**
600
		 * Method: _setupEditable
601
         *      This initialization method is called in the constructor and is responsible for setting up the editing
602
         *      of a node. Until now the editing functionality is intended only for editing sticky notes on the canvas.
603
		 *      If the sticky note is clicked ,the html paragraph inside the sticky note will be hidden and the
604
         *      textarea will be displayed.
605
		 *
606
         * Returns:
607
         *      This {<Node>} instance for chaining.
608
		 */
609
		_setupEditable: function() {
610
			if (!this.editable) return this;
611
			
612
			var container = this.container
613
			var editable  = container.find('.'+ Factory.getModule('Config').Classes.EDITABLE);
614
			var textarea  = editable.find('textarea');				
615
			var paragraph = editable.find('p');
616
						
617
			editable.on('dblclick', function(event) {
618
				paragraph.toggle(false);
619
				textarea.toggle(true).focus();
620
			});
621
			
622
			jQuery(document).on(Factory.getModule('Config').Events.NODE_UNSELECTED, function(event) {
623
				textarea.blur();
624
			});
625
				
626
			return this;
627
		}, 
628
629
        /**
630
         *  Group: Event Handling
631
         */
632
633
        /**
634
         *  Method: _registerEventHandlers
635
         *      Register a listener for edge add and delete events so that we can check the edge count and hide the
636
         *      connection handle in case we are 'full'.
637
         *
638
         *  Returns:
639
         *      This {<Node>} instance for chaining.
640
         */
641
        _registerEventHandlers: function() {
642
            jQuery(document).on(Factory.getModule('Config').Events.EDGE_ADDED,   this._checkEdgeCapacity.bind(this));
643
            jQuery(document).on(Factory.getModule('Config').Events.EDGE_DELETED, this._checkEdgeCapacity.bind(this));
644
645
            jQuery(document).on(Factory.getModule('Config').Events.NODE_SELECTED, function(event, ui) {
646
                if (jQuery(ui.selected).data('node') == this) {
647
                    this.select();
648
                }
649
            }.bind(this));
650
651
            jQuery(document).on(Factory.getModule('Config').Events.NODE_UNSELECTED, function(event, ui) {
652
                if (jQuery(ui.unselected).data('node') == this) {
653
                    this.deselect();
654
                }
655
            }.bind(this));
656
657
            return this;
658
        },
659
660
        /**
661
         *  Method: _checkEdgeCapacity
662
         *      Check if we reached the max. number of outgoing edges allowed and hide the connection handle
663
         *      in this case.
664
         *
665
         *  Returns:
666
         *      This {<Node>} instance for chaining.
667
         */
668
        _checkEdgeCapacity: function() {
669
            // no need to hide the connection handle if there is none or if we allow infinite connections
670
            if (typeof this._connectionHandle === 'undefined' || this.numberOfOutgoingConnections == -1) return;
671
672
            if (this.outgoingEdges.length >= this.numberOfOutgoingConnections) {
673
                // full
674
                this._connectionHandle.hide();
675
            } else {
676
                this._connectionHandle.show();
677
            }
678
679
            return this;
680
        },
681
682
        /**
683
         * Group: Configuration
684
         */
685
686
        /**
687
         * Method: _connectorAnchors
688
         *      This method returns the position of the anchors for the connectors. The format is taken from jsPlumb and
689
         *      is defined as a four-tuple: [x, y, edge_x, edge_y]. The values for x and y define the relative offset of
690
         *      the connector from the left upper corner of the container. Edge_x and edgy_y determine the direction of
691
         *      connected edges pointing from or to the connector.
692
         *
693
         * Returns:
694
         *      {Object} with 'in' and 'out' keys containing jsPlumb four-tuple connector definitions.
695
         */
696
        _connectorAnchors: function() {
697
            return {
698
                'in':  [0.5, 0, 0, -1],
699
                'out': [0.5, 0, 0,  1]
700
            }
701
        },
702
703
        /**
704
         * Method: _connectorOffset
705
         *      This method returns an object with additional precise pixel offsets for the connectors as defined in
706
         *      <Node::_connectorAnchors>.
707
         *
708
         * Returns:
709
         *      {Object} with 'in' and 'out' keys containing {Objects} with pixel offsets of the connectors.
710
         */
711
        _connectorOffset: function() {
712
            // XXX: We need to use the offset of the image container because FF has difficulties to calculate the
713
            //      offsets of inline SVG elements directly.
714
            var topOffset    = this._nodeImageContainer.offset().top - this.container.offset().top;
715
            var bottomOffset = topOffset + this._nodeImage.height() + this.connector.offset.bottom;
716
717
            return {
718
                'in': {
719
                    'x': 0,
720
                    'y': topOffset
721
                },
722
                'out': {
723
                    'x': 0,
724
                    'y': bottomOffset
725
                }
726
            }
727
        },
728
729
        /**
730
         * Group: Logic
731
         */
732
733
        /**
734
         * Method: allowsConnectionTo
735
         *      This method checks if it is allowed to draw an object between this node (source) and the other node
736
         *      passed as parameter (target). Connections are allowed if and only if, the node does not connect to
737
         *      itself, the outgoing connections of this node, respectively the incoming connections of the other node
738
         *      are not exceeded and the notation file allows a connection between these two nodes.
739
         *
740
         * Parameters:
741
         *      {<Node>} otherNode - the node instance to connect to
742
         *
743
         * Returns:
744
         *      A {Boolean} that is true if the connection is allowed or false otherwise
745
         */
746
        allowsConnectionsTo: function(otherNode) {
747
            // no connections to same node
748
            if (this === otherNode) return false;
749
750
            // otherNode must be in the 'allowConnectionTo' list defined in the notations
751
            var allowed = _.any(this.allowConnectionTo, function(nodeClass) { return otherNode instanceof nodeClass; });
752
            if (!allowed) return false;
753
754
            // there is already a connection between these nodes
755
            var connections = jsPlumb.getConnections({
756
                //TODO: the selector should suffice, but due to a bug in jsPlumb we need the IDs here
757
                //TODO: maybe that is fixed in a newer version of jsPlumb
758
                source: this.container.attr('id'),
759
                target: otherNode.container.attr('id')
760
            });
761
762
            if (connections.length != 0) return false;
763
            // no connection if endpoint is full
764
            var endpoints = jsPlumb.getEndpoints(otherNode.container);
765
            if (endpoints) {
766
                //TODO: find a better way to determine endpoint
767
                var targetEndpoint = _.find(endpoints, function(endpoint){
768
                    return endpoint.isTarget || endpoint._makeTargetCreator
769
                });
770
                if (targetEndpoint && targetEndpoint.isFull()) return false;
771
            }
772
773
            return true;
774
        },
775
776
        //TODO: documentation
777
        addToNodeGroup: function(nodegroup) {
778
            this.nodegroups[nodegroup.id] = nodegroup;
779
        },
780
781
        removeFromNodeGroup: function(nodegroup) {
782
            delete this.nodegroups[nodegroup.id];
783
        },
784
785
        // the internal variant of removeFromNodeGroup also notifies the nodegroup of the removal
786
        _removeFromNodeGroup: function(nodegroup) {
787
            nodegroup.removeNode(this);
788
789
            this.removeFromNodeGroup(nodegroup);
790
        },
791
792
        /**
793
         * Method: setChildProperties
794
         *      This method will evaluate and set the properties of the passed child according to the value specified
795
         *      in the childProperties key of the notation file.
796
         *
797
         * Parameters:
798
         *      {<Node>} other - the child of the current node.
799
         *
800
         * Returns:
801
         *      This {<Node>} instance for chaining.
802
         */
803
        setChildProperties: function(otherNode) {
804
            _.each(this.childProperties, function(childValues, propertyName) {
805
                var property = otherNode.properties[propertyName];
806
                if (typeof property === 'undefined' || property === null) return;
807
808
                _.each(childValues, function(value, key) {
809
                    if (key === 'hidden') {
810
                        property.setHidden(value);
811
                    } else if (key === 'value') {
812
                        property.setValue(value);
813
                    }
814
                });
815
            });
816
817
            return this;
818
        },
819
820
        /**
821
         * Method: restoreChildProperties
822
         *      This method will reset the children properties previously enforced this node.
823
         *
824
         * Parameters:
825
         *     {<Node>} other - the child of the current node.
826
         *
827
         * Returns:
828
         *      This {<Node>} instance for chaining.
829
         */
830
        restoreChildProperties: function(otherNode) {
831
            _.each(this.childProperties, function(childValue, propertyName) {
832
                var property = otherNode.properties[propertyName];
833
                if (typeof property === 'undefined' || property === null) return;
834
835
                _.each(childValue, function(value, key) {
836
                    if (key === 'hidden') {
837
                        property.setHidden(!value);
838
                    }
839
                });
840
            });
841
842
            return this;
843
        },
844
845
        /**
846
         * Group: Accessors
847
         */
848
849
        /**
850
         * Method: getChildren
851
         *
852
         * Returns:
853
         *      All direct children as {Array[{<Node}>]} of this node.
854
         */
855
        getChildren: function() {
856
            var children = [];
857
            _.each(this.outgoingEdges, function(edge) {
858
                children.push(edge.target);
859
            }.bind(this));
860
            return children;
861
        },
862
863
         /**
864
         * Method: toDict
865
         *
866
         * Returns:
867
         *      A key-value {Object} representing of this node.
868
         */
869
        toDict: function() {
870
            var properties = _.map(this.properties, function(prop) { return prop.toDict() });
871
872
            return {
873
                properties: _.reduce(properties, function(memo, prop) {
874
                    return _.extend(memo, prop);
875
                }, {}),
876
                id:       this.id,
877
                kind:     this.kind,
878
                x:        this.x,
879
                y:        this.y,
880
                outgoing: this.outgoing,
881
                incoming: this.incoming
882
            };
883
        },
884
885
        /**
886
         *  Method: _hierarchy
887
         *          Recursively computes a dictionary representation of this node's hierarchy. NOTE: This works
888
         *          currently only with trees. Circular structures, like arbitrary graphs, will produce infinite loops.
889
         *
890
         *  Returns:
891
         *      A dictionary representation of this node's hierarchy. Each entry represents a node with its ID and a
892
         *      list of children.
893
         */
894
        _hierarchy: function() {
895
            var result = {id: this.id};
896
897
            var children = this.getChildren();
898
            if (children.length != 0) {
899
                result.children = _.map(children, function(node) {return node._hierarchy();});
900
            }
901
902
            return result;
903
        },
904
905
        /**
906
         * Group: DOM Manipulation
907
         */
908
909
        /**
910
         * Method: moveBy
911
         *      Moves the node's visual representation by the given offset and reports to backend. The center of the
912
         *      node's image is the anchor point for the translation.
913
         *
914
         * Parameters:
915
         *   {  Object} offset - Object of the form of {x: ..., y: ...} containing the pixel offset to move the node by.
916
         *
917
         * Returns:
918
         *      This {<Node>} instance for chaining.
919
         */
920
        moveBy: function(offset) {
921
            var position = this.container.position();
922
923
            return this.moveToPixel({
924
                x: position.left + this._nodeImage.xCenter + offset.x,
925
                y: position.top  + this._nodeImage.yCenter + offset.y
926
            });
927
        },
928
929
        /**
930
         * Method: moveToPixel
931
         *      Moves the node's visual representation to the given coordinates and reports to backend. The center of
932
         *      the node's image is the anchor point for the translation.
933
         *
934
         * Parameters:
935
         *      {Object} position  - Object in the form of {x: ..., y: ...} containing the pixel coordinates to move the
936
         *                           node to.
937
         *      {Boolean} animated - [optional] If true, the node repositioning is animated.
938
         *
939
         * Returns:
940
         *      This {<Node>} instance for chaining.
941
         */
942
        moveToPixel: function(position, animated) {
943
            var gridPos = Canvas.toGrid(position);
944
            this.x = Math.max(gridPos.x, 0);
945
            this.y = Math.max(gridPos.y, 0);
946
947
            this._moveContainerToPixel(position, animated);
948
            // call home
949
            jQuery(document).trigger(Factory.getModule('Config').Events.NODE_PROPERTY_CHANGED, [this.id, {'x': this.x, 'y': this.y}]);
950
951
            return this;
952
        },
953
954
        /**
955
         * Method: moveToGrid
956
         *      Moves the node's visual representation to the given grid coordinates and reports to backend.
957
         *
958
         * Parameters:
959
         *      {Object} position  - Object in the form of {x: ..., y: ...} containing the grid coordinates to move the
960
         *                           node to.
961
         *      {Boolean} animated - [optional] If true, the node repositioning is animated.
962
         *
963
         * Returns:
964
         *      This {<Node>} instance for chaining.
965
         */
966
        moveToGrid: function(gridPos, animated) {
967
            this.x = Math.floor(Math.max(gridPos.x, 1));
968
            this.y = Math.floor(Math.max(gridPos.y, 1));
969
970
            var pixelPos = Canvas.toPixel(this.x, this.y);
971
            this._moveContainerToPixel(pixelPos, animated);
972
973
            // call home
974
            jQuery(document).trigger(Factory.getModule('Config').Events.NODE_PROPERTY_CHANGED, [this.id, {'x': this.x, 'y': this.y}]);
975
976
            return this;
977
        },
978
979
        /**
980
         * Method: remove
981
         *      Removes the whole visual representation including endpoints from the canvas.
982
         *
983
         * Returns:
984
         *      A {Boolean} indicating successful node deletion.
985
         */
986
        remove: function() {
987
            if (!this.deletable) return false;
988
989
            // Remove all incoming and outgoing edges
990
            _.each(_.union(this.incomingEdges, this.outgoingEdges), function(edge) {
991
                this.graph.deleteEdge(edge);
992
            }.bind(this));
993
994
            // Tell jsPlumb to remove node endpoints
995
            _.each(jsPlumb.getEndpoints(this.container), function(endpoint) {
996
                jsPlumb.deleteEndpoint(endpoint);
997
            });
998
999
            // Remove us from all NodeGroups we are part of
1000
            _.each(this.nodegroups, function(nodegroup) {
1001
                this._removeFromNodeGroup(nodegroup);
1002
            }.bind(this));
1003
1004
            // Call home
1005
            jQuery(document).trigger(Factory.getModule('Config').Events.NODE_DELETED, this.id);
1006
            this.container.remove();
1007
1008
            return true;
1009
        },
1010
1011
        /**
1012
         * Method: _moveContainerToPixel
1013
         *      Moves the node's <Node::container> to the pixel position specified in the position parameter. Does not
1014
         *      take <Canvas> offset or the grid into account. The node's image center is the anchor point for the
1015
         *      translation.
1016
         *
1017
         * Parameters:
1018
         *      {Object}  position - Object of the form {x: ..., y: ...}, where x and y point to integer pixel values
1019
         *                           where the node's container shall be moved to.
1020
         *      {Boolean} animated - [optional] If true, the node repositioning is animated.
1021
         *
1022
         * Returns:
1023
         *      This {<Node>} instance for chaining.
1024
         */
1025
        _moveContainerToPixel: function(position, animated) {
1026
            var halfGrid = Canvas.gridSize / 2;
1027
            var x        = Math.max(position.x - this._nodeImage.xCenter, halfGrid);
1028
            var y        = Math.max(position.y - this._nodeImage.yCenter, halfGrid);
1029
1030
            if (animated) {
1031
                jsPlumb.animate(this.container.attr('id'), {
1032
                    left: x,
1033
                    top:  y
1034
                }, {
1035
                    duration: 200,
1036
                    queue:    false,
1037
                    done:     function() { Canvas.enlarge(position); }
1038
                });
1039
            } else {
1040
                this.container.css({
1041
                    left: x,
1042
                    top:  y
1043
                });
1044
                Canvas.enlarge(position);
1045
                // ask jsPlumb to repaint the selectee in order to redraw its connections
1046
                jsPlumb.repaint(this.container);
1047
            }
1048
1049
            return this;
1050
        },
1051
1052
        /**
1053
         * Group: Highlighting
1054
         */
1055
1056
        /**
1057
         * Method: disable
1058
         *      Disables the node visually (fade out) to make it appear to be not interactive for the user.
1059
         *
1060
         * Returns:
1061
         *      This {<Node>} instance for chaining.
1062
         */
1063
        disable: function() {
1064
            this.container.addClass(Factory.getModule('Config').Classes.DISABLED);
1065
1066
            return this;
1067
        },
1068
1069
        /**
1070
         * Method: enable
1071
         *      This method node re-enables the node visually and makes appear interactive to the user.
1072
         *
1073
         * Returns:
1074
         *      This {<Node>} instance for chaining.
1075
         */
1076
        enable: function() {
1077
            this.container.removeClass(Factory.getModule('Config').Classes.DISABLED);
1078
1079
            return this;
1080
        },
1081
1082
        /**
1083
         * Method: select
1084
         *      Marks the node as selected by adding the corresponding CSS class.
1085
         *
1086
         * Returns:
1087
         *      This {<Node>} instance for chaining.
1088
         */
1089
        select: function() {
1090
            this.container.addClass(Factory.getModule('Config').Classes.SELECTED);
1091
1092
            return this;
1093
        },
1094
1095
        /**
1096
         * Method: deselect
1097
         *      This method deselects the node by removing the corresponding CSS class to it.
1098
         *
1099
         * Returns:
1100
         *      This {<Node>} instance for chaining.
1101
         */
1102
        deselect: function() {
1103
            this.container.removeClass(Factory.getModule('Config').Classes.SELECTED);
1104
1105
            return this;
1106
        },
1107
1108
        /**
1109
         * Method: highlight
1110
         *      This method highlights the node visually as long as the node is not already disabled or selected. It is
1111
         *      for instance called when the user hovers over a node.
1112
         *
1113
         * Returns:
1114
         *      This {<Node>} instance for chaining.
1115
         */
1116
        highlight: function() {
1117
            this.container.addClass(Factory.getModule('Config').Classes.HIGHLIGHTED);
1118
1119
            return this;
1120
        },
1121
1122
        /**
1123
         * Method: unhighlight
1124
         *      Unhighlights the node' visual appearance. The method is for instance calls when the user leaves a
1125
         *      hovered node. P.S.: The weird word unhighlighting is an adoption of the jQueryUI dev team speak, all
1126
         *      credits to them :)!
1127
         *
1128
         * Returns:
1129
         *      This {<Node>} instance for chaining.
1130
         */
1131
        unhighlight: function() {
1132
            this.container.removeClass(Factory.getModule('Config').Classes.HIGHLIGHTED);
1133
1134
            return this;
1135
        },
1136
1137
        /**
1138
         * Method: showBadge
1139
         *      Display a badge on the node.
1140
         *
1141
         * Parameters:
1142
         *      {String} text  - The text that should be displayed in the badge.
1143
         *      {String} style - [optional] The style (color) of the badge.
1144
         *                       See http://twitter.github.io/bootstrap/components.html#labels-badges.
1145
         *
1146
         * Returns:
1147
         *      This {<Node>} instance for chaining.
1148
         */
1149
        showBadge: function(text, style) {
1150
            this._badge
1151
                .text(text)
1152
                .addClass('badge')
1153
                .show();
1154
            if (typeof style !== 'undefined')
1155
                this._badge.addClass('badge-' + style);
1156
1157
            return this;
1158
        },
1159
1160
        /**
1161
         * Method: hideBadge
1162
         *      Hides the badge displayed on the node.
1163
         *
1164
         * Returns:
1165
         *      This {<Node>} instance for chaining.
1166
         */
1167
        hideBadge: function() {
1168
            this._badge
1169
                .text('')
1170
                .removeClass()
1171
                .hide();
1172
1173
            return this;
1174
        }
1175
    });
1176
});
1177