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

FuzzEd/static/script/editor.js   F

Complexity

Total Complexity 126
Complexity/F 1.88

Size

Lines of Code 1086
Function Count 67

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 2 Features 0
Metric Value
cc 0
dl 0
loc 1086
rs 2.3726
c 3
b 2
f 0
wmc 126
nc 82944
mnd 10
bc 105
fnc 67
bpm 1.5671
cpm 1.8805
noi 35

How to fix   Complexity   

Complexity

Complex classes like FuzzEd/static/script/editor.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', 'class', 'menus', 'canvas', 'backend', 'alerts', 'progress_indicator', 'jquery-classlist', 'jsplumb'],
2
function(Factory, Class, Menus, Canvas, Backend, Alerts, Progress) {
3
    /**
4
     *  Package: Base
5
     */
6
7
    /**
8
     * Class: Editor
9
     *      This is the _abstract_ base class for all graph-kind-specific editors. It manages the visual components
10
     *      like menus and the graph itself. It is also responsible for global keybindings.
11
     */
12
    return Class.extend({
13
        /**
14
         * Group: Members:
15
         *      {<Graph>}             graph                         - <Graph> instance to be edited.
16
         *      {<PropertiesMenu>}    properties                    - The <PropertiesMenu> instance used by this editor
17
         *                                                            for changing the properties of nodes of the edited
18
         *                                                            graph.
19
         *      {<ShapesMenu>}        shapes                        - The <Menu::ShapeMenu> instance use by this editor
20
         *                                                            to show the available shapes for the kind of the
21
         *                                                            edited graph.
22
         *      {<Backend>}           _backend                      - The instance of the <Backend> that is used to
23
         *                                                            communicate graph changes to the server.
24
         *      {Object}              _currentMinContentOffsets     - Previously calculated minimal content offsets.
25
         *      {jQuery Selector}     _nodeOffsetPrintStylesheet    - The dynamically generated stylesheet used to fix
26
         *                                                            the node offset when printing the page.
27
         *      {Underscore Template} _nodeOffsetStylesheetTemplate - The underscore.js template used to generate the
28
         *                                                            CSS transformation for the print offset.
29
         */
30
        graph:                         undefined,
31
        properties:                    undefined,
32
        shapes:                        undefined,
33
        layout:                        undefined,
34
35
        _backend:                      undefined,
36
        _currentMinNodeOffsets:        {'top': 0, 'left': 0},
37
        _nodeOffsetPrintStylesheet:    undefined,
38
        _nodeOffsetStylesheetTemplate: undefined,
39
40
        _clipboard:                    undefined,
41
42
        /**
43
         * Group: Initialization
44
         */
45
46
        /**
47
         * Constructor: init
48
         *      Sets up the editor interface, handlers and loads the graph with the given ID from the backend.
49
         *
50
         * Parameters:
51
         *      {Number} graphId - The ID of the graph that is going to be edited by this editor.
52
         */
53
        init: function(graphId) {
54
            if (typeof graphId !== 'number')
55
                throw new TypeError('numeric graph ID', typeof graphId);
56
57
            this._backend = Backend.establish(graphId);
58
59
            // remember certain UI elements
60
            this._progressIndicator = jQuery('#' + Factory.getModule('Config').IDs.PROGRESS_INDICATOR);
61
            this._progressMessage = jQuery('#' + Factory.getModule('Config').IDs.PROGRESS_MESSAGE);
62
63
            // run a few sub initializer
64
            this._setupJsPlumb()
65
                ._setupNodeOffsetPrintStylesheet()
66
                ._setupEventCallbacks()
67
                ._setupMenuActions()
68
                ._setupDropDownBlur();
69
70
            // fetch the content from the backend
71
            this._loadGraph(graphId);
72
        },
73
74
        /**
75
         * Group: Graph Loading
76
         */
77
78
        /**
79
         * Method: _loadGraph
80
         *      Asynchronously loads the graph with the given ID from the backend. <_loadGraphFromJson> will be called
81
         *      when retrieval was successful.
82
         *
83
         * Parameters:
84
         *      {Number} graphId - ID of the graph that should be loaded.
85
         *
86
         * Returns:
87
         *      This {<Editor>} instance for chaining.
88
         */
89
        _loadGraph: function(graphId) {
90
            this._backend.getGraph(
91
                this._loadGraphFromJson.bind(this),
92
                this._loadGraphError.bind(this)
93
            );
94
95
            return this;
96
        },
97
98
        /**
99
         * Method: _loadGraphCompleted
100
         *      Callback that gets fired when the graph is loaded completely. We need to perform certain actions
101
         *      afterwards, like initialization of menus and activation of backend observers to prevent calls to the
102
         *      backend while the graph is initially constructed.
103
         *
104
         * Returns:
105
         *      This {<Editor>} instance for chaining.
106
         */
107
        _loadGraphCompleted: function(readOnly) {
108
            // create manager objects for the bars
109
            this.properties = Factory.create(Factory.getModule('Menus').PropertiesMenu, Factory.getNotation().propertiesDisplayOrder);
110
            this.shapes     = Factory.create(Factory.getModule('Menus').ShapeMenu);
111
            this.layout     = Factory.create(Factory.getModule('Menus').LayoutMenu);
112
113
            this.graph.layoutMenu = this.layout;
114
            this._backend.activate();
115
116
            if (readOnly) {
117
                Alerts.showInfoAlert('Remember:', 'this diagram is read-only');
118
                this.shapes.disable();
119
                this.properties.disable();
120
                Canvas.disableInteraction();
121
            }
122
123
            // update the available menu items for the first time
124
            this._updateMenuActions();
125
126
            // enable user interaction
127
            this._setupMouse()
128
                ._setupKeyBindings(readOnly);
129
130
            // fade out the splash screen
131
            jQuery('#' + Factory.getModule('Config').IDs.SPLASH).fadeOut(Factory.getModule('Config').Splash.FADE_TIME, function() {
132
                jQuery(this).remove();
133
            });
134
135
            return this;
136
        },
137
138
        /**
139
         * Method: _loadGraphError
140
         *      Callback that gets called in case <_loadGraph> results in an error.
141
         */
142
        _loadGraphError: function(response, textStatus, errorThrown) {
143
            throw new NetworkError('could not retrieve graph');
144
        },
145
146
        /**
147
         * Method: _loadGraphFromJson
148
         *      Callback triggered by the backend, passing the loaded JSON representation of the graph. It will
149
         *      initialize the editor's graph instance using the <Graph> class returned in <getGraphClass>.
150
         *
151
         * Parameters:
152
         *      {Object} json - JSON representation of the graph, loaded from the backend.
153
         *
154
         *  Returns:
155
         *      This {<Editor>} instance for chaining.
156
         */
157
        _loadGraphFromJson: function(json) {
158
            this.graph = Factory.create('Graph', json);
159
            this._loadGraphCompleted(json.readOnly);
160
161
            return this;
162
        },
163
164
        /**
165
         *  Group: Setup
166
         */
167
        
168
        /**
169
         * Method: _setupDropDownBlur
170
         *      Register an event handler that takes care of closing and blurring all currently open drop down menu
171
         *      items from the toolbar.
172
         *
173
         * Returns:
174
         *      This {<Editor>} instance for chaining.
175
         */
176
        _setupDropDownBlur: function () {
177
            jQuery('#' + Factory.getModule('Config').IDs.CANVAS).mousedown(function(event) {
178
                // close open bootstrap dropdown
179
                jQuery('.dropdown.open')
180
                    .removeClass('open')
181
                    .find('a')
182
                    .blur();
183
            });
184
            
185
            return this;
186
        },
187
188
        /**
189
         * Method: _setupMenuActions
190
         *      Registers the event handlers for graph type - independent menu entries that trigger JS calls
191
         *
192
         * Returns:
193
         *      This {<Editor>} instance for chaining.
194
         */
195
        _setupMenuActions: function() {
196
            jQuery('#' + Factory.getModule('Config').IDs.ACTION_GRID_TOGGLE).click(function() {
197
                Canvas.toggleGrid();
198
            }.bind(this));
199
200
            jQuery('#' + Factory.getModule('Config').IDs.ACTION_CUT).click(function() {
201
                this._cutSelection();
202
            }.bind(this));
203
204
            jQuery('#' + Factory.getModule('Config').IDs.ACTION_COPY).click(function() {
205
                this._copySelection();
206
            }.bind(this));
207
208
            jQuery('#' + Factory.getModule('Config').IDs.ACTION_PASTE).click(function() {
209
                this._paste();
210
            }.bind(this));
211
212
            jQuery('#' + Factory.getModule('Config').IDs.ACTION_DELETE).click(function() {
213
                this._deleteSelection();
214
            }.bind(this));
215
216
            jQuery('#' + Factory.getModule('Config').IDs.ACTION_SELECTALL).click(function(event) {
217
                this._selectAll(event);
218
            }.bind(this));
219
220
            jQuery('#' + Factory.getModule('Config').IDs.ACTION_LAYOUT_CLUSTER).click(function() {
221
                this.graph._layoutWithAlgorithm(this.graph._getClusterLayoutAlgorithm());
222
            }.bind(this));
223
224
            jQuery('#' + Factory.getModule('Config').IDs.ACTION_LAYOUT_TREE).click(function() {
225
                this.graph._layoutWithAlgorithm(this.graph._getTreeLayoutAlgorithm());
226
            }.bind(this));
227
228
            // set the shortcut hints from 'Ctrl+' to '⌘' when on Mac
229
            if (navigator.platform == 'MacIntel' || navigator.platform == 'MacPPC') {
230
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_CUT + ' span').text('⌘X');
231
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_COPY + ' span').text('⌘C');
232
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_PASTE + ' span').text('⌘P');
233
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_SELECTALL + ' span').text('⌘A');
234
            }
235
236
            return this;
237
        },
238
239
        /**
240
         * Method: _setupJsPlumb
241
         *      Sets all jsPlumb defaults used by this editor.
242
         *
243
         * Returns:
244
         *      This {<Editor>} instance for chaining.
245
         */
246
        _setupJsPlumb: function() {
247
            jsPlumb.importDefaults({
248
                EndpointStyle: {
249
                    fillStyle:    Factory.getModule('Config').JSPlumb.ENDPOINT_FILL
250
                },
251
                Endpoint:        [Factory.getModule('Config').JSPlumb.ENDPOINT_STYLE, {
252
                    radius:       Factory.getModule('Config').JSPlumb.ENDPOINT_RADIUS,
253
                    cssClass:     Factory.getModule('Config').Classes.JSPLUMB_ENDPOINT,
254
                    hoverClass:   Factory.getModule('Config').Classes.HIGHLIGHTED
255
                }],
256
                PaintStyle: {
257
                    strokeStyle:  Factory.getModule('Config').JSPlumb.STROKE_COLOR,
258
                    lineWidth:    Factory.getModule('Config').JSPlumb.STROKE_WIDTH,
259
                    outlineColor: Factory.getModule('Config').JSPlumb.OUTLINE_COLOR,
260
                    outlineWidth: Factory.getModule('Config').JSPlumb.OUTLINE_WIDTH
261
                },
262
                HoverPaintStyle: {
263
                    strokeStyle:  Factory.getModule('Config').JSPlumb.STROKE_COLOR_HIGHLIGHTED
264
                },
265
                HoverClass:       Factory.getModule('Config').Classes.HIGHLIGHTED,
266
                Connector:       [Factory.getModule('Config').JSPlumb.CONNECTOR_STYLE, Factory.getModule('Config').JSPlumb.CONNECTOR_OPTIONS],
267
                ConnectionsDetachable: false,
268
                ConnectionOverlays: Factory.getModule('Config').JSPlumb.CONNECTION_OVERLAYS
269
            });
270
271
            jsPlumb.connectorClass = Factory.getModule('Config').Classes.JSPLUMB_CONNECTOR;
272
273
            return this;
274
        },
275
276
        /**
277
         * Method: _setupMouse
278
         *      Sets up callbacks that fire when the user interacts with the editor using his mouse. So far this is
279
         *      only concerns resizing the window.
280
         *
281
         * Returns:
282
         *      This {<Editor>} instance for chaining.
283
         */
284
        _setupMouse: function() {
285
            jQuery(window).resize(function() {
286
                var content = jQuery('#' + Factory.getModule('Config').IDs.CONTENT);
287
288
                Canvas.enlarge({
289
                    x: content.width(),
290
                    y: content.height()
291
                }, true);
292
            }.bind(this));
293
294
            return this;
295
        },
296
297
        /**
298
         * Method: _setupKeyBindings
299
         *      Setup the global key bindings
300
         *
301
         * Keys:
302
         *      ESCAPE             - Clear selection.
303
         *      DELETE/BACKSPACE   - Delete all selected elements (nodes/edges).
304
         *      UP/RIGHT/DOWN/LEFT - Move the node in the according direction
305
         *      CTRL/CMD + A       - Select all nodes and edges
306
         *      CTRL/CMD + C/X/V   - Copy, cut and paste
307
         *
308
         * Returns:
309
         *      This {<Editor>} instance for chaining.
310
         */
311
        _setupKeyBindings: function(readOnly) {
312
            if (readOnly) return this;
313
314
            jQuery(document).keydown(function(event) {
315
                if (event.which == jQuery.ui.keyCode.ESCAPE) {
316
                    this._escapePressed(event);
317
                } else if (event.which === jQuery.ui.keyCode.DELETE || event.which === jQuery.ui.keyCode.BACKSPACE) {
318
                    this._deletePressed(event);
319
                } else if (event.which === jQuery.ui.keyCode.UP) {
320
                    this._arrowKeyPressed(event, 0, -1);
321
                } else if (event.which === jQuery.ui.keyCode.RIGHT) {
322
                    this._arrowKeyPressed(event, 1, 0);
323
                } else if (event.which === jQuery.ui.keyCode.DOWN) {
324
                    this._arrowKeyPressed(event, 0, 1);
325
                } else if (event.which === jQuery.ui.keyCode.LEFT) {
326
                    this._arrowKeyPressed(event, -1, 0);
327
                } else if (event.which === 'A'.charCodeAt() && (event.metaKey || event.ctrlKey)) {
328
                    this._selectAllPressed(event);
329
                } else if (event.which === 'C'.charCodeAt() && (event.metaKey || event.ctrlKey)) {
330
                    this._copyPressed(event);
331
                } else if (event.which === 'X'.charCodeAt() && (event.metaKey || event.ctrlKey)) {
332
                    this._cutPressed(event);
333
                } else if (event.which === 'V'.charCodeAt() && (event.metaKey || event.ctrlKey)) {
334
                    this._pastePressed(event);
335
                }
336
            }.bind(this));
337
338
            return this;
339
        },
340
341
        /**
342
         * Method: _setupNodeOffsetPrintStylesheet
343
         *      Creates a print stylesheet which is used to compensate the node offsets on the canvas when printing.
344
         *      Also sets up the CSS template which is used to change the transformation every time the content changes.
345
         *
346
         * Returns:
347
         *      This {<Editor>} instance for chaining.
348
         */
349
        _setupNodeOffsetPrintStylesheet: function() {
350
            // dynamically create a stylesheet, append it to the head and keep the reference to it
351
            this._nodeOffsetPrintStylesheet = jQuery('<style>')
352
                .attr('type',  'text/css')
353
                .attr('media', 'print')
354
                .appendTo('head');
355
356
            // this style will transform all elements on the canvas by the given 'x' and 'y' offset
357
            var transformCssTemplateText =
358
                '#' + Factory.getModule('Config').IDs.CANVAS + ' > * {\n' +
359
                '   transform: translate(<%= x %>px,<%= y %>px);\n' +
360
                '   -ms-transform: translate(<%= x %>px,<%= y %>px); /* IE 9 */\n' +
361
                '   -webkit-transform: translate(<%= x %>px,<%= y %>px); /* Safari and Chrome */\n' +
362
                '   -o-transform: translate(<%= x %>px,<%= y %>px); /* Opera */\n' +
363
                '   -moz-transform: translate(<%= x %>px,<%= y %>px); /* Firefox */\n' +
364
                '}';
365
            // store this as a template so we can use it later to manipulate the offset
366
            this._nodeOffsetStylesheetTemplate = _.template(transformCssTemplateText);
367
368
            return this;
369
        },
370
371
        /**
372
         * Method: _setupEventCallbacks
373
         *      Registers all event listeners of the editor.
374
         *
375
         * On:
376
         *      <Config::Events::NODE_DRAG_STOPPED>
377
         *      <Config::Events::NODE_ADDED>
378
         *      <Config::Events::NODE_DELETED>
379
         *
380
         * Returns:
381
         *      This {<Editor>} instance for chaining.
382
         */
383
        _setupEventCallbacks: function() {
384
            // events that trigger a re-calculation of the print offsets
385
            jQuery(document).on(Factory.getModule('Config').Events.NODE_DRAG_STOPPED,  this._updatePrintOffsets.bind(this));
386
            jQuery(document).on(Factory.getModule('Config').Events.NODE_ADDED,         this._updatePrintOffsets.bind(this));
387
            jQuery(document).on(Factory.getModule('Config').Events.NODE_DELETED,       this._updatePrintOffsets.bind(this));
388
389
            // update the available menu actions corresponding to the current selection
390
            jQuery(document).on([ Factory.getModule('Config').Events.NODE_SELECTED,
391
                                  Factory.getModule('Config').Events.NODE_UNSELECTED ].join(' '),
392
                this._updateMenuActions.bind(this));
393
394
            // show status of global AJAX events in navbar
395
            jQuery(document).ajaxSend(Progress.showAjaxProgress);
396
            jQuery(document).ajaxSuccess(Progress.flashAjaxSuccessMessage);
397
            jQuery(document).ajaxError(Progress.flashAjaxErrorMessage);
398
399
            return this;
400
        },
401
402
        /**
403
         * Group: Graph Editing
404
         */
405
406
        /**
407
         * Method: _deleteSelection
408
         *      Will remove the selected nodes and edges.
409
         *
410
         * Returns:
411
         *      This {<Editor>} instance for chaining
412
         */
413
        _deleteSelection: function() {
414
            var deletableNodes      = this._deletable(this._selectedNodes());
415
            var deletableEdges      = this._deletable(this._selectedEdges());
416
            var deletableNodeGroups = this._deletable(this._selectedNodeGroups());
417
418
            // delete selected nodes
419
            _.each(deletableNodes, this.graph.deleteNode.bind(this.graph));
420
421
            // delete selected edges
422
            _.each(deletableEdges, this.graph.deleteEdge.bind(this.graph));
423
424
            // delete selected node groups
425
            _.each(deletableNodeGroups, this.graph.deleteNodeGroup.bind(this.graph));
426
427
            // if at least one element was deletable, hide the properties window (if it isn't already)
428
            if (_.union(deletableNodes, deletableEdges, deletableNodeGroups).length > 0) this.properties.hide();
429
430
            // update the available menu actions
431
            this._updateMenuActions();
432
433
            return this;
434
        },
435
436
        /**
437
         * Method: _selectedNodes
438
         *      Finds currently selected nodes.
439
         *
440
         * Returns:
441
         *      An array of currently selected {<Node>} instances.
442
         */
443
        _selectedNodes: function() {
444
            var selectedNodes = '.' + Factory.getModule('Config').Classes.SELECTED + '.' + Factory.getModule('Config').Classes.NODE;
445
446
            var nodes = [];
447
            jQuery(selectedNodes).each(function(index, element) {
448
                var node = this.graph.getNodeById(jQuery(element).data(Factory.getModule('Config').Keys.NODE).id);
449
                nodes.push(node);
450
            }.bind(this));
451
452
            return nodes;
453
        },
454
455
        /**
456
         * Method: _selectedEdges
457
         *      Finds currently selected edges.
458
         *
459
         * Returns:
460
         *      An array of currently selected {<Edge>} instances.
461
         */
462
        _selectedEdges: function() {
463
            var selectedEdges = '.' + Factory.getModule('Config').Classes.SELECTED + '.' + Factory.getModule('Config').Classes.JSPLUMB_CONNECTOR;
464
465
            var edges = [];
466
            jQuery(selectedEdges).each(function(index, element) {
467
                var edge = jQuery(element).data(Factory.getModule('Config').Keys.EDGE);
468
                edges.push(edge);
469
            }.bind(this));
470
471
            return edges;
472
        },
473
474
        /**
475
         * Method: _selectedNodeGroups
476
         *      Finds currently selected node groups.
477
         *
478
         * Returns:
479
         *      An array of currently selected {<NodeGroup>} instances.
480
         */
481
        _selectedNodeGroups: function() {
482
            var nodegroups = [];
483
            // find selected node groups (NASTY!!!)
484
            var allNodeGroups = '.' + Factory.getModule('Config').Classes.NODEGROUP;
485
486
            jQuery(allNodeGroups).each(function(index, element) {
487
                var nodeGroup = jQuery(element).data(Factory.getModule('Config').Keys.NODEGROUP);
488
                // since the selectable element is an svg path, we need to look for that nested element and check its
489
                //   state of selection via the CSS class .selected
490
                if (nodeGroup.container.find("svg path").hasClass(Factory.getModule('Config').Classes.SELECTED)) {
491
                    nodegroups.push(nodeGroup);
492
                }
493
            }.bind(this));
494
495
            return nodegroups;
496
        },
497
498
        /**
499
         * Method: _copyable
500
         *      Filters the given array of elements (Nodes, Edges, NodeGroups) for copyable ones.
501
         *
502
         * Returns:
503
         *      An array of elements that are copyable.
504
         */
505
        _copyable: function(elements) {
506
            return _.filter(elements, function(elem) { return elem.copyable; });
507
        },
508
509
        /**
510
         * Method: _deletable
511
         *      Filters the given array of elements (Nodes, Edges, NodeGroups) for deletable ones.
512
         *
513
         * Returns:
514
         *      An array of elements that are deletable.
515
         */
516
        _deletable: function(elements) {
517
            return _.filter(elements, function(elem) { return elem.deletable; });
518
        },
519
520
        /**
521
         * Method: _cuttable
522
         *      Filters the given array of elements (Nodes, Edges, NodeGroups) for cuttable ones.
523
         *
524
         * Returns:
525
         *      An array of elements that are cuttable.
526
         */
527
        _cuttable: function(elements) {
528
            return _.intersection(this._copyable(elements), this._deletable(elements));
529
        },
530
531
        /**
532
         * Method: _updateMenuActions
533
         *      Will update the enabled/disabled status of the menu actions depending on the current selection.
534
         *
535
         * Returns:
536
         *      This {<Editor>} instance for chaining.
537
         */
538
539
        _updateMenuActions: function() {
540
            var selectedNodes = this._selectedNodes();
541
            var selectedElems =       selectedNodes.concat(
542
                                this._selectedEdges().concat(
543
                                this._selectedNodeGroups()));
544
545
            // copy is only available when at least one node is copyable as copying edges or node groups solely won't
546
            //  have any effect (because they can't be restored)
547
            if (this._copyable(selectedNodes).length > 0) {
548
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_COPY).parent().removeClass('disabled');
549
            } else {
550
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_COPY).parent().addClass('disabled');
551
            }
552
553
            // same here: cut is only available when at least one node is cuttable
554
            if (this._cuttable(selectedNodes).length > 0) {
555
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_CUT).parent().removeClass('disabled');
556
            } else {
557
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_CUT).parent().addClass('disabled');
558
            }
559
560
            // delete is only available when the selection is not empty and at least one element is deletable
561
            if (selectedElems.length > 0 && this._deletable(selectedElems).length > 0) {
562
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_DELETE).parent().removeClass('disabled');
563
            } else {
564
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_DELETE).parent().addClass('disabled');
565
            }
566
567
            // paste is only available when the graph is not read-only there is something in the clipboard
568
            if (!this.graph.readOnly && this._getClipboard()) {
569
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_PASTE).parent().removeClass('disabled');
570
            } else {
571
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_PASTE).parent().addClass('disabled');
572
            }
573
574
            // select all is only available when the graph is not read-only
575
            if (!this.graph.readOnly) {
576
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_SELECTALL).parent().removeClass('disabled');
577
            } else {
578
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_SELECTALL).parent().addClass('disabled');
579
            }
580
581
            // layouting is only available when the graph is not read-only
582
            if (!this.graph.readOnly) {
583
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_LAYOUT_CLUSTER).parent().removeClass('disabled');
584
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_LAYOUT_TREE).parent().removeClass('disabled');
585
            } else {
586
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_LAYOUT_CLUSTER).parent().addClass('disabled');
587
                jQuery('#' + Factory.getModule('Config').IDs.ACTION_LAYOUT_TREE).parent().addClass('disabled');
588
            }
589
590
            return this;
591
        },
592
593
        /**
594
         * Method: _selectAll
595
         *      Will select all nodes and edges.
596
         *
597
         * Parameters:
598
         *      {jQuery::Event} event - the issued select all keypress event
599
         *
600
         * Returns:
601
         *      This {<Editor>} instance for chaining.
602
         */
603
604
        _selectAll: function(event) {
605
            //XXX: trigger selection start event manually here
606
            //XXX: hack to emulate a new selection process
607
            Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStart(event);
608
609
            jQuery('.' + Factory.getModule('Config').Classes.SELECTEE)
610
               .addClass(Factory.getModule('Config').Classes.SELECTING)
611
               .addClass(Factory.getModule('Config').Classes.SELECTED);
612
613
            //XXX: trigger selection stop event manually here
614
            //XXX: nasty hack to bypass draggable and selectable incompatibility, see also canvas.js
615
            Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStop(null);
616
        },
617
618
        /**
619
         * Method: _deselectAll
620
         *      Deselects all the nodes and edges in the current graph.
621
         *
622
         * Parameters:
623
         *      {jQuery::Event} event - (optional) the issued select all keypress event
624
         *
625
         * Returns:
626
         *      This {<Editor>} instance for chaining.
627
         */
628
        _deselectAll: function(event) {
629
            if (typeof event === 'undefined') {
630
                event = window.event;
631
            }
632
633
            //XXX: Since a deselect-click (which we simulate here) only works without metaKey or ctrlKey pressed,
634
            // we need to deactivate them manually.
635
            var hackEvent = jQuery.extend({}, event, {
636
                metaKey: false,
637
                ctrlKey: false
638
            });
639
640
            //XXX: deselect everything
641
            // This uses the jQuery.ui.selectable internal functions.
642
            // We need to trigger them manually in order to simulate a click on the canvas.
643
            Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStart(hackEvent);
644
            Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStop(hackEvent);
645
646
            return this;
647
        },
648
649
        /**
650
         * Method: _copySelection
651
         *      Will copy all selected nodes by serializing and saving them to HTML5 Local Storage or the _clipboard
652
         *      variable if Local Storage is not available.
653
         *
654
         * Returns:
655
         *      This {<Editor>} instance for chaining.
656
         */
657
        _copySelection: function() {
658
            // select copyable elements and transform them into dicts for later serialization
659
            var nodes      = _.invoke(this._copyable(this._selectedNodes()), 'toDict');
660
            var edges      = _.invoke(this._copyable(this._selectedEdges()), 'toDict');
661
            var nodegroups = _.invoke(this._copyable(this._selectedNodeGroups()), 'toDict');
662
663
            // copying only makes sense, when at least one node is involved (i.e. in the current selection), because
664
            //  edges and node groups can only be recreated at paste, when nodes are as well.
665
            if (nodes.length === 0) return;
666
667
            var clipboard = {
668
                'pasteCount': 0,
669
                'nodes':      nodes,
670
                'edges':      edges,
671
                'nodeGroups': nodegroups
672
            };
673
674
            this._updateClipboard(clipboard);
675
676
            // update available menu actions, so the paste menu action will be enabled
677
            this._updateMenuActions();
678
        },
679
680
        /**
681
         * Method: _paste
682
         *      Will paste previously copied nodes from HTML5 Local Storage or the _clipboard variable.
683
         *
684
         * Returns:
685
         *      This {<Editor>} instance for chaining.
686
         */
687
        _paste: function() {
688
            // fetch clipboard contents
689
            var clipboard = this._getClipboard();
690
691
            // if there is nothing in the clipboard, return
692
            if (!clipboard) return;
693
694
            // deselect the original nodes and edges
695
            this._deselectAll();
696
697
            // increase paste count (used for nicely positioning multiple pastes)
698
            var pasteCount = ++clipboard.pasteCount;
699
            this._updateClipboard(clipboard);
700
701
            var nodes       = clipboard.nodes;
702
            var edges       = clipboard.edges;
703
            var nodeGroups  = clipboard.nodeGroups;
704
705
            var ids         = {}; // stores to every old id the newly generated id to connect the nodes again
706
            var boundingBox = this._boundingBoxForNodes(nodes); // used along with pasteCount to place the copy nicely
707
708
            _.each(nodes, function(jsonNode) {
709
                var pasteId  = this.graph.createId();
710
                ids[jsonNode.id] = pasteId;
711
                jsonNode.id = pasteId;
712
                jsonNode.x += pasteCount * (boundingBox.width + 1);
713
                jsonNode.y += pasteCount * (boundingBox.height + 1);
714
715
                var node = this.graph.addNode(jsonNode);
716
                if (node) node.select();
717
            }.bind(this));
718
719
            _.each(edges, function(jsonEdge) {
720
                jsonEdge.id = undefined;
721
                jsonEdge.source = ids[jsonEdge.sourceNodeId] || jsonEdge.sourceNodeId;
722
                jsonEdge.target = ids[jsonEdge.targetNodeId] || jsonEdge.targetNodeId;
723
724
                var edge = this.graph.addEdge(jsonEdge);
725
                if (edge) edge.select();
726
            }.bind(this));
727
728
            _.each(nodeGroups, function(jsonNodeGroup) {
729
                // remove the original nodeGroup's identity
730
                jsonNodeGroup.id = undefined;
731
                // map old ids to new ids
732
                jsonNodeGroup.nodeIds = _.map(jsonNodeGroup.nodeIds, function(nodeId) {
733
                    return ids[nodeId] || nodeId;
734
                });
735
736
                var nodeGroup = this.graph.addNodeGroup(jsonNodeGroup);
737
                if (nodeGroup) nodeGroup.select();
738
            }.bind(this));
739
740
            //XXX: trigger selection stop event manually here
741
            //XXX: nasty hack to bypass draggable and selectable incompatibility, see also canvas.js
742
            Canvas.container.data(Factory.getModule('Config').Keys.SELECTABLE)._mouseStop(null);
743
        },
744
745
        /**
746
         * Method: _cutSelection
747
         *      Will delete and copy selected nodes by using _updateClipboard().
748
         *
749
         * Returns:
750
         *      This {<Editor>} instance for chaining.
751
         */
752
        _cutSelection: function() {
753
            this._copySelection();
754
            this._deleteSelection();
755
756
            // set the just copied clipboard's pasteCount to -1, so that it will paste right in place of the original.
757
            var clipboard = this._getClipboard();
758
            --clipboard.pasteCount;
759
            this._updateClipboard(clipboard);
760
        },
761
762
        /**
763
         * Method: _groupSelection
764
         *
765
         *   Will create a new NodeGroup with the current selected nodes.
766
         *
767
         *   Note: Node Groups are an abstract concept, that every diagram type has to implement on its own. So this
768
         *   method can't be accessed by the user, unless you provide Menu Actions or Key Events for it.
769
         *   (see dfd/editor.js for examples of correct subclassing)
770
         *
771
         * Returns:
772
         *   This Editor instance for chaining.
773
         */
774
        _groupSelection: function() {
775
            var selectedNodes = this._selectedNodes();
776
777
            if(selectedNodes.length > 1)
778
            {
779
                var jsonNodeGroup = {
780
                    nodeIds: _.map(selectedNodes, function(node){return node.id;}.bind(this))
781
                };
782
                this.graph.addNodeGroup(jsonNodeGroup);
783
            }
784
785
            return this;
786
        },
787
788
        /**
789
         * Method: _ungroupSelection
790
         *
791
         *   Will ungroup either the NodeGroup, that only consists of the selected nodes, or the selected NodeGroups
792
         *   directly.
793
         *
794
         *   Note: This method can't be accessed by the user, unless you provide Menu Actions or Key Events for it.
795
         *   (see dfd/editor.js for examples of correct subclassing)
796
         *
797
         * Returns:
798
         *   Success
799
         */
800
        _ungroupSelection: function() {
801
            var nodeIds = _.map(this._selectedNodes(), function(node) { return node.id; }.bind(this));
802
803
            // case [1]: find the correct node group, whose node ids match the selected ids
804
            // (i.e. the user has to select all members of a NodeGroup to remove the NodeGroup)
805
            _.each(this.graph.nodeGroups, function(ng) {
806
                var ngIds = ng.nodeIds();
807
                // math recap: two sets are equal, when both their differences are zero length
808
                if (jQuery(ngIds).not(nodeIds).length === 0 && jQuery(nodeIds).not(ngIds).length === 0) {
809
                    this.graph.deleteNodeGroup(ng);
810
                    return true;
811
                }
812
            }.bind(this));
813
814
            // case [2]: the user selected NodeGroups, (s)he wants to delete, do him/her the favor to delete them
815
            _.each(this._selectedNodeGroups(), this.graph.deleteNodeGroup.bind(this.graph));
816
817
            return false;
818
        },
819
820
        /**
821
         * Method: _boundingBoxForNodes
822
         *      Returns the (smallest) bounding box for the given nodes by accessing their x and y coordinates and
823
         *      finding the minimum and maximum. Used by _paste() to place the copy nicely.
824
         *
825
         * Returns:
826
         *      An {Object} containing the 'width' and 'height' keys of the calculated bounding box.
827
         */
828
829
        _boundingBoxForNodes: function(nodes) {
830
            var topMostNode     = { 'y': Number.MAX_VALUE };
831
            var leftMostNode    = { 'x': Number.MAX_VALUE };
832
            var bottomMostNode  = { 'y': 0 };
833
            var rightMostNode   = { 'x': 0 };
834
835
            _.each(nodes, function(node) {
836
                if (node.y < topMostNode.y)    { topMostNode    = node; }
837
                if (node.x < leftMostNode.x)   { leftMostNode   = node; }
838
                if (node.y > bottomMostNode.y) { bottomMostNode = node; }
839
                if (node.x > rightMostNode.x)  { rightMostNode  = node; }
840
            }.bind(this));
841
842
            return {
843
                'width':  rightMostNode.x - leftMostNode.x,
844
                'height': bottomMostNode.y - topMostNode.y
845
            };
846
        },
847
848
849
        /**
850
         * Group: Clipboard Handling
851
         */
852
853
        /**
854
         * Method: _updateClipboard
855
         *      Saves the given clipboardDict either to html5 Local Storage or at least to the Graph's _clipboard var
856
         *      as JSON string.
857
         *
858
         * Parameters:
859
         *      {Object} clipboardDict - JSON object to be stored
860
         *
861
         * Returns:
862
         *      This {<Editor>} instance for chaining.
863
         */
864
        _updateClipboard: function(clipboardDict) {
865
            var clipboardString = JSON.stringify(clipboardDict);
866
            if (typeof window.Storage !== 'undefined') {
867
                localStorage['clipboard_' + this.graph.kind] = clipboardString;
868
            } else { // fallback (doesn't work cross-tab)
869
                this._clipboard = clipboardString;
870
            }
871
872
            return this;
873
        },
874
875
        /**
876
         * Method: _getClipboard
877
         *      Returns the current clipboard either from html5 Local Storage or from the Graph's _clipboard var as
878
         *      JSON.
879
         *
880
         * Returns:
881
         *      The clipboard contents as {Object}.
882
         */
883
        _getClipboard: function() {
884
            if (typeof window.Storage !== 'undefined' &&
885
                typeof localStorage['clipboard_' + this.graph.kind] !== 'undefined') {
886
                return JSON.parse(localStorage['clipboard_' + this.graph.kind]);
887
            } else if (typeof this._clipboard !== 'undefined') {
888
                return JSON.parse(this._clipboard);
889
            } else {
890
                return false;
891
            }
892
        },
893
894
        /**
895
         * Group: Keyboard Interaction
896
         */
897
898
        /**
899
         * Method: _arrowKeyPressed
900
         *      Event callback for handling presses of arrow keys. Will move the selected nodes in the given direction
901
         *      by and offset equal to the canvas' grid size. The movement is not done when an input field is currently
902
         *      in focus.
903
         *
904
         * Parameters:
905
         *      {jQuery::Event} event      - the issued delete keypress event
906
         *      {Number}        xDirection - signum of the arrow key's x direction movement (e.g. -1 for left)
907
         *      {Number}        yDirection - signum of the arrow key's y direction movement (e.g.  1 for down)
908
         *
909
         * Return:
910
         *      This {<Editor>} instance for chaining
911
         */
912
        _arrowKeyPressed: function(event, xDirection, yDirection) {
913
            if (jQuery(event.target).is('input, textarea')) return this;
914
915
            var selectedNodes = '.' + Factory.getModule('Config').Classes.SELECTED + '.' + Factory.getModule('Config').Classes.NODE;
916
            jQuery(selectedNodes).each(function(index, element) {
917
                var node = jQuery(element).data(Factory.getModule('Config').Keys.NODE);
918
                node.moveBy({
919
                    x: xDirection * Canvas.gridSize,
920
                    y: yDirection * Canvas.gridSize
921
                });
922
            }.bind(this));
923
924
            jQuery(document).trigger(Factory.getModule('Config').Events.NODES_MOVED);
925
926
            return this;
927
        },
928
929
        /**
930
         * Method: _deletePressed
931
         *      Event callback for handling delete key presses. Will remove the selected elements by calling
932
         *      _deleteSelection as long as no input field is currently focused (allows e.g. character removal in
933
         *      properties).
934
         *
935
         * Parameters:
936
         *      {jQuery::Event} event - the issued delete keypress event
937
         *
938
         * Returns:
939
         *      This {<Editor>} instance for chaining
940
         */
941
        _deletePressed: function(event) {
942
            // prevent that node is being deleted when we edit an input field
943
            if (jQuery(event.target).is('input, textarea')) return this;
944
            event.preventDefault();
945
            this._deleteSelection();
946
            return this;
947
        },
948
949
        /**
950
         * Method: _escapePressed
951
         *      Event callback for handling escape key presses. Will deselect any selected nodes and edges by calling
952
         *      _deselectAll().
953
         *
954
         * Parameters:
955
         *      {jQuery::Event} event - the issued escape keypress event
956
         *
957
         * Returns:
958
         *      This {<Editor>} instance for chaining
959
         */
960
        _escapePressed: function(event) {
961
            event.preventDefault();
962
            this._deselectAll(event);
963
            return this;
964
        },
965
966
        /**
967
         * Method: _selectAllPressed
968
         *      Event callback for handling a select all (CTRL/CMD + A) key presses.
969
         *
970
         * Parameters:
971
         *      {jQuery::Event} event - the issued select all keypress event
972
         *
973
         * Returns:
974
         *      This {<Editor>} instance for chaining.
975
         */
976
        _selectAllPressed: function(event) {
977
            if (jQuery(event.target).is('input, textarea')) return this;
978
            event.preventDefault();
979
            this._selectAll(event);
980
            return this;
981
        },
982
983
        /**
984
         * Method: _copyPressed
985
         *      Event callback for handling a copy (CTRL/CMD + C) key press. Will copy selected nodes by serializing
986
         *      and saving them to HTML5 Local Storage or the _clipboard var by calling _copySelection().
987
         *
988
         * Parameters:
989
         *      {jQuery::Event} event - the issued select all keypress event
990
         *
991
         * Returns:
992
         *      This {<Editor>} instance for chaining.
993
         */
994
        _copyPressed: function(event) {
995
            if (jQuery(event.target).is('input, textarea')) return this;
996
            event.preventDefault();
997
            this._copySelection();
998
            return this;
999
        },
1000
1001
        /**
1002
         * Method: _pastePressed
1003
         *      Event callback for handling a paste (CTRL/CMD + V) key press. Will paste previously copied nodes from
1004
         *      HTML% Local Storage or the _clipboard var by calling _paste().
1005
         *
1006
         * Parameters:
1007
         *      {jQuery::Event} event - the issued select all keypress event
1008
         *
1009
         * Returns:
1010
         *      This {<Editor>} instance for chaining.
1011
         */
1012
        _pastePressed: function(event) {
1013
            if (jQuery(event.target).is('input, textarea')) return this;
1014
            event.preventDefault();
1015
            this._paste();
1016
            return this;
1017
        },
1018
1019
        /**
1020
         * Method: _cutPressed
1021
         *      Event callback for handling a cut (CTRL/CMD + X) key press. Will delete and copy selected nodes by
1022
         *      calling _cutSelection().
1023
         *
1024
         * Parameters:
1025
         *      {jQuery::Event} event - the issued select all keypress event
1026
         *
1027
         * Returns:
1028
         *      This {<Editor>} instance for chaining.
1029
         */
1030
        _cutPressed: function(event) {
1031
            if (jQuery(event.target).is('input, textarea')) return this;
1032
            event.preventDefault();
1033
            this._cutSelection();
1034
            return this;
1035
        },
1036
1037
        /**
1038
         * Group: Print Offset Calculation
1039
         */
1040
1041
        /**
1042
         * Method: _calculateContentOffsets
1043
         *      Calculates the minimal offsets from top and left among all elements displayed on the canvas.
1044
         *
1045
         * Returns:
1046
         *      An {Object} containing the minimal top ('top') and minimal left ('left') offset to the browser borders.
1047
         */
1048
        _calculateContentOffsets: function() {
1049
            var minLeftOffset = window.Infinity;
1050
            var minTopOffset  = window.Infinity;
1051
1052
            jQuery('.' + Factory.getModule('Config').Classes.NODE + ', .' + Factory.getModule('Config').Classes.MIRROR).each(function(index, element) {
1053
                var offset = jQuery(element).offset();
1054
                minLeftOffset = Math.min(minLeftOffset, offset.left);
1055
                minTopOffset  = Math.min(minTopOffset,  offset.top);
1056
            });
1057
1058
            return {
1059
                'top':  minTopOffset,
1060
                'left': minLeftOffset
1061
            };
1062
        },
1063
1064
        /**
1065
         * Method: _updatePrintOffsets
1066
         *      Calculate the minimal offsets of elements on the canvas and updates the print stylesheet so that it will
1067
         *      compensate these offsets while printing (using CSS transforms). This update is triggered every time a
1068
         *      node was added, removed or moved on the canvas.
1069
         */
1070
        _updatePrintOffsets: function(event) {
1071
            var minOffsets = this._calculateContentOffsets();
1072
1073
            if (minOffsets.top  == this._currentMinNodeOffsets.top &&
1074
                minOffsets.left == this._currentMinNodeOffsets.left) {
1075
                    // nothing changed
1076
                    return;
1077
                }
1078
1079
            // replace the style text with the new transformation style
1080
            this._nodeOffsetPrintStylesheet.text(this._nodeOffsetStylesheetTemplate({
1081
                'x': -minOffsets.left + 1, // add a tolerance pixel to avoid cut edges,
1082
                'y': -minOffsets.top + 1
1083
            }));
1084
        }
1085
    });
1086
});
1087