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