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
|
|
|
|