· 9 years ago · Nov 24, 2016, 10:42 AM
1// JointJS diagramming library.
2// (c) 2011-2015 client IO
3
4// joint.dia.Link base model.
5// --------------------------
6joint.dia.Link = joint.dia.Cell.extend({
7
8 // The default markup for links.
9 markup: [
10 '<path class="connection" stroke="black" d="M 0 0 0 0"/>',
11 '<path class="marker-source" fill="black" stroke="black" d="M 0 0 0 0"/>',
12 '<path class="marker-target" fill="black" stroke="black" d="M 5 0 0 0"/>',
13 '<path class="connection-wrap" d="M 0 0 0 0"/>',
14 '<g class="labels"/>',
15 '<g class="marker-vertices"/>',
16 '<g class="marker-arrowheads"/>',
17 '<g class="link-tools"/>'
18 ].join(''),
19
20 labelMarkup: [
21 '<g class="label">',
22 '<rect />',
23 '<text />',
24 '</g>'
25 ].join(''),
26
27 toolMarkup: [
28 '<g class="link-tool">',
29 '<g class="tool-remove" event="remove">',
30 '<circle r="11" />',
31 '<path transform="scale(.8) translate(-16, -16)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z" />',
32 '<title>Remove link.</title>',
33 '</g>',
34 '<g class="tool-options" event="link:options">',
35 '<circle r="11" transform="translate(25)"/>',
36 '<path fill="white" transform="scale(.55) translate(29, -16)" d="M31.229,17.736c0.064-0.571,0.104-1.148,0.104-1.736s-0.04-1.166-0.104-1.737l-4.377-1.557c-0.218-0.716-0.504-1.401-0.851-2.05l1.993-4.192c-0.725-0.91-1.549-1.734-2.458-2.459l-4.193,1.994c-0.647-0.347-1.334-0.632-2.049-0.849l-1.558-4.378C17.165,0.708,16.588,0.667,16,0.667s-1.166,0.041-1.737,0.105L12.707,5.15c-0.716,0.217-1.401,0.502-2.05,0.849L6.464,4.005C5.554,4.73,4.73,5.554,4.005,6.464l1.994,4.192c-0.347,0.648-0.632,1.334-0.849,2.05l-4.378,1.557C0.708,14.834,0.667,15.412,0.667,16s0.041,1.165,0.105,1.736l4.378,1.558c0.217,0.715,0.502,1.401,0.849,2.049l-1.994,4.193c0.725,0.909,1.549,1.733,2.459,2.458l4.192-1.993c0.648,0.347,1.334,0.633,2.05,0.851l1.557,4.377c0.571,0.064,1.148,0.104,1.737,0.104c0.588,0,1.165-0.04,1.736-0.104l1.558-4.377c0.715-0.218,1.399-0.504,2.049-0.851l4.193,1.993c0.909-0.725,1.733-1.549,2.458-2.458l-1.993-4.193c0.347-0.647,0.633-1.334,0.851-2.049L31.229,17.736zM16,20.871c-2.69,0-4.872-2.182-4.872-4.871c0-2.69,2.182-4.872,4.872-4.872c2.689,0,4.871,2.182,4.871,4.872C20.871,18.689,18.689,20.871,16,20.871z"/>',
37 '<title>Link options.</title>',
38 '</g>',
39 '</g>'
40 ].join(''),
41
42 // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`).
43 // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for
44 // dragging vertices (changin their position). The latter is used for removing vertices.
45 vertexMarkup: [
46 '<g class="marker-vertex-group" transform="translate(<%= x %>, <%= y %>)">',
47 '<circle class="marker-vertex" idx="<%= idx %>" r="10" />',
48 '<path class="marker-vertex-remove-area" idx="<%= idx %>" d="M16,5.333c-7.732,0-14,4.701-14,10.5c0,1.982,0.741,3.833,2.016,5.414L2,25.667l5.613-1.441c2.339,1.317,5.237,2.107,8.387,2.107c7.732,0,14-4.701,14-10.5C30,10.034,23.732,5.333,16,5.333z" transform="translate(5, -33)"/>',
49 '<path class="marker-vertex-remove" idx="<%= idx %>" transform="scale(.8) translate(9.5, -37)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z">',
50 '<title>Remove vertex.</title>',
51 '</path>',
52 '</g>'
53 ].join(''),
54
55 arrowheadMarkup: [
56 '<g class="marker-arrowhead-group marker-arrowhead-group-<%= end %>">',
57 '<path class="marker-arrowhead" end="<%= end %>" d="M 26 0 L 0 13 L 26 26 z" />',
58 '</g>'
59 ].join(''),
60
61 defaults: {
62
63 type: 'link',
64 source: {},
65 target: {}
66 },
67
68 disconnect: function() {
69
70 return this.set({ source: g.point(0, 0), target: g.point(0, 0) });
71 },
72
73 // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter.
74 label: function(idx, value) {
75
76 idx = idx || 0;
77
78 var labels = this.get('labels') || [];
79
80 // Is it a getter?
81 if (arguments.length === 0 || arguments.length === 1) {
82
83 return labels[idx];
84 }
85
86 var newValue = _.merge({}, labels[idx], value);
87
88 var newLabels = labels.slice();
89 newLabels[idx] = newValue;
90
91 return this.set({ labels: newLabels });
92 },
93
94 translate: function(tx, ty, opt) {
95
96 var attrs = {};
97 var source = this.get('source');
98 var target = this.get('target');
99 var vertices = this.get('vertices');
100
101 if (!source.id) {
102 attrs.source = { x: (source.x || 0) + tx, y: (source.y || 0) + ty };
103 }
104
105 if (!target.id) {
106 attrs.target = { x: (target.x || 0) + tx, y: (target.y || 0) + ty };
107 }
108
109 if (vertices && vertices.length) {
110 attrs.vertices = _.map(vertices, function(vertex) {
111 return { x: vertex.x + tx, y: vertex.y + ty };
112 });
113 }
114
115 // enrich the option object
116 opt = opt || {};
117 opt.translateBy = opt.translateBy || this.id;
118 opt.tx = tx;
119 opt.ty = ty;
120
121 return this.set(attrs, opt);
122 },
123
124 reparent: function(opt) {
125
126 var newParent;
127
128 if (this.graph) {
129
130 var source = this.graph.getCell(this.get('source').id);
131 var target = this.graph.getCell(this.get('target').id);
132 var prevParent = this.graph.getCell(this.get('parent'));
133
134 if (source && target) {
135 newParent = this.graph.getCommonAncestor(source, target);
136 }
137
138 if (prevParent && (!newParent || newParent.id !== prevParent.id)) {
139 // Unembed the link if source and target has no common ancestor
140 // or common ancestor changed
141 prevParent.unembed(this, opt);
142 }
143
144 if (newParent) {
145 newParent.embed(this, opt);
146 }
147 }
148
149 return newParent;
150 },
151
152 isLink: function() {
153
154 return true;
155 },
156
157 hasLoop: function(opt) {
158
159 opt = opt || {};
160
161 var sourceId = this.get('source').id;
162 var targetId = this.get('target').id;
163
164 if (!sourceId || !targetId) {
165 // Link "pinned" to the paper does not have a loop.
166 return false;
167 }
168
169 var loop = sourceId === targetId;
170
171 // Note that there in the deep mode a link can have a loop,
172 // even if it connects only a parent and its embed.
173 // A loop "target equals source" is valid in both shallow and deep mode.
174 if (!loop && opt.deep && this.graph) {
175
176 var sourceElement = this.graph.getCell(sourceId);
177 var targetElement = this.graph.getCell(targetId);
178
179 loop = sourceElement.isEmbeddedIn(targetElement) || targetElement.isEmbeddedIn(sourceElement);
180 }
181
182 return loop;
183 },
184
185 getSourceElement: function() {
186
187 var source = this.get('source');
188
189 return (source && source.id && this.graph && this.graph.getCell(source.id)) || null;
190 },
191
192 getTargetElement: function() {
193
194 var target = this.get('target');
195
196 return (target && target.id && this.graph && this.graph.getCell(target.id)) || null;
197 },
198
199 // Returns the common ancestor for the source element,
200 // target element and the link itself.
201 getRelationshipAncestor: function() {
202
203 var connectionAncestor;
204
205 if (this.graph) {
206
207 var cells = _.compact([
208 this,
209 this.getSourceElement(), // null if source is a point
210 this.getTargetElement() // null if target is a point
211 ]);
212
213 connectionAncestor = this.graph.getCommonAncestor.apply(this.graph, cells);
214 }
215
216 return connectionAncestor || null;
217 },
218
219 // Is source, target and the link itself embedded in a given element?
220 isRelationshipEmbeddedIn: function(element) {
221
222 var elementId = _.isString(element) ? element : element.id;
223 var ancestor = this.getRelationshipAncestor();
224
225 return !!ancestor && (ancestor.id === elementId || ancestor.isEmbeddedIn(elementId));
226 }
227});
228
229
230// joint.dia.Link base view and controller.
231// ----------------------------------------
232
233joint.dia.LinkView = joint.dia.CellView.extend({
234
235 className: function() {
236 return _.unique(this.model.get('type').split('.').concat('link')).join(' ');
237 },
238
239 options: {
240
241 shortLinkLength: 100,
242 doubleLinkTools: false,
243 longLinkLength: 160,
244 linkToolsOffset: 40,
245 doubleLinkToolsOffset: 60,
246 sampleInterval: 50
247 },
248
249 _z: null,
250
251 initialize: function(options) {
252
253 joint.dia.CellView.prototype.initialize.apply(this, arguments);
254
255 // create methods in prototype, so they can be accessed from any instance and
256 // don't need to be create over and over
257 if (typeof this.constructor.prototype.watchSource !== 'function') {
258 this.constructor.prototype.watchSource = this.createWatcher('source');
259 this.constructor.prototype.watchTarget = this.createWatcher('target');
260 }
261
262 // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to
263 // `<g class="label">` nodes wrapped by Vectorizer. This allows for quick access to the
264 // nodes in `updateLabelPosition()` in order to update the label positions.
265 this._labelCache = {};
266
267 // keeps markers bboxes and positions again for quicker access
268 this._markerCache = {};
269
270 // bind events
271 this.startListening();
272 },
273
274 startListening: function() {
275
276 var model = this.model;
277
278 this.listenTo(model, 'change:markup', this.render);
279 this.listenTo(model, 'change:smooth change:manhattan change:router change:connector', this.update);
280 this.listenTo(model, 'change:toolMarkup', this.onToolsChange);
281 this.listenTo(model, 'change:labels change:labelMarkup', this.onLabelsChange);
282 this.listenTo(model, 'change:vertices change:vertexMarkup', this.onVerticesChange);
283 this.listenTo(model, 'change:source', this.onSourceChange);
284 this.listenTo(model, 'change:target', this.onTargetChange);
285 },
286
287 onSourceChange: function(cell, source, opt) {
288
289 // Start watching the new source model.
290 this.watchSource(cell, source);
291 // This handler is called when the source attribute is changed.
292 // This can happen either when someone reconnects the link (or moves arrowhead),
293 // or when an embedded link is translated by its ancestor.
294 // 1. Always do update.
295 // 2. Do update only if the opposite end ('target') is also a point.
296 if (!opt.translateBy || !this.model.get('target').id) {
297 opt.updateConnectionOnly = true;
298 this.update(this.model, null, opt);
299 }
300 },
301
302 onTargetChange: function(cell, target, opt) {
303
304 // Start watching the new target model.
305 this.watchTarget(cell, target);
306 // See `onSourceChange` method.
307 if (!opt.translateBy) {
308 opt.updateConnectionOnly = true;
309 this.update(this.model, null, opt);
310 }
311 },
312
313 onVerticesChange: function(cell, changed, opt) {
314
315 this.renderVertexMarkers();
316
317 // If the vertices have been changed by a translation we do update only if the link was
318 // the only link that was translated. If the link was translated via another element which the link
319 // is embedded in, this element will be translated as well and that triggers an update.
320 // Note that all embeds in a model are sorted - first comes links, then elements.
321 if (!opt.translateBy || opt.translateBy === this.model.id) {
322 // Vertices were changed (not as a reaction on translate)
323 // or link.translate() was called or
324 opt.updateConnectionOnly = true;
325 this.update(cell, null, opt);
326 }
327 },
328
329 onToolsChange: function() {
330
331 this.renderTools().updateToolsPosition();
332 },
333
334 onLabelsChange: function() {
335
336 this.renderLabels().updateLabelPositions();
337 },
338
339 // Rendering
340 //----------
341
342 render: function() {
343
344 this.$el.empty();
345
346 // A special markup can be given in the `properties.markup` property. This might be handy
347 // if e.g. arrowhead markers should be `<image>` elements or any other element than `<path>`s.
348 // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors
349 // of elements with special meaning though. Therefore, those classes should be preserved in any
350 // special markup passed in `properties.markup`.
351 var model = this.model;
352 var children = V(model.get('markup') || model.markup);
353
354 // custom markup may contain only one children
355 if (!_.isArray(children)) children = [children];
356
357 // Cache all children elements for quicker access.
358 this._V = {}; // vectorized markup;
359 _.each(children, function(child) {
360 var c = child.attr('class');
361 c && (this._V[$.camelCase(c)] = child);
362 }, this);
363
364 // Only the connection path is mandatory
365 if (!this._V.connection) throw new Error('link: no connection path in the markup');
366
367 // partial rendering
368 this.renderTools();
369 this.renderVertexMarkers();
370 this.renderArrowheadMarkers();
371
372 this.vel.append(children);
373
374 // rendering labels has to be run after the link is appended to DOM tree. (otherwise <Text> bbox
375 // returns zero values)
376 this.renderLabels();
377
378 // start watching the ends of the link for changes
379 this.watchSource(model, model.get('source'))
380 .watchTarget(model, model.get('target'))
381 .update();
382
383 return this;
384 },
385
386 renderLabels: function() {
387
388 if (!this._V.labels) return this;
389
390 this._labelCache = {};
391 var $labels = $(this._V.labels.node).empty();
392
393 var labels = this.model.get('labels') || [];
394 if (!labels.length) return this;
395
396 var labelTemplate = _.template(this.model.get('labelMarkup') || this.model.labelMarkup);
397 // This is a prepared instance of a vectorized SVGDOM node for the label element resulting from
398 // compilation of the labelTemplate. The purpose is that all labels will just `clone()` this
399 // node to create a duplicate.
400 var labelNodeInstance = V(labelTemplate());
401
402 var canLabelMove = this.can('labelMove');
403
404 _.each(labels, function(label, idx) {
405
406 var labelNode = labelNodeInstance.clone().node;
407 V(labelNode).attr('label-idx', idx);
408 if (canLabelMove) {
409 V(labelNode).attr('cursor', 'move');
410 }
411
412 // Cache label nodes so that the `updateLabels()` can just update the label node positions.
413 this._labelCache[idx] = V(labelNode);
414
415 var $text = $(labelNode).find('text');
416 var $rect = $(labelNode).find('rect');
417
418 // Text attributes with the default `text-anchor` and font-size set.
419 var textAttributes = _.extend({ 'text-anchor': 'middle', 'font-size': 14 }, joint.util.getByPath(label, 'attrs/text', '/'));
420
421 $text.attr(_.omit(textAttributes, 'text'));
422
423 if (!_.isUndefined(textAttributes.text)) {
424
425 V($text[0]).text(textAttributes.text + '');
426 }
427
428 // Note that we first need to append the `<text>` element to the DOM in order to
429 // get its bounding box.
430 $labels.append(labelNode);
431
432 // `y-alignment` - center the text element around its y coordinate.
433 var textBbox = V($text[0]).bbox(true, $labels[0]);
434 V($text[0]).translate(0, -textBbox.height / 2);
435
436 // Add default values.
437 var rectAttributes = _.extend({
438
439 fill: 'white',
440 rx: 3,
441 ry: 3
442
443 }, joint.util.getByPath(label, 'attrs/rect', '/'));
444
445 $rect.attr(_.extend(rectAttributes, {
446 x: textBbox.x,
447 y: textBbox.y - textBbox.height / 2, // Take into account the y-alignment translation.
448 width: textBbox.width,
449 height: textBbox.height
450 }));
451
452 }, this);
453
454 return this;
455 },
456
457 renderTools: function() {
458
459 if (!this._V.linkTools) return this;
460
461 // Tools are a group of clickable elements that manipulate the whole link.
462 // A good example of this is the remove tool that removes the whole link.
463 // Tools appear after hovering the link close to the `source` element/point of the link
464 // but are offset a bit so that they don't cover the `marker-arrowhead`.
465
466 var $tools = $(this._V.linkTools.node).empty();
467 var toolTemplate = _.template(this.model.get('toolMarkup') || this.model.toolMarkup);
468 var tool = V(toolTemplate());
469
470 $tools.append(tool.node);
471
472 // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly.
473 this._toolCache = tool;
474
475 // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the
476 // link as well but only if the link is longer than `longLinkLength`.
477 if (this.options.doubleLinkTools) {
478
479 var tool2;
480 if (this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup) {
481 toolTemplate = _.template(this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup);
482 tool2 = V(toolTemplate());
483 } else {
484 tool2 = tool.clone();
485 }
486
487 $tools.append(tool2.node);
488 this._tool2Cache = tool2;
489 }
490
491 return this;
492 },
493
494 renderVertexMarkers: function() {
495
496 if (!this._V.markerVertices) return this;
497
498 var $markerVertices = $(this._V.markerVertices.node).empty();
499
500 // A special markup can be given in the `properties.vertexMarkup` property. This might be handy
501 // if default styling (elements) are not desired. This makes it possible to use any
502 // SVG elements for .marker-vertex and .marker-vertex-remove tools.
503 var markupTemplate = _.template(this.model.get('vertexMarkup') || this.model.vertexMarkup);
504
505 _.each(this.model.get('vertices'), function(vertex, idx) {
506
507 $markerVertices.append(V(markupTemplate(_.extend({ idx: idx }, vertex))).node);
508 });
509
510 return this;
511 },
512
513 renderArrowheadMarkers: function() {
514
515 // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case.
516 if (!this._V.markerArrowheads) return this;
517
518 var $markerArrowheads = $(this._V.markerArrowheads.node);
519
520 $markerArrowheads.empty();
521
522 // A special markup can be given in the `properties.vertexMarkup` property. This might be handy
523 // if default styling (elements) are not desired. This makes it possible to use any
524 // SVG elements for .marker-vertex and .marker-vertex-remove tools.
525 var markupTemplate = _.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup);
526
527 this._V.sourceArrowhead = V(markupTemplate({ end: 'source' }));
528 this._V.targetArrowhead = V(markupTemplate({ end: 'target' }));
529
530 $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node);
531
532 return this;
533 },
534
535 // Updating
536 //---------
537
538 // Default is to process the `attrs` object and set attributes on subelements based on the selectors.
539 update: function(model, attributes, opt) {
540
541 opt = opt || {};
542
543 if (!opt.updateConnectionOnly) {
544 // update SVG attributes defined by 'attrs/'.
545 this.updateAttributes();
546 }
547
548 // update the link path, label position etc.
549 this.updateConnection(opt);
550 this.updateLabelPositions();
551 this.updateToolsPosition();
552 this.updateArrowheadMarkers();
553
554 // Local perpendicular flag (as opposed to one defined on paper).
555 // Could be enabled inside a connector/router. It's valid only
556 // during the update execution.
557 this.options.perpendicular = null;
558 // Mark that postponed update has been already executed.
559 this.updatePostponed = false;
560
561 return this;
562 },
563
564 updateConnection: function(opt) {
565
566 opt = opt || {};
567
568 var model = this.model;
569 var route;
570
571 if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) {
572 // The link is being translated by an ancestor that will
573 // shift source point, target point and all vertices
574 // by an equal distance.
575 var tx = opt.tx || 0;
576 var ty = opt.ty || 0;
577
578 route = this.route = _.map(this.route, function(point) {
579 // translate point by point by delta translation
580 return g.point(point).offset(tx, ty);
581 });
582
583 // translate source and target connection and marker points.
584 this._translateConnectionPoints(tx, ty);
585
586 } else {
587 // Necessary path finding
588 route = this.route = this.findRoute(model.get('vertices') || [], opt);
589 // finds all the connection points taking new vertices into account
590 this._findConnectionPoints(route);
591 }
592
593 var pathData = this.getPathData(route);
594
595 // The markup needs to contain a `.connection`
596 this._V.connection.attr('d', pathData);
597 this._V.connectionWrap && this._V.connectionWrap.attr('d', pathData);
598
599 this._translateAndAutoOrientArrows(this._V.markerSource, this._V.markerTarget);
600 },
601
602 updateAttributes: function() {
603
604 // Update attributes.
605 _.each(this.model.get('attrs'), function(attrs, selector) {
606
607 var processedAttributes = [];
608
609 // If the `fill` or `stroke` attribute is an object, it is in the special JointJS gradient format and so
610 // it becomes a special attribute and is treated separately.
611 if (_.isObject(attrs.fill)) {
612
613 this.applyGradient(selector, 'fill', attrs.fill);
614 processedAttributes.push('fill');
615 }
616
617 if (_.isObject(attrs.stroke)) {
618
619 this.applyGradient(selector, 'stroke', attrs.stroke);
620 processedAttributes.push('stroke');
621 }
622
623 // If the `filter` attribute is an object, it is in the special JointJS filter format and so
624 // it becomes a special attribute and is treated separately.
625 if (_.isObject(attrs.filter)) {
626
627 this.applyFilter(selector, attrs.filter);
628 processedAttributes.push('filter');
629 }
630
631 // remove processed special attributes from attrs
632 if (processedAttributes.length > 0) {
633
634 processedAttributes.unshift(attrs);
635 attrs = _.omit.apply(_, processedAttributes);
636 }
637
638 this.findBySelector(selector).attr(attrs);
639
640 }, this);
641 },
642
643 _findConnectionPoints: function(vertices) {
644
645 // cache source and target points
646 var sourcePoint, targetPoint, sourceMarkerPoint, targetMarkerPoint;
647
648 var firstVertex = _.first(vertices);
649
650 sourcePoint = this.getConnectionPoint(
651 'source', this.model.get('source'), firstVertex || this.model.get('target')
652 ).round();
653
654 var lastVertex = _.last(vertices);
655
656 targetPoint = this.getConnectionPoint(
657 'target', this.model.get('target'), lastVertex || sourcePoint
658 ).round();
659
660 // Move the source point by the width of the marker taking into account
661 // its scale around x-axis. Note that scale is the only transform that
662 // makes sense to be set in `.marker-source` attributes object
663 // as all other transforms (translate/rotate) will be replaced
664 // by the `translateAndAutoOrient()` function.
665 var cache = this._markerCache;
666
667 if (this._V.markerSource) {
668
669 cache.sourceBBox = cache.sourceBBox || this._V.markerSource.bbox(true);
670
671 sourceMarkerPoint = g.point(sourcePoint).move(
672 firstVertex || targetPoint,
673 cache.sourceBBox.width * this._V.markerSource.scale().sx * -1
674 ).round();
675 }
676
677 if (this._V.markerTarget) {
678
679 cache.targetBBox = cache.targetBBox || this._V.markerTarget.bbox(true);
680
681 targetMarkerPoint = g.point(targetPoint).move(
682 lastVertex || sourcePoint,
683 cache.targetBBox.width * this._V.markerTarget.scale().sx * -1
684 ).round();
685 }
686
687 // if there was no markup for the marker, use the connection point.
688 cache.sourcePoint = sourceMarkerPoint || sourcePoint;
689 cache.targetPoint = targetMarkerPoint || targetPoint;
690
691 // make connection points public
692 this.sourcePoint = sourcePoint;
693 this.targetPoint = targetPoint;
694 },
695
696 _translateConnectionPoints: function(tx, ty) {
697
698 var cache = this._markerCache;
699
700 cache.sourcePoint.offset(tx, ty);
701 cache.targetPoint.offset(tx, ty);
702 this.sourcePoint.offset(tx, ty);
703 this.targetPoint.offset(tx, ty);
704 },
705
706 updateLabelPositions: function() {
707
708 if (!this._V.labels) return this;
709
710 // This method assumes all the label nodes are stored in the `this._labelCache` hash table
711 // by their indexes in the `this.get('labels')` array. This is done in the `renderLabels()` method.
712
713 var labels = this.model.get('labels') || [];
714 if (!labels.length) return this;
715
716 var connectionElement = this._V.connection.node;
717 var connectionLength = connectionElement.getTotalLength();
718
719 // Firefox returns connectionLength=NaN in odd cases (for bezier curves).
720 // In that case we won't update labels at all.
721 if (!_.isNaN(connectionLength)) {
722
723 var samples;
724
725 _.each(labels, function(label, idx) {
726
727 var position = label.position;
728 var distance = _.isObject(position) ? position.distance : position;
729 var offset = _.isObject(position) ? position.offset : { x: 0, y: 0 };
730
731 distance = (distance > connectionLength) ? connectionLength : distance; // sanity check
732 distance = (distance < 0) ? connectionLength + distance : distance;
733 distance = (distance > 1) ? distance : connectionLength * distance;
734
735 var labelCoordinates = connectionElement.getPointAtLength(distance);
736
737 if (_.isObject(offset)) {
738
739 // Just offset the label by the x,y provided in the offset object.
740 labelCoordinates = g.point(labelCoordinates).offset(offset.x, offset.y);
741
742 } else if (_.isNumber(offset)) {
743
744 if (!samples) {
745 samples = this._samples || this._V.connection.sample(this.options.sampleInterval);
746 }
747
748 // Offset the label by the amount provided in `offset` to an either
749 // side of the link.
750
751 // 1. Find the closest sample & its left and right neighbours.
752 var minSqDistance = Infinity;
753 var closestSample;
754 var closestSampleIndex;
755 var p;
756 var sqDistance;
757 for (var i = 0, len = samples.length; i < len; i++) {
758 p = samples[i];
759 sqDistance = g.line(p, labelCoordinates).squaredLength();
760 if (sqDistance < minSqDistance) {
761 minSqDistance = sqDistance;
762 closestSample = p;
763 closestSampleIndex = i;
764 }
765 }
766 var prevSample = samples[closestSampleIndex - 1];
767 var nextSample = samples[closestSampleIndex + 1];
768
769 // 2. Offset the label on the perpendicular line between
770 // the current label coordinate ("at `distance`") and
771 // the next sample.
772 var angle = 0;
773 if (nextSample) {
774 angle = g.point(labelCoordinates).theta(nextSample);
775 } else if (prevSample) {
776 angle = g.point(prevSample).theta(labelCoordinates);
777 }
778 labelCoordinates = g.point(labelCoordinates).offset(offset).rotate(labelCoordinates, angle - 90);
779 }
780
781 this._labelCache[idx].attr('transform', 'translate(' + labelCoordinates.x + ', ' + labelCoordinates.y + ')');
782
783 }, this);
784 }
785
786 return this;
787 },
788
789
790 updateToolsPosition: function() {
791
792 if (!this._V.linkTools) return this;
793
794 // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker.
795 // Note that the offset is hardcoded here. The offset should be always
796 // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking
797 // this up all the time would be slow.
798
799 var scale = '';
800 var offset = this.options.linkToolsOffset;
801 var connectionLength = this.getConnectionLength();
802
803 // Firefox returns connectionLength=NaN in odd cases (for bezier curves).
804 // In that case we won't update tools position at all.
805 if (!_.isNaN(connectionLength)) {
806
807 // If the link is too short, make the tools half the size and the offset twice as low.
808 if (connectionLength < this.options.shortLinkLength) {
809 scale = 'scale(.5)';
810 offset /= 2;
811 }
812
813 var toolPosition = this.getPointAtLength(offset);
814
815 this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale);
816
817 if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) {
818
819 var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset;
820
821 toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset);
822 this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale);
823 this._tool2Cache.attr('visibility', 'visible');
824
825 } else if (this.options.doubleLinkTools) {
826
827 this._tool2Cache.attr('visibility', 'hidden');
828 }
829 }
830
831 return this;
832 },
833
834
835 updateArrowheadMarkers: function() {
836
837 if (!this._V.markerArrowheads) return this;
838
839 // getting bbox of an element with `display="none"` in IE9 ends up with access violation
840 if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this;
841
842 var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1;
843 this._V.sourceArrowhead.scale(sx);
844 this._V.targetArrowhead.scale(sx);
845
846 this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead);
847
848 return this;
849 },
850
851 // Returns a function observing changes on an end of the link. If a change happens and new end is a new model,
852 // it stops listening on the previous one and starts listening to the new one.
853 createWatcher: function(endType) {
854
855 // create handler for specific end type (source|target).
856 var onModelChange = _.partial(this.onEndModelChange, endType);
857
858 function watchEndModel(link, end) {
859
860 end = end || {};
861
862 var endModel = null;
863 var previousEnd = link.previous(endType) || {};
864
865 if (previousEnd.id) {
866 this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange);
867 }
868
869 if (end.id) {
870 // If the observed model changes, it caches a new bbox and do the link update.
871 endModel = this.paper.getModelById(end.id);
872 this.listenTo(endModel, 'change', onModelChange);
873 }
874
875 onModelChange.call(this, endModel, { cacheOnly: true });
876
877 return this;
878 }
879
880 return watchEndModel;
881 },
882
883 onEndModelChange: function(endType, endModel, opt) {
884
885 var doUpdate = !opt.cacheOnly;
886 var model = this.model;
887 var end = model.get(endType) || {};
888
889 if (endModel) {
890
891 var selector = this.constructor.makeSelector(end);
892 var oppositeEndType = endType == 'source' ? 'target' : 'source';
893 var oppositeEnd = model.get(oppositeEndType) || {};
894 var oppositeSelector = oppositeEnd.id && this.constructor.makeSelector(oppositeEnd);
895
896 // Caching end models bounding boxes.
897 // If `opt.handleBy` equals the client-side ID of this link view and it is a loop link, then we already cached
898 // the bounding boxes in the previous turn (e.g. for loop link, the change:source event is followed
899 // by change:target and so on change:source, we already chached the bounding boxes of - the same - element).
900 if (opt.handleBy === this.cid && selector == oppositeSelector) {
901
902 // Source and target elements are identical. We're dealing with a loop link. We are handling `change` event for the
903 // second time now. There is no need to calculate bbox and find magnet element again.
904 // It was calculated already for opposite link end.
905 this[endType + 'BBox'] = this[oppositeEndType + 'BBox'];
906 this[endType + 'View'] = this[oppositeEndType + 'View'];
907 this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet'];
908
909 } else if (opt.translateBy) {
910 // `opt.translateBy` optimizes the way we calculate bounding box of the source/target element.
911 // If `opt.translateBy` is an ID of the element that was originally translated. This allows us
912 // to just offset the cached bounding box by the translation instead of calculating the bounding
913 // box from scratch on every translate.
914
915 var bbox = this[endType + 'BBox'];
916 bbox.x += opt.tx;
917 bbox.y += opt.ty;
918
919 } else {
920 // The slowest path, source/target could have been rotated or resized or any attribute
921 // that affects the bounding box of the view might have been changed.
922
923 var view = this.paper.findViewByModel(end.id);
924 var magnetElement = view.el.querySelector(selector);
925
926 this[endType + 'BBox'] = view.getStrokeBBox(magnetElement);
927 this[endType + 'View'] = view;
928 this[endType + 'Magnet'] = magnetElement;
929 }
930
931 if (opt.handleBy === this.cid && opt.translateBy &&
932 model.isEmbeddedIn(endModel) &&
933 !_.isEmpty(model.get('vertices'))) {
934 // Loop link whose element was translated and that has vertices (that need to be translated with
935 // the parent in which my element is embedded).
936 // If the link is embedded, has a loop and vertices and the end model
937 // has been translated, do not update yet. There are vertices still to be updated (change:vertices
938 // event will come in the next turn).
939 doUpdate = false;
940 }
941
942 if (!this.updatePostponed && oppositeEnd.id) {
943 // The update was not postponed (that can happen e.g. on the first change event) and the opposite
944 // end is a model (opposite end is the opposite end of the link we're just updating, e.g. if
945 // we're reacting on change:source event, the oppositeEnd is the target model).
946
947 var oppositeEndModel = this.paper.getModelById(oppositeEnd.id);
948
949 // Passing `handleBy` flag via event option.
950 // Note that if we are listening to the same model for event 'change' twice.
951 // The same event will be handled by this method also twice.
952 if (end.id === oppositeEnd.id) {
953 // We're dealing with a loop link. Tell the handlers in the next turn that they should update
954 // the link instead of me. (We know for sure there will be a next turn because
955 // loop links react on at least two events: change on the source model followed by a change on
956 // the target model).
957 opt.handleBy = this.cid;
958 }
959
960 if (opt.handleBy === this.cid || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) {
961
962 // Here are two options:
963 // - Source and target are connected to the same model (not necessarily the same port).
964 // - Both end models are translated by the same ancestor. We know that opposite end
965 // model will be translated in the next turn as well.
966 // In both situations there will be more changes on the model that trigger an
967 // update. So there is no need to update the linkView yet.
968 this.updatePostponed = true;
969 doUpdate = false;
970 }
971 }
972
973 } else {
974
975 // the link end is a point ~ rect 1x1
976 this[endType + 'BBox'] = g.rect(end.x || 0, end.y || 0, 1, 1);
977 this[endType + 'View'] = this[endType + 'Magnet'] = null;
978 }
979
980 if (doUpdate) {
981 opt.updateConnectionOnly = true;
982 this.update(model, null, opt);
983 }
984 },
985
986 _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) {
987
988 // Make the markers "point" to their sticky points being auto-oriented towards
989 // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them.
990 if (sourceArrow) {
991 sourceArrow.translateAndAutoOrient(
992 this.sourcePoint,
993 _.first(this.route) || this.targetPoint,
994 this.paper.viewport
995 );
996 }
997
998 if (targetArrow) {
999 targetArrow.translateAndAutoOrient(
1000 this.targetPoint,
1001 _.last(this.route) || this.sourcePoint,
1002 this.paper.viewport
1003 );
1004 }
1005 },
1006
1007 removeVertex: function(idx) {
1008
1009 var vertices = _.clone(this.model.get('vertices'));
1010
1011 if (vertices && vertices.length) {
1012
1013 vertices.splice(idx, 1);
1014 this.model.set('vertices', vertices, { ui: true });
1015 }
1016
1017 return this;
1018 },
1019
1020 // This method ads a new vertex to the `vertices` array of `.connection`. This method
1021 // uses a heuristic to find the index at which the new `vertex` should be placed at assuming
1022 // the new vertex is somewhere on the path.
1023 addVertex: function(vertex) {
1024
1025 // As it is very hard to find a correct index of the newly created vertex,
1026 // a little heuristics is taking place here.
1027 // The heuristics checks if length of the newly created
1028 // path is lot more than length of the old path. If this is the case,
1029 // new vertex was probably put into a wrong index.
1030 // Try to put it into another index and repeat the heuristics again.
1031
1032 var vertices = (this.model.get('vertices') || []).slice();
1033 // Store the original vertices for a later revert if needed.
1034 var originalVertices = vertices.slice();
1035
1036 // A `<path>` element used to compute the length of the path during heuristics.
1037 var path = this._V.connection.node.cloneNode(false);
1038
1039 // Length of the original path.
1040 var originalPathLength = path.getTotalLength();
1041 // Current path length.
1042 var pathLength;
1043 // Tolerance determines the highest possible difference between the length
1044 // of the old and new path. The number has been chosen heuristically.
1045 var pathLengthTolerance = 20;
1046 // Total number of vertices including source and target points.
1047 var idx = vertices.length + 1;
1048
1049 // Loop through all possible indexes and check if the difference between
1050 // path lengths changes significantly. If not, the found index is
1051 // most probably the right one.
1052 while (idx--) {
1053
1054 vertices.splice(idx, 0, vertex);
1055 V(path).attr('d', this.getPathData(this.findRoute(vertices)));
1056
1057 pathLength = path.getTotalLength();
1058
1059 // Check if the path lengths changed significantly.
1060 if (pathLength - originalPathLength > pathLengthTolerance) {
1061
1062 // Revert vertices to the original array. The path length has changed too much
1063 // so that the index was not found yet.
1064 vertices = originalVertices.slice();
1065
1066 } else {
1067
1068 break;
1069 }
1070 }
1071
1072 if (idx === -1) {
1073 // If no suitable index was found for such a vertex, make the vertex the first one.
1074 idx = 0;
1075 vertices.splice(idx, 0, vertex);
1076 }
1077
1078 this.model.set('vertices', vertices, { ui: true });
1079
1080 return idx;
1081 },
1082
1083 // Send a token (an SVG element, usually a circle) along the connection path.
1084 // Example: `paper.findViewByModel(link).sendToken(V('circle', { r: 7, fill: 'green' }).node)`
1085 // `duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`.
1086 // `callback` is optional and is a function to be called once the token reaches the target.
1087 sendToken: function(token, duration, callback) {
1088
1089 duration = duration || 1000;
1090
1091 V(this.paper.viewport).append(token);
1092 V(token).animateAlongPath({ dur: duration + 'ms', repeatCount: 1 }, this._V.connection.node);
1093 _.delay(function() { V(token).remove(); callback && callback(); }, duration);
1094 },
1095
1096 findRoute: function(oldVertices) {
1097
1098 var namespace = joint.routers;
1099 var router = this.model.get('router');
1100 var defaultRouter = this.paper.options.defaultRouter;
1101
1102 if (!router) {
1103
1104 if (this.model.get('manhattan')) {
1105 // backwards compability
1106 router = { name: 'orthogonal' };
1107 } else if (defaultRouter) {
1108 router = defaultRouter;
1109 } else {
1110 return oldVertices;
1111 }
1112 }
1113
1114 var args = router.args || {};
1115 var routerFn = _.isFunction(router) ? router : namespace[router.name];
1116
1117 if (!_.isFunction(routerFn)) {
1118 throw new Error('unknown router: "' + router.name + '"');
1119 }
1120
1121 var newVertices = routerFn.call(this, oldVertices || [], args, this);
1122
1123 return newVertices;
1124 },
1125
1126 // Return the `d` attribute value of the `<path>` element representing the link
1127 // between `source` and `target`.
1128 getPathData: function(vertices) {
1129
1130 var namespace = joint.connectors;
1131 var connector = this.model.get('connector');
1132 var defaultConnector = this.paper.options.defaultConnector;
1133
1134 if (!connector) {
1135
1136 // backwards compability
1137 if (this.model.get('smooth')) {
1138 connector = { name: 'smooth' };
1139 } else {
1140 connector = defaultConnector || {};
1141 }
1142 }
1143
1144 var connectorFn = _.isFunction(connector) ? connector : namespace[connector.name];
1145 var args = connector.args || {};
1146
1147 if (!_.isFunction(connectorFn)) {
1148 throw new Error('unknown connector: "' + connector.name + '"');
1149 }
1150
1151 var pathData = connectorFn.call(
1152 this,
1153 this._markerCache.sourcePoint, // Note that the value is translated by the size
1154 this._markerCache.targetPoint, // of the marker. (We'r not using this.sourcePoint)
1155 vertices || (this.model.get('vertices') || {}),
1156 args, // options
1157 this
1158 );
1159
1160 return pathData;
1161 },
1162
1163 // Find a point that is the start of the connection.
1164 // If `selectorOrPoint` is a point, then we're done and that point is the start of the connection.
1165 // If the `selectorOrPoint` is an element however, we need to know a reference point (or element)
1166 // that the link leads to in order to determine the start of the connection on the original element.
1167 getConnectionPoint: function(end, selectorOrPoint, referenceSelectorOrPoint) {
1168
1169 var spot;
1170
1171 // If the `selectorOrPoint` (or `referenceSelectorOrPoint`) is `undefined`, the `source`/`target` of the link model is `undefined`.
1172 // We want to allow this however so that one can create links such as `var link = new joint.dia.Link` and
1173 // set the `source`/`target` later.
1174 _.isEmpty(selectorOrPoint) && (selectorOrPoint = { x: 0, y: 0 });
1175 _.isEmpty(referenceSelectorOrPoint) && (referenceSelectorOrPoint = { x: 0, y: 0 });
1176
1177 if (!selectorOrPoint.id) {
1178
1179 // If the source is a point, we don't need a reference point to find the sticky point of connection.
1180 spot = g.point(selectorOrPoint);
1181
1182 } else {
1183
1184 // If the source is an element, we need to find a point on the element boundary that is closest
1185 // to the reference point (or reference element).
1186 // Get the bounding box of the spot relative to the paper viewport. This is necessary
1187 // in order to follow paper viewport transformations (scale/rotate).
1188 // `_sourceBbox` (`_targetBbox`) comes from `_sourceBboxUpdate` (`_sourceBboxUpdate`)
1189 // method, it exists since first render and are automatically updated
1190 var spotBbox = end === 'source' ? this.sourceBBox : this.targetBBox;
1191
1192 var reference;
1193
1194 if (!referenceSelectorOrPoint.id) {
1195
1196 // Reference was passed as a point, therefore, we're ready to find the sticky point of connection on the source element.
1197 reference = g.point(referenceSelectorOrPoint);
1198
1199 } else {
1200
1201 // Reference was passed as an element, therefore we need to find a point on the reference
1202 // element boundary closest to the source element.
1203 // Get the bounding box of the spot relative to the paper viewport. This is necessary
1204 // in order to follow paper viewport transformations (scale/rotate).
1205 var referenceBbox = end === 'source' ? this.targetBBox : this.sourceBBox;
1206
1207 reference = g.rect(referenceBbox).intersectionWithLineFromCenterToPoint(g.rect(spotBbox).center());
1208 reference = reference || g.rect(referenceBbox).center();
1209 }
1210
1211 // If `perpendicularLinks` flag is set on the paper and there are vertices
1212 // on the link, then try to find a connection point that makes the link perpendicular
1213 // even though the link won't point to the center of the targeted object.
1214 if (this.paper.options.perpendicularLinks || this.options.perpendicular) {
1215
1216 var horizontalLineRect = g.rect(0, reference.y, this.paper.options.width, 1);
1217 var verticalLineRect = g.rect(reference.x, 0, 1, this.paper.options.height);
1218 var nearestSide;
1219
1220 if (horizontalLineRect.intersect(g.rect(spotBbox))) {
1221
1222 nearestSide = g.rect(spotBbox).sideNearestToPoint(reference);
1223 switch (nearestSide) {
1224 case 'left':
1225 spot = g.point(spotBbox.x, reference.y);
1226 break;
1227 case 'right':
1228 spot = g.point(spotBbox.x + spotBbox.width, reference.y);
1229 break;
1230 default:
1231 spot = g.rect(spotBbox).center();
1232 break;
1233 }
1234
1235 } else if (verticalLineRect.intersect(g.rect(spotBbox))) {
1236
1237 nearestSide = g.rect(spotBbox).sideNearestToPoint(reference);
1238 switch (nearestSide) {
1239 case 'top':
1240 spot = g.point(reference.x, spotBbox.y);
1241 break;
1242 case 'bottom':
1243 spot = g.point(reference.x, spotBbox.y + spotBbox.height);
1244 break;
1245 default:
1246 spot = g.rect(spotBbox).center();
1247 break;
1248 }
1249
1250 } else {
1251
1252 // If there is no intersection horizontally or vertically with the object bounding box,
1253 // then we fall back to the regular situation finding straight line (not perpendicular)
1254 // between the object and the reference point.
1255
1256 spot = g.rect(spotBbox).intersectionWithLineFromCenterToPoint(reference);
1257 spot = spot || g.rect(spotBbox).center();
1258 }
1259
1260 } else if (this.paper.options.linkConnectionPoint) {
1261
1262 var view = end === 'target' ? this.targetView : this.sourceView;
1263 var magnet = end === 'target' ? this.targetMagnet : this.sourceMagnet;
1264
1265 spot = this.paper.options.linkConnectionPoint(this, view, magnet, reference);
1266
1267 } else {
1268
1269 spot = g.rect(spotBbox).intersectionWithLineFromCenterToPoint(reference);
1270 spot = spot || g.rect(spotBbox).center();
1271 }
1272 }
1273
1274 return spot;
1275 },
1276
1277 // Public API
1278 // ----------
1279
1280 getConnectionLength: function() {
1281
1282 return this._V.connection.node.getTotalLength();
1283 },
1284
1285 getPointAtLength: function(length) {
1286
1287 return this._V.connection.node.getPointAtLength(length);
1288 },
1289
1290 // Interaction. The controller part.
1291 // ---------------------------------
1292
1293 _beforeArrowheadMove: function() {
1294
1295 this._z = this.model.get('z');
1296 this.model.toFront();
1297
1298 // Let the pointer propagate throught the link view elements so that
1299 // the `evt.target` is another element under the pointer, not the link itself.
1300 this.el.style.pointerEvents = 'none';
1301
1302 if (this.paper.options.markAvailable) {
1303 this._markAvailableMagnets();
1304 }
1305 },
1306
1307 _afterArrowheadMove: function() {
1308
1309 if (!_.isNull(this._z)) {
1310 this.model.set('z', this._z, { ui: true });
1311 this._z = null;
1312 }
1313
1314 // Put `pointer-events` back to its original value. See `startArrowheadMove()` for explanation.
1315 // Value `auto` doesn't work in IE9. We force to use `visiblePainted` instead.
1316 // See `https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events`.
1317 this.el.style.pointerEvents = 'visiblePainted';
1318
1319 if (this.paper.options.markAvailable) {
1320 this._unmarkAvailableMagnets();
1321 }
1322 },
1323
1324 _createValidateConnectionArgs: function(arrowhead) {
1325 // It makes sure the arguments for validateConnection have the following form:
1326 // (source view, source magnet, target view, target magnet and link view)
1327 var args = [];
1328
1329 args[4] = arrowhead;
1330 args[5] = this;
1331
1332 var oppositeArrowhead;
1333 var i = 0;
1334 var j = 0;
1335
1336 if (arrowhead === 'source') {
1337 i = 2;
1338 oppositeArrowhead = 'target';
1339 } else {
1340 j = 2;
1341 oppositeArrowhead = 'source';
1342 }
1343
1344 var end = this.model.get(oppositeArrowhead);
1345
1346 if (end.id) {
1347 args[i] = this.paper.findViewByModel(end.id);
1348 args[i + 1] = end.selector && args[i].el.querySelector(end.selector);
1349 }
1350
1351 function validateConnectionArgs(cellView, magnet) {
1352 args[j] = cellView;
1353 args[j + 1] = cellView.el === magnet ? undefined : magnet;
1354 return args;
1355 }
1356
1357 return validateConnectionArgs;
1358 },
1359
1360 _markAvailableMagnets: function() {
1361
1362 var elements = this.paper.model.getElements();
1363 var validate = this.paper.options.validateConnection;
1364
1365 _.chain(elements).map(this.paper.findViewByModel, this.paper).each(function(view) {
1366
1367 var isElementAvailable = view.el.getAttribute('magnet') !== 'false' &&
1368 validate.apply(this.paper, this._validateConnectionArgs(view, null));
1369
1370 var availableMagnets = _.filter(view.el.querySelectorAll('[magnet]'), function(magnet) {
1371 return validate.apply(this.paper, this._validateConnectionArgs(view, magnet));
1372 }, this);
1373
1374 if (isElementAvailable) {
1375 V(view.el).addClass('available-magnet');
1376 }
1377
1378 _.each(availableMagnets, function(magnet) {
1379 V(magnet).addClass('available-magnet');
1380 });
1381
1382 if (isElementAvailable || availableMagnets.length) {
1383 V(view.el).addClass('available-cell');
1384 }
1385
1386 }, this).value();
1387 },
1388
1389 _unmarkAvailableMagnets: function() {
1390
1391 _.each(this.paper.el.querySelectorAll('.available-cell, .available-magnet'), function(magnet) {
1392 V(magnet).removeClass('available-magnet').removeClass('available-cell');
1393 });
1394 },
1395
1396 startArrowheadMove: function(end, opt) {
1397 opt = _.defaults(opt || {}, { whenNotAllowed: 'revert' });
1398 // Allow to delegate events from an another view to this linkView in order to trigger arrowhead
1399 // move without need to click on the actual arrowhead dom element.
1400 this._action = 'arrowhead-move';
1401 this._whenNotAllowed = opt.whenNotAllowed;
1402 this._arrowhead = end;
1403 this._initialEnd = _.clone(this.model.get(end)) || { x: 0, y: 0 };
1404 this._validateConnectionArgs = this._createValidateConnectionArgs(this._arrowhead);
1405 this._beforeArrowheadMove();
1406 },
1407
1408 // Return `true` if the link is allowed to perform a certain UI `feature`.
1409 // Example: `can('vertexMove')`, `can('labelMove')`.
1410 can: function(feature) {
1411
1412 var interactive = _.isFunction(this.options.interactive) ? this.options.interactive(this, 'pointerdown') : this.options.interactive;
1413 if (interactive === false) return false;
1414 if (!_.isObject(interactive) || interactive[feature] !== false) return true;
1415 return false;
1416 },
1417
1418 pointerdown: function(evt, x, y) {
1419
1420 joint.dia.CellView.prototype.pointerdown.apply(this, arguments);
1421 this.notify('link:pointerdown', evt, x, y);
1422
1423 this._dx = x;
1424 this._dy = y;
1425
1426 // if are simulating pointerdown on a link during a magnet click, skip link interactions
1427 if (evt.target.getAttribute('magnet') != null) return;
1428
1429 var interactive = _.isFunction(this.options.interactive) ? this.options.interactive(this, 'pointerdown') : this.options.interactive;
1430 if (interactive === false) return;
1431
1432 var className = evt.target.getAttribute('class');
1433 var parentClassName = evt.target.parentNode.getAttribute('class');
1434 var labelNode;
1435 if (parentClassName === 'label') {
1436 className = parentClassName;
1437 labelNode = evt.target.parentNode;
1438 } else {
1439 labelNode = evt.target;
1440 }
1441
1442 switch (className) {
1443
1444 case 'marker-vertex':
1445 if (this.can('vertexMove')) {
1446 this._action = 'vertex-move';
1447 this._vertexIdx = evt.target.getAttribute('idx');
1448 }
1449 break;
1450
1451 case 'marker-vertex-remove':
1452 case 'marker-vertex-remove-area':
1453 if (this.can('vertexRemove')) {
1454 this.removeVertex(evt.target.getAttribute('idx'));
1455 }
1456 break;
1457
1458 case 'marker-arrowhead':
1459 if (this.can('arrowheadMove')) {
1460 this.startArrowheadMove(evt.target.getAttribute('end'));
1461 }
1462 break;
1463
1464 case 'label':
1465 if (this.can('labelMove')) {
1466 this._action = 'label-move';
1467 this._labelIdx = parseInt(V(labelNode).attr('label-idx'), 10);
1468 // Precalculate samples so that we don't have to do that
1469 // over and over again while dragging the label.
1470 this._samples = this._V.connection.sample(1);
1471 this._linkLength = this._V.connection.node.getTotalLength();
1472 }
1473 break;
1474
1475 default:
1476
1477 var targetParentEvent = evt.target.parentNode.getAttribute('event');
1478 if (targetParentEvent) {
1479
1480 // `remove` event is built-in. Other custom events are triggered on the paper.
1481 if (targetParentEvent === 'remove') {
1482 this.model.remove();
1483 } else {
1484 this.paper.trigger(targetParentEvent, evt, this, x, y);
1485 }
1486
1487 } else {
1488 if (this.can('vertexAdd')) {
1489
1490 // Store the index at which the new vertex has just been placed.
1491 // We'll be update the very same vertex position in `pointermove()`.
1492 this._vertexIdx = this.addVertex({ x: x, y: y });
1493 this._action = 'vertex-move';
1494 }
1495 }
1496 }
1497 },
1498
1499 pointermove: function(evt, x, y) {
1500
1501 switch (this._action) {
1502
1503 case 'vertex-move':
1504
1505 var vertices = _.clone(this.model.get('vertices'));
1506 vertices[this._vertexIdx] = { x: x, y: y };
1507 this.model.set('vertices', vertices, { ui: true });
1508 break;
1509
1510 case 'label-move':
1511
1512 var dragPoint = { x: x, y: y };
1513 var label = this.model.get('labels')[this._labelIdx];
1514 var samples = this._samples;
1515 var minSqDistance = Infinity;
1516 var closestSample;
1517 var closestSampleIndex;
1518 var p;
1519 var sqDistance;
1520 for (var i = 0, len = samples.length; i < len; i++) {
1521 p = samples[i];
1522 sqDistance = g.line(p, dragPoint).squaredLength();
1523 if (sqDistance < minSqDistance) {
1524 minSqDistance = sqDistance;
1525 closestSample = p;
1526 closestSampleIndex = i;
1527 }
1528 }
1529 var prevSample = samples[closestSampleIndex - 1];
1530 var nextSample = samples[closestSampleIndex + 1];
1531
1532 var closestSampleDistance = g.point(closestSample).distance(dragPoint);
1533 var offset = 0;
1534 if (prevSample && nextSample) {
1535 offset = g.line(prevSample, nextSample).pointOffset(dragPoint);
1536 } else if (prevSample) {
1537 offset = g.line(prevSample, closestSample).pointOffset(dragPoint);
1538 } else if (nextSample) {
1539 offset = g.line(closestSample, nextSample).pointOffset(dragPoint);
1540 }
1541
1542 this.model.label(this._labelIdx, {
1543 position: {
1544 distance: closestSample.distance / this._linkLength,
1545 offset: offset
1546 }
1547 });
1548 break;
1549
1550 case 'arrowhead-move':
1551
1552 if (this.paper.options.snapLinks) {
1553
1554 // checking view in close area of the pointer
1555
1556 var r = this.paper.options.snapLinks.radius || 50;
1557 var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r });
1558
1559 this._closestView && this._closestView.unhighlight(this._closestEnd.selector, { connecting: true, snapping: true });
1560 this._closestView = this._closestEnd = null;
1561
1562 var distance;
1563 var minDistance = Number.MAX_VALUE;
1564 var pointer = g.point(x, y);
1565
1566 _.each(viewsInArea, function(view) {
1567
1568 // skip connecting to the element in case '.': { magnet: false } attribute present
1569 if (view.el.getAttribute('magnet') !== 'false') {
1570
1571 // find distance from the center of the model to pointer coordinates
1572 distance = view.model.getBBox().center().distance(pointer);
1573
1574 // the connection is looked up in a circle area by `distance < r`
1575 if (distance < r && distance < minDistance) {
1576
1577 if (this.paper.options.validateConnection.apply(
1578 this.paper, this._validateConnectionArgs(view, null)
1579 )) {
1580 minDistance = distance;
1581 this._closestView = view;
1582 this._closestEnd = { id: view.model.id };
1583 }
1584 }
1585 }
1586
1587 view.$('[magnet]').each(_.bind(function(index, magnet) {
1588
1589 var bbox = V(magnet).bbox(false, this.paper.viewport);
1590
1591 distance = pointer.distance({
1592 x: bbox.x + bbox.width / 2,
1593 y: bbox.y + bbox.height / 2
1594 });
1595
1596 if (distance < r && distance < minDistance) {
1597
1598 if (this.paper.options.validateConnection.apply(
1599 this.paper, this._validateConnectionArgs(view, magnet)
1600 )) {
1601 minDistance = distance;
1602 this._closestView = view;
1603 this._closestEnd = {
1604 id: view.model.id,
1605 selector: view.getSelector(magnet),
1606 port: magnet.getAttribute('port')
1607 };
1608 }
1609 }
1610
1611 }, this));
1612
1613 }, this);
1614
1615 this._closestView && this._closestView.highlight(this._closestEnd.selector, { connecting: true, snapping: true });
1616
1617 this.model.set(this._arrowhead, this._closestEnd || { x: x, y: y }, { ui: true });
1618
1619 } else {
1620
1621 // checking views right under the pointer
1622
1623 // Touchmove event's target is not reflecting the element under the coordinates as mousemove does.
1624 // It holds the element when a touchstart triggered.
1625 var target = (evt.type === 'mousemove')
1626 ? evt.target
1627 : document.elementFromPoint(evt.clientX, evt.clientY);
1628
1629 if (this._targetEvent !== target) {
1630 // Unhighlight the previous view under pointer if there was one.
1631 this._magnetUnderPointer && this._viewUnderPointer.unhighlight(this._magnetUnderPointer, { connecting: true });
1632 this._viewUnderPointer = this.paper.findView(target);
1633 if (this._viewUnderPointer) {
1634 // If we found a view that is under the pointer, we need to find the closest
1635 // magnet based on the real target element of the event.
1636 this._magnetUnderPointer = this._viewUnderPointer.findMagnet(target);
1637
1638 if (this._magnetUnderPointer && this.paper.options.validateConnection.apply(
1639 this.paper,
1640 this._validateConnectionArgs(this._viewUnderPointer, this._magnetUnderPointer)
1641 )) {
1642 // If there was no magnet found, do not highlight anything and assume there
1643 // is no view under pointer we're interested in reconnecting to.
1644 // This can only happen if the overall element has the attribute `'.': { magnet: false }`.
1645 this._magnetUnderPointer && this._viewUnderPointer.highlight(this._magnetUnderPointer, { connecting: true });
1646 } else {
1647 // This type of connection is not valid. Disregard this magnet.
1648 this._magnetUnderPointer = null;
1649 }
1650 } else {
1651 // Make sure we'll unset previous magnet.
1652 this._magnetUnderPointer = null;
1653 }
1654 }
1655
1656 this._targetEvent = target;
1657
1658 this.model.set(this._arrowhead, { x: x, y: y }, { ui: true });
1659 }
1660
1661 break;
1662 }
1663
1664 this._dx = x;
1665 this._dy = y;
1666
1667 joint.dia.CellView.prototype.pointermove.apply(this, arguments);
1668 this.notify('link:pointermove', evt, x, y);
1669 },
1670
1671 pointerup: function(evt, x, y) {
1672
1673 if (this._action === 'label-move') {
1674
1675 this._samples = null;
1676
1677 } else if (this._action === 'arrowhead-move') {
1678
1679 var paperOptions = this.paper.options;
1680 var arrowhead = this._arrowhead;
1681
1682 if (paperOptions.snapLinks) {
1683
1684 // Finish off link snapping. Everything except view unhighlighting was already done on pointermove.
1685 this._closestView && this._closestView.unhighlight(this._closestEnd.selector, { connecting: true, snapping: true });
1686 this._closestView = this._closestEnd = null;
1687
1688 } else {
1689
1690 var viewUnderPointer = this._viewUnderPointer;
1691 var magnetUnderPointer = this._magnetUnderPointer;
1692
1693 this._viewUnderPointer = null;
1694 this._magnetUnderPointer = null;
1695
1696 if (magnetUnderPointer) {
1697
1698 viewUnderPointer.unhighlight(magnetUnderPointer, { connecting: true });
1699 // Find a unique `selector` of the element under pointer that is a magnet. If the
1700 // `this._magnetUnderPointer` is the root element of the `this._viewUnderPointer` itself,
1701 // the returned `selector` will be `undefined`. That means we can directly pass it to the
1702 // `source`/`target` attribute of the link model below.
1703 var selector = viewUnderPointer.getSelector(magnetUnderPointer);
1704 var port = magnetUnderPointer.getAttribute('port');
1705 var arrowheadValue = { id: viewUnderPointer.model.id };
1706 if (selector != null) arrowheadValue.port = port;
1707 if (port != null) arrowheadValue.selector = selector;
1708 this.model.set(arrowhead, arrowheadValue, { ui: true });
1709 }
1710 }
1711
1712 // If the changed link is not allowed, revert to its previous state.
1713 if (!this.paper.linkAllowed(this)) {
1714
1715 switch (this._whenNotAllowed) {
1716
1717 case 'remove':
1718 this.model.remove();
1719 break;
1720
1721 case 'revert':
1722 default:
1723 this.model.set(arrowhead, this._initialEnd, { ui: true });
1724 break;
1725 }
1726 }
1727
1728 // Reparent the link if embedding is enabled
1729 if (paperOptions.embeddingMode && this.model.reparent()) {
1730 // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()).
1731 this._z = null;
1732 }
1733
1734 this._afterArrowheadMove();
1735 }
1736
1737 this._action = null;
1738 this._whenNotAllowed = null;
1739
1740 this.notify('link:pointerup', evt, x, y);
1741 joint.dia.CellView.prototype.pointerup.apply(this, arguments);
1742 }
1743
1744}, {
1745
1746 makeSelector: function(end) {
1747
1748 var selector = '[model-id="' + end.id + '"]';
1749 // `port` has a higher precendence over `selector`. This is because the selector to the magnet
1750 // might change while the name of the port can stay the same.
1751 if (end.port) {
1752 selector += ' [port="' + end.port + '"]';
1753 } else if (end.selector) {
1754 selector += ' ' + end.selector;
1755 }
1756
1757 return selector;
1758 }
1759
1760});