@joint/core 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/LICENSE +376 -0
  2. package/README.md +49 -0
  3. package/dist/geometry.js +6486 -0
  4. package/dist/geometry.min.js +8 -0
  5. package/dist/joint.d.ts +5536 -0
  6. package/dist/joint.js +39629 -0
  7. package/dist/joint.min.js +8 -0
  8. package/dist/joint.nowrap.js +39626 -0
  9. package/dist/joint.nowrap.min.js +8 -0
  10. package/dist/vectorizer.js +9135 -0
  11. package/dist/vectorizer.min.js +8 -0
  12. package/dist/version.mjs +3 -0
  13. package/index.js +3 -0
  14. package/joint.mjs +27 -0
  15. package/package.json +192 -0
  16. package/src/V/annotation.mjs +0 -0
  17. package/src/V/index.mjs +2642 -0
  18. package/src/anchors/index.mjs +123 -0
  19. package/src/config/index.mjs +12 -0
  20. package/src/connectionPoints/index.mjs +202 -0
  21. package/src/connectionStrategies/index.mjs +73 -0
  22. package/src/connectors/curve.mjs +553 -0
  23. package/src/connectors/index.mjs +6 -0
  24. package/src/connectors/jumpover.mjs +452 -0
  25. package/src/connectors/normal.mjs +12 -0
  26. package/src/connectors/rounded.mjs +17 -0
  27. package/src/connectors/smooth.mjs +44 -0
  28. package/src/connectors/straight.mjs +110 -0
  29. package/src/dia/Cell.mjs +945 -0
  30. package/src/dia/CellView.mjs +1316 -0
  31. package/src/dia/Element.mjs +519 -0
  32. package/src/dia/ElementView.mjs +859 -0
  33. package/src/dia/Graph.mjs +1112 -0
  34. package/src/dia/HighlighterView.mjs +319 -0
  35. package/src/dia/Link.mjs +565 -0
  36. package/src/dia/LinkView.mjs +2207 -0
  37. package/src/dia/Paper.mjs +3171 -0
  38. package/src/dia/PaperLayer.mjs +75 -0
  39. package/src/dia/ToolView.mjs +69 -0
  40. package/src/dia/ToolsView.mjs +128 -0
  41. package/src/dia/attributes/calc.mjs +128 -0
  42. package/src/dia/attributes/connection.mjs +75 -0
  43. package/src/dia/attributes/defs.mjs +76 -0
  44. package/src/dia/attributes/eval.mjs +64 -0
  45. package/src/dia/attributes/index.mjs +69 -0
  46. package/src/dia/attributes/legacy.mjs +148 -0
  47. package/src/dia/attributes/offset.mjs +53 -0
  48. package/src/dia/attributes/props.mjs +30 -0
  49. package/src/dia/attributes/shape.mjs +92 -0
  50. package/src/dia/attributes/text.mjs +180 -0
  51. package/src/dia/index.mjs +13 -0
  52. package/src/dia/layers/GridLayer.mjs +176 -0
  53. package/src/dia/ports.mjs +874 -0
  54. package/src/elementTools/Control.mjs +153 -0
  55. package/src/elementTools/HoverConnect.mjs +37 -0
  56. package/src/elementTools/index.mjs +5 -0
  57. package/src/env/index.mjs +43 -0
  58. package/src/g/bezier.mjs +175 -0
  59. package/src/g/curve.mjs +956 -0
  60. package/src/g/ellipse.mjs +245 -0
  61. package/src/g/extend.mjs +64 -0
  62. package/src/g/geometry.helpers.mjs +58 -0
  63. package/src/g/index.mjs +17 -0
  64. package/src/g/intersection.mjs +511 -0
  65. package/src/g/line.bearing.mjs +30 -0
  66. package/src/g/line.length.mjs +5 -0
  67. package/src/g/line.mjs +356 -0
  68. package/src/g/line.squaredLength.mjs +10 -0
  69. package/src/g/path.mjs +2260 -0
  70. package/src/g/point.mjs +375 -0
  71. package/src/g/points.mjs +247 -0
  72. package/src/g/polygon.mjs +51 -0
  73. package/src/g/polyline.mjs +523 -0
  74. package/src/g/rect.mjs +556 -0
  75. package/src/g/types.mjs +10 -0
  76. package/src/highlighters/addClass.mjs +27 -0
  77. package/src/highlighters/index.mjs +5 -0
  78. package/src/highlighters/list.mjs +111 -0
  79. package/src/highlighters/mask.mjs +220 -0
  80. package/src/highlighters/opacity.mjs +17 -0
  81. package/src/highlighters/stroke.mjs +100 -0
  82. package/src/layout/index.mjs +4 -0
  83. package/src/layout/ports/port.mjs +188 -0
  84. package/src/layout/ports/portLabel.mjs +224 -0
  85. package/src/linkAnchors/index.mjs +76 -0
  86. package/src/linkTools/Anchor.mjs +235 -0
  87. package/src/linkTools/Arrowhead.mjs +103 -0
  88. package/src/linkTools/Boundary.mjs +48 -0
  89. package/src/linkTools/Button.mjs +121 -0
  90. package/src/linkTools/Connect.mjs +85 -0
  91. package/src/linkTools/HoverConnect.mjs +161 -0
  92. package/src/linkTools/Segments.mjs +393 -0
  93. package/src/linkTools/Vertices.mjs +253 -0
  94. package/src/linkTools/helpers.mjs +33 -0
  95. package/src/linkTools/index.mjs +8 -0
  96. package/src/mvc/Collection.mjs +560 -0
  97. package/src/mvc/Data.mjs +46 -0
  98. package/src/mvc/Dom/Dom.mjs +587 -0
  99. package/src/mvc/Dom/Event.mjs +130 -0
  100. package/src/mvc/Dom/animations.mjs +122 -0
  101. package/src/mvc/Dom/events.mjs +69 -0
  102. package/src/mvc/Dom/index.mjs +13 -0
  103. package/src/mvc/Dom/methods.mjs +392 -0
  104. package/src/mvc/Dom/props.mjs +77 -0
  105. package/src/mvc/Dom/vars.mjs +5 -0
  106. package/src/mvc/Events.mjs +337 -0
  107. package/src/mvc/Listener.mjs +33 -0
  108. package/src/mvc/Model.mjs +239 -0
  109. package/src/mvc/View.mjs +323 -0
  110. package/src/mvc/ViewBase.mjs +182 -0
  111. package/src/mvc/index.mjs +9 -0
  112. package/src/mvc/mvcUtils.mjs +90 -0
  113. package/src/polyfills/array.js +4 -0
  114. package/src/polyfills/base64.js +68 -0
  115. package/src/polyfills/index.mjs +5 -0
  116. package/src/polyfills/number.js +3 -0
  117. package/src/polyfills/string.js +3 -0
  118. package/src/polyfills/typedArray.js +47 -0
  119. package/src/routers/index.mjs +6 -0
  120. package/src/routers/manhattan.mjs +856 -0
  121. package/src/routers/metro.mjs +91 -0
  122. package/src/routers/normal.mjs +6 -0
  123. package/src/routers/oneSide.mjs +60 -0
  124. package/src/routers/orthogonal.mjs +323 -0
  125. package/src/routers/rightAngle.mjs +1056 -0
  126. package/src/shapes/index.mjs +3 -0
  127. package/src/shapes/standard.mjs +755 -0
  128. package/src/util/cloneCells.mjs +67 -0
  129. package/src/util/getRectPoint.mjs +65 -0
  130. package/src/util/index.mjs +5 -0
  131. package/src/util/svgTagTemplate.mjs +110 -0
  132. package/src/util/util.mjs +1754 -0
  133. package/src/util/utilHelpers.mjs +2402 -0
  134. package/src/util/wrappers.mjs +56 -0
  135. package/types/geometry.d.ts +815 -0
  136. package/types/index.d.ts +53 -0
  137. package/types/joint.d.ts +4391 -0
  138. package/types/joint.head.d.ts +12 -0
  139. package/types/vectorizer.d.ts +327 -0
@@ -0,0 +1,2207 @@
1
+ import { CellView } from './CellView.mjs';
2
+ import { Link } from './Link.mjs';
3
+ import V from '../V/index.mjs';
4
+ import { addClassNamePrefix, merge, assign, isObject, isFunction, clone, isPercentage, result, isEqual } from '../util/index.mjs';
5
+ import { Point, Line, Path, normalizeAngle, Rect, Polyline } from '../g/index.mjs';
6
+ import * as routers from '../routers/index.mjs';
7
+ import * as connectors from '../connectors/index.mjs';
8
+
9
+
10
+ const Flags = {
11
+ TOOLS: CellView.Flags.TOOLS,
12
+ RENDER: 'RENDER',
13
+ UPDATE: 'UPDATE',
14
+ LABELS: 'LABELS',
15
+ SOURCE: 'SOURCE',
16
+ TARGET: 'TARGET',
17
+ CONNECTOR: 'CONNECTOR'
18
+ };
19
+
20
+ // Link base view and controller.
21
+ // ----------------------------------------
22
+
23
+ export const LinkView = CellView.extend({
24
+
25
+ className: function() {
26
+
27
+ var classNames = CellView.prototype.className.apply(this).split(' ');
28
+
29
+ classNames.push('link');
30
+
31
+ return classNames.join(' ');
32
+ },
33
+
34
+ _labelCache: null,
35
+ _labelSelectors: null,
36
+ _V: null,
37
+ _dragData: null, // deprecated
38
+
39
+ metrics: null,
40
+ decimalsRounding: 2,
41
+
42
+ initialize: function() {
43
+
44
+ CellView.prototype.initialize.apply(this, arguments);
45
+
46
+ // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to
47
+ // `<g class="label">` nodes wrapped by Vectorizer. This allows for quick access to the
48
+ // nodes in `updateLabelPosition()` in order to update the label positions.
49
+ this._labelCache = {};
50
+
51
+ // a cache of label selectors
52
+ this._labelSelectors = {};
53
+
54
+ // cache of default markup nodes
55
+ this._V = {};
56
+
57
+ // connection path metrics
58
+ this.cleanNodesCache();
59
+ },
60
+
61
+ presentationAttributes: {
62
+ markup: [Flags.RENDER],
63
+ attrs: [Flags.UPDATE],
64
+ router: [Flags.UPDATE],
65
+ connector: [Flags.CONNECTOR],
66
+ labels: [Flags.LABELS],
67
+ labelMarkup: [Flags.LABELS],
68
+ vertices: [Flags.UPDATE],
69
+ source: [Flags.SOURCE, Flags.UPDATE],
70
+ target: [Flags.TARGET, Flags.UPDATE]
71
+ },
72
+
73
+ initFlag: [Flags.RENDER, Flags.SOURCE, Flags.TARGET, Flags.TOOLS],
74
+
75
+ UPDATE_PRIORITY: 1,
76
+
77
+ confirmUpdate: function(flags, opt) {
78
+
79
+ opt || (opt = {});
80
+
81
+ if (this.hasFlag(flags, Flags.SOURCE)) {
82
+ if (!this.updateEndProperties('source')) return flags;
83
+ flags = this.removeFlag(flags, Flags.SOURCE);
84
+ }
85
+
86
+ if (this.hasFlag(flags, Flags.TARGET)) {
87
+ if (!this.updateEndProperties('target')) return flags;
88
+ flags = this.removeFlag(flags, Flags.TARGET);
89
+ }
90
+
91
+ const { paper, sourceView, targetView } = this;
92
+ if (paper && ((sourceView && !paper.isViewMounted(sourceView)) || (targetView && !paper.isViewMounted(targetView)))) {
93
+ // Wait for the sourceView and targetView to be rendered
94
+ return flags;
95
+ }
96
+
97
+ if (this.hasFlag(flags, Flags.RENDER)) {
98
+ this.render();
99
+ this.updateHighlighters(true);
100
+ this.updateTools(opt);
101
+ flags = this.removeFlag(flags, [Flags.RENDER, Flags.UPDATE, Flags.LABELS, Flags.TOOLS, Flags.CONNECTOR]);
102
+ return flags;
103
+ }
104
+
105
+ let updateHighlighters = false;
106
+
107
+ const { model } = this;
108
+ const { attributes } = model;
109
+ let updateLabels = this.hasFlag(flags, Flags.LABELS);
110
+
111
+ if (updateLabels) {
112
+ this.onLabelsChange(model, attributes.labels, opt);
113
+ flags = this.removeFlag(flags, Flags.LABELS);
114
+ updateHighlighters = true;
115
+ }
116
+
117
+ const updateAll = this.hasFlag(flags, Flags.UPDATE);
118
+ const updateConnector = this.hasFlag(flags, Flags.CONNECTOR);
119
+ if (updateAll || updateConnector) {
120
+ if (!updateAll) {
121
+ // Keep the current route and update the geometry
122
+ this.updatePath();
123
+ this.updateDOM();
124
+ } else if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) {
125
+ // The link is being translated by an ancestor that will
126
+ // shift source point, target point and all vertices
127
+ // by an equal distance.
128
+ this.translate(opt.tx, opt.ty);
129
+ } else {
130
+ this.update();
131
+ }
132
+ this.updateTools(opt);
133
+ flags = this.removeFlag(flags, [Flags.UPDATE, Flags.TOOLS, Flags.CONNECTOR]);
134
+ updateLabels = false;
135
+ updateHighlighters = true;
136
+ }
137
+
138
+ if (updateLabels) {
139
+ this.updateLabelPositions();
140
+ }
141
+
142
+ if (updateHighlighters) {
143
+ this.updateHighlighters();
144
+ }
145
+
146
+ if (this.hasFlag(flags, Flags.TOOLS)) {
147
+ this.updateTools(opt);
148
+ flags = this.removeFlag(flags, Flags.TOOLS);
149
+ }
150
+
151
+ return flags;
152
+ },
153
+
154
+ requestConnectionUpdate: function(opt) {
155
+ this.requestUpdate(this.getFlag(Flags.UPDATE), opt);
156
+ },
157
+
158
+ isLabelsRenderRequired: function(opt = {}) {
159
+
160
+ const previousLabels = this.model.previous('labels');
161
+ if (!previousLabels) return true;
162
+
163
+ // Here is an optimization for cases when we know, that change does
164
+ // not require re-rendering of all labels.
165
+ if (('propertyPathArray' in opt) && ('propertyValue' in opt)) {
166
+ // The label is setting by `prop()` method
167
+ var pathArray = opt.propertyPathArray || [];
168
+ var pathLength = pathArray.length;
169
+ if (pathLength > 1) {
170
+ // We are changing a single label here e.g. 'labels/0/position'
171
+ var labelExists = !!previousLabels[pathArray[1]];
172
+ if (labelExists) {
173
+ if (pathLength === 2) {
174
+ // We are changing the entire label. Need to check if the
175
+ // markup is also being changed.
176
+ return ('markup' in Object(opt.propertyValue));
177
+ } else if (pathArray[2] !== 'markup') {
178
+ // We are changing a label property but not the markup
179
+ return false;
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ return true;
186
+ },
187
+
188
+ onLabelsChange: function(_link, _labels, opt) {
189
+
190
+ // Note: this optimization works in async=false mode only
191
+ if (this.isLabelsRenderRequired(opt)) {
192
+ this.renderLabels();
193
+ } else {
194
+ this.updateLabels();
195
+ }
196
+ },
197
+
198
+ // Rendering.
199
+ // ----------
200
+
201
+ render: function() {
202
+
203
+ this.vel.empty();
204
+ this.unmountLabels();
205
+ this._V = {};
206
+ this.renderMarkup();
207
+ // rendering labels has to be run after the link is appended to DOM tree. (otherwise <Text> bbox
208
+ // returns zero values)
209
+ this.renderLabels();
210
+ this.update();
211
+
212
+ return this;
213
+ },
214
+
215
+ renderMarkup: function() {
216
+
217
+ var link = this.model;
218
+ var markup = link.get('markup') || link.markup;
219
+ if (!markup) throw new Error('dia.LinkView: markup required');
220
+ if (Array.isArray(markup)) return this.renderJSONMarkup(markup);
221
+ if (typeof markup === 'string') return this.renderStringMarkup(markup);
222
+ throw new Error('dia.LinkView: invalid markup');
223
+ },
224
+
225
+ renderJSONMarkup: function(markup) {
226
+
227
+ var doc = this.parseDOMJSON(markup, this.el);
228
+ // Selectors
229
+ this.selectors = doc.selectors;
230
+ // Fragment
231
+ this.vel.append(doc.fragment);
232
+ },
233
+
234
+ renderStringMarkup: function(markup) {
235
+
236
+ // A special markup can be given in the `properties.markup` property. This might be handy
237
+ // if e.g. arrowhead markers should be `<image>` elements or any other element than `<path>`s.
238
+ // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors
239
+ // of elements with special meaning though. Therefore, those classes should be preserved in any
240
+ // special markup passed in `properties.markup`.
241
+ var children = V(markup);
242
+ // custom markup may contain only one children
243
+ if (!Array.isArray(children)) children = [children];
244
+
245
+ this.vel.append(children);
246
+ },
247
+
248
+ _getLabelMarkup: function(labelMarkup) {
249
+
250
+ if (!labelMarkup) return undefined;
251
+
252
+ if (Array.isArray(labelMarkup)) return this.parseDOMJSON(labelMarkup, null);
253
+ if (typeof labelMarkup === 'string') return this._getLabelStringMarkup(labelMarkup);
254
+ throw new Error('dia.linkView: invalid label markup');
255
+ },
256
+
257
+ _getLabelStringMarkup: function(labelMarkup) {
258
+
259
+ var children = V(labelMarkup);
260
+ var fragment = document.createDocumentFragment();
261
+
262
+ if (!Array.isArray(children)) {
263
+ fragment.appendChild(children.node);
264
+
265
+ } else {
266
+ for (var i = 0, n = children.length; i < n; i++) {
267
+ var currentChild = children[i].node;
268
+ fragment.appendChild(currentChild);
269
+ }
270
+ }
271
+
272
+ return { fragment: fragment, selectors: {}}; // no selectors
273
+ },
274
+
275
+ // Label markup fragment may come wrapped in <g class="label" />, or not.
276
+ // If it doesn't, add the <g /> container here.
277
+ _normalizeLabelMarkup: function(markup) {
278
+
279
+ if (!markup) return undefined;
280
+
281
+ var fragment = markup.fragment;
282
+ if (!(markup.fragment instanceof DocumentFragment) || !markup.fragment.hasChildNodes()) throw new Error('dia.LinkView: invalid label markup.');
283
+
284
+ var vNode;
285
+ var childNodes = fragment.childNodes;
286
+
287
+ if ((childNodes.length > 1) || childNodes[0].nodeName.toUpperCase() !== 'G') {
288
+ // default markup fragment is not wrapped in <g />
289
+ // add a <g /> container
290
+ vNode = V('g').append(fragment);
291
+ } else {
292
+ vNode = V(childNodes[0]);
293
+ }
294
+
295
+ vNode.addClass('label');
296
+
297
+ return { node: vNode.node, selectors: markup.selectors };
298
+ },
299
+
300
+ renderLabels: function() {
301
+
302
+ var cache = this._V;
303
+ var vLabels = cache.labels;
304
+ var labelCache = this._labelCache = {};
305
+ var labelSelectors = this._labelSelectors = {};
306
+ var model = this.model;
307
+ var labels = model.attributes.labels || [];
308
+ var labelsCount = labels.length;
309
+
310
+ if (labelsCount === 0) {
311
+ if (vLabels) vLabels.remove();
312
+ return this;
313
+ }
314
+
315
+ if (vLabels) {
316
+ vLabels.empty();
317
+ } else {
318
+ // there is no label container in the markup but some labels are defined
319
+ // add a <g class="labels" /> container
320
+ vLabels = cache.labels = V('g').addClass('labels');
321
+ if (this.options.labelsLayer) {
322
+ vLabels.addClass(addClassNamePrefix(result(this, 'className')));
323
+ vLabels.attr('model-id', model.id);
324
+ }
325
+ }
326
+
327
+ for (var i = 0; i < labelsCount; i++) {
328
+
329
+ var label = labels[i];
330
+ var labelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(label.markup));
331
+ var labelNode;
332
+ var selectors;
333
+ if (labelMarkup) {
334
+
335
+ labelNode = labelMarkup.node;
336
+ selectors = labelMarkup.selectors;
337
+
338
+ } else {
339
+
340
+ var builtinDefaultLabel = model._builtins.defaultLabel;
341
+ var builtinDefaultLabelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(builtinDefaultLabel.markup));
342
+ var defaultLabel = model._getDefaultLabel();
343
+ var defaultLabelMarkup = this._normalizeLabelMarkup(this._getLabelMarkup(defaultLabel.markup));
344
+ var defaultMarkup = defaultLabelMarkup || builtinDefaultLabelMarkup;
345
+
346
+ labelNode = defaultMarkup.node;
347
+ selectors = defaultMarkup.selectors;
348
+ }
349
+
350
+ labelNode.setAttribute('label-idx', i); // assign label-idx
351
+ vLabels.append(labelNode);
352
+ labelCache[i] = labelNode; // cache node for `updateLabels()` so it can just update label node positions
353
+
354
+ var rootSelector = this.selector;
355
+ if (selectors[rootSelector]) throw new Error('dia.LinkView: ambiguous label root selector.');
356
+ selectors[rootSelector] = labelNode;
357
+
358
+ labelSelectors[i] = selectors; // cache label selectors for `updateLabels()`
359
+ }
360
+ if (!vLabels.parent()) {
361
+ this.mountLabels();
362
+ }
363
+
364
+ this.updateLabels();
365
+
366
+ return this;
367
+ },
368
+
369
+ mountLabels: function() {
370
+ const { el, paper, model, _V, options } = this;
371
+ const { labels: vLabels } = _V;
372
+ if (!vLabels || !model.hasLabels()) return;
373
+ const { node } = vLabels;
374
+ if (options.labelsLayer) {
375
+ paper.getLayerView(options.labelsLayer).insertSortedNode(node, model.get('z'));
376
+ } else {
377
+ if (node.parentNode !== el) {
378
+ el.appendChild(node);
379
+ }
380
+ }
381
+ },
382
+
383
+ unmountLabels: function() {
384
+ const { options, _V } = this;
385
+ if (!_V) return;
386
+ const { labels: vLabels } = _V;
387
+ if (vLabels && options.labelsLayer) {
388
+ vLabels.remove();
389
+ }
390
+ },
391
+
392
+ findLabelNodes: function(labelIndex, selector) {
393
+ const labelRoot = this._labelCache[labelIndex];
394
+ if (!labelRoot) return [];
395
+ const labelSelectors = this._labelSelectors[labelIndex];
396
+ return this.findBySelector(selector, labelRoot, labelSelectors);
397
+ },
398
+
399
+ findLabelNode: function(labelIndex, selector) {
400
+ const [node = null] = this.findLabelNodes(labelIndex, selector);
401
+ return node;
402
+ },
403
+
404
+ // merge default label attrs into label attrs (or use built-in default label attrs if neither is provided)
405
+ // keep `undefined` or `null` because `{}` means something else
406
+ _mergeLabelAttrs: function(hasCustomMarkup, labelAttrs, defaultLabelAttrs, builtinDefaultLabelAttrs) {
407
+
408
+ if (labelAttrs === null) return null;
409
+ if (labelAttrs === undefined) {
410
+
411
+ if (defaultLabelAttrs === null) return null;
412
+ if (defaultLabelAttrs === undefined) {
413
+
414
+ if (hasCustomMarkup) return undefined;
415
+ return builtinDefaultLabelAttrs;
416
+ }
417
+
418
+ if (hasCustomMarkup) return defaultLabelAttrs;
419
+ return merge({}, builtinDefaultLabelAttrs, defaultLabelAttrs);
420
+ }
421
+
422
+ if (hasCustomMarkup) return merge({}, defaultLabelAttrs, labelAttrs);
423
+ return merge({}, builtinDefaultLabelAttrs, defaultLabelAttrs, labelAttrs);
424
+ },
425
+
426
+ // merge default label size into label size (no built-in default)
427
+ // keep `undefined` or `null` because `{}` means something else
428
+ _mergeLabelSize: function(labelSize, defaultLabelSize) {
429
+
430
+ if (labelSize === null) return null;
431
+ if (labelSize === undefined) {
432
+
433
+ if (defaultLabelSize === null) return null;
434
+ if (defaultLabelSize === undefined) return undefined;
435
+
436
+ return defaultLabelSize;
437
+ }
438
+
439
+ return merge({}, defaultLabelSize, labelSize);
440
+ },
441
+
442
+ updateLabels: function() {
443
+
444
+ if (!this._V.labels) return this;
445
+
446
+ var model = this.model;
447
+ var labels = model.get('labels') || [];
448
+ var canLabelMove = this.can('labelMove');
449
+
450
+ var builtinDefaultLabel = model._builtins.defaultLabel;
451
+ var builtinDefaultLabelAttrs = builtinDefaultLabel.attrs;
452
+
453
+ var defaultLabel = model._getDefaultLabel();
454
+ var defaultLabelMarkup = defaultLabel.markup;
455
+ var defaultLabelAttrs = defaultLabel.attrs;
456
+ var defaultLabelSize = defaultLabel.size;
457
+
458
+ for (var i = 0, n = labels.length; i < n; i++) {
459
+
460
+ var labelNode = this._labelCache[i];
461
+ labelNode.setAttribute('cursor', (canLabelMove ? 'move' : 'default'));
462
+
463
+ var selectors = this._labelSelectors[i];
464
+
465
+ var label = labels[i];
466
+ var labelMarkup = label.markup;
467
+ var labelAttrs = label.attrs;
468
+ var labelSize = label.size;
469
+
470
+ var attrs = this._mergeLabelAttrs(
471
+ (labelMarkup || defaultLabelMarkup),
472
+ labelAttrs,
473
+ defaultLabelAttrs,
474
+ builtinDefaultLabelAttrs
475
+ );
476
+
477
+ var size = this._mergeLabelSize(
478
+ labelSize,
479
+ defaultLabelSize
480
+ );
481
+
482
+ this.updateDOMSubtreeAttributes(labelNode, attrs, {
483
+ rootBBox: new Rect(size),
484
+ selectors: selectors
485
+ });
486
+ }
487
+
488
+ return this;
489
+ },
490
+
491
+ // remove vertices that lie on (or nearly on) straight lines within the link
492
+ // return the number of removed points
493
+ removeRedundantLinearVertices: function(opt) {
494
+
495
+ const SIMPLIFY_THRESHOLD = 0.001;
496
+
497
+ const link = this.model;
498
+ const vertices = link.vertices();
499
+ const routePoints = [this.sourceAnchor, ...vertices, this.targetAnchor];
500
+ const numRoutePoints = routePoints.length;
501
+
502
+ // put routePoints into a polyline and try to simplify
503
+ const polyline = new Polyline(routePoints);
504
+ polyline.simplify({ threshold: SIMPLIFY_THRESHOLD });
505
+ const polylinePoints = polyline.points.map((point) => (point.toJSON())); // JSON of points after simplification
506
+ const numPolylinePoints = polylinePoints.length; // number of points after simplification
507
+
508
+ // shortcut if simplification did not remove any redundant vertices:
509
+ if (numRoutePoints === numPolylinePoints) return 0;
510
+
511
+ // else: set simplified polyline points as link vertices
512
+ // remove first and last polyline points again (= source/target anchors)
513
+ link.vertices(polylinePoints.slice(1, numPolylinePoints - 1), opt);
514
+ return (numRoutePoints - numPolylinePoints);
515
+ },
516
+
517
+ getEndView: function(type) {
518
+ switch (type) {
519
+ case 'source':
520
+ return this.sourceView || null;
521
+ case 'target':
522
+ return this.targetView || null;
523
+ default:
524
+ throw new Error('dia.LinkView: type parameter required.');
525
+ }
526
+ },
527
+
528
+ getEndAnchor: function(type) {
529
+ switch (type) {
530
+ case 'source':
531
+ return new Point(this.sourceAnchor);
532
+ case 'target':
533
+ return new Point(this.targetAnchor);
534
+ default:
535
+ throw new Error('dia.LinkView: type parameter required.');
536
+ }
537
+ },
538
+
539
+ getEndConnectionPoint: function(type) {
540
+ switch (type) {
541
+ case 'source':
542
+ return new Point(this.sourcePoint);
543
+ case 'target':
544
+ return new Point(this.targetPoint);
545
+ default:
546
+ throw new Error('dia.LinkView: type parameter required.');
547
+ }
548
+ },
549
+
550
+ getEndMagnet: function(type) {
551
+ switch (type) {
552
+ case 'source':
553
+ var sourceView = this.sourceView;
554
+ if (!sourceView) break;
555
+ return this.sourceMagnet || sourceView.el;
556
+ case 'target':
557
+ var targetView = this.targetView;
558
+ if (!targetView) break;
559
+ return this.targetMagnet || targetView.el;
560
+ default:
561
+ throw new Error('dia.LinkView: type parameter required.');
562
+ }
563
+ return null;
564
+ },
565
+
566
+
567
+ // Updating.
568
+ // ---------
569
+
570
+ update: function() {
571
+ this.updateRoute();
572
+ this.updatePath();
573
+ this.updateDOM();
574
+ return this;
575
+ },
576
+
577
+ translate: function(tx = 0, ty = 0) {
578
+ const { route, path } = this;
579
+ if (!route || !path) return;
580
+ // translate the route
581
+ const polyline = new Polyline(route);
582
+ polyline.translate(tx, ty);
583
+ this.route = polyline.points;
584
+ // translate source and target connection and anchor points.
585
+ this.sourcePoint.offset(tx, ty);
586
+ this.targetPoint.offset(tx, ty);
587
+ this.sourceAnchor.offset(tx, ty);
588
+ this.targetAnchor.offset(tx, ty);
589
+ // translate the geometry path
590
+ path.translate(tx, ty);
591
+ this.updateDOM();
592
+ },
593
+
594
+ updateDOM() {
595
+ const { el, model, selectors } = this;
596
+ this.cleanNodesCache();
597
+ // update SVG attributes defined by 'attrs/'.
598
+ this.updateDOMSubtreeAttributes(el, model.attr(), { selectors });
599
+ // update the label position etc.
600
+ this.updateLabelPositions();
601
+ // *Deprecated*
602
+ // Local perpendicular flag (as opposed to one defined on paper).
603
+ // Could be enabled inside a connector/router. It's valid only
604
+ // during the update execution.
605
+ this.options.perpendicular = null;
606
+ },
607
+
608
+ updateRoute: function() {
609
+ const { model } = this;
610
+ const vertices = model.vertices();
611
+ // 1. Find Anchors
612
+ const anchors = this.findAnchors(vertices);
613
+ const sourceAnchor = this.sourceAnchor = anchors.source;
614
+ const targetAnchor = this.targetAnchor = anchors.target;
615
+ // 2. Find Route
616
+ const route = this.findRoute(vertices);
617
+ this.route = route;
618
+ // 3. Find Connection Points
619
+ var connectionPoints = this.findConnectionPoints(route, sourceAnchor, targetAnchor);
620
+ this.sourcePoint = connectionPoints.source;
621
+ this.targetPoint = connectionPoints.target;
622
+ },
623
+
624
+ updatePath: function() {
625
+ const { route, sourcePoint, targetPoint } = this;
626
+ // 4. Find Connection
627
+ const path = this.findPath(route, sourcePoint.clone(), targetPoint.clone());
628
+ this.path = path;
629
+ },
630
+
631
+ findAnchorsOrdered: function(firstEndType, firstRef, secondEndType, secondRef) {
632
+
633
+ var firstAnchor, secondAnchor;
634
+ var firstAnchorRef, secondAnchorRef;
635
+ var model = this.model;
636
+ var firstDef = model.get(firstEndType);
637
+ var secondDef = model.get(secondEndType);
638
+ var firstView = this.getEndView(firstEndType);
639
+ var secondView = this.getEndView(secondEndType);
640
+ var firstMagnet = this.getEndMagnet(firstEndType);
641
+ var secondMagnet = this.getEndMagnet(secondEndType);
642
+
643
+ // Anchor first
644
+ if (firstView) {
645
+ if (firstRef) {
646
+ firstAnchorRef = new Point(firstRef);
647
+ } else if (secondView) {
648
+ firstAnchorRef = secondMagnet;
649
+ } else {
650
+ firstAnchorRef = new Point(secondDef);
651
+ }
652
+ firstAnchor = this.getAnchor(firstDef.anchor, firstView, firstMagnet, firstAnchorRef, firstEndType);
653
+ } else {
654
+ firstAnchor = new Point(firstDef);
655
+ }
656
+
657
+ // Anchor second
658
+ if (secondView) {
659
+ secondAnchorRef = new Point(secondRef || firstAnchor);
660
+ secondAnchor = this.getAnchor(secondDef.anchor, secondView, secondMagnet, secondAnchorRef, secondEndType);
661
+ } else {
662
+ secondAnchor = new Point(secondDef);
663
+ }
664
+
665
+ var res = {};
666
+ res[firstEndType] = firstAnchor;
667
+ res[secondEndType] = secondAnchor;
668
+ return res;
669
+ },
670
+
671
+ findAnchors: function(vertices) {
672
+
673
+ var model = this.model;
674
+ var firstVertex = vertices[0];
675
+ var lastVertex = vertices[vertices.length - 1];
676
+
677
+ if (model.target().priority && !model.source().priority) {
678
+ // Reversed order
679
+ return this.findAnchorsOrdered('target', lastVertex, 'source', firstVertex);
680
+ }
681
+
682
+ // Usual order
683
+ return this.findAnchorsOrdered('source', firstVertex, 'target', lastVertex);
684
+ },
685
+
686
+ findConnectionPoints: function(route, sourceAnchor, targetAnchor) {
687
+
688
+ var firstWaypoint = route[0];
689
+ var lastWaypoint = route[route.length - 1];
690
+ var model = this.model;
691
+ var sourceDef = model.get('source');
692
+ var targetDef = model.get('target');
693
+ var sourceView = this.sourceView;
694
+ var targetView = this.targetView;
695
+ var paperOptions = this.paper.options;
696
+ var sourceMagnet, targetMagnet;
697
+
698
+ // Connection Point Source
699
+ var sourcePoint;
700
+ if (sourceView && !sourceView.isNodeConnection(this.sourceMagnet)) {
701
+ sourceMagnet = (this.sourceMagnet || sourceView.el);
702
+ var sourceConnectionPointDef = sourceDef.connectionPoint || paperOptions.defaultConnectionPoint;
703
+ var sourcePointRef = firstWaypoint || targetAnchor;
704
+ var sourceLine = new Line(sourcePointRef, sourceAnchor);
705
+ sourcePoint = this.getConnectionPoint(
706
+ sourceConnectionPointDef,
707
+ sourceView,
708
+ sourceMagnet,
709
+ sourceLine,
710
+ 'source'
711
+ );
712
+ } else {
713
+ sourcePoint = sourceAnchor;
714
+ }
715
+ // Connection Point Target
716
+ var targetPoint;
717
+ if (targetView && !targetView.isNodeConnection(this.targetMagnet)) {
718
+ targetMagnet = (this.targetMagnet || targetView.el);
719
+ var targetConnectionPointDef = targetDef.connectionPoint || paperOptions.defaultConnectionPoint;
720
+ var targetPointRef = lastWaypoint || sourceAnchor;
721
+ var targetLine = new Line(targetPointRef, targetAnchor);
722
+ targetPoint = this.getConnectionPoint(
723
+ targetConnectionPointDef,
724
+ targetView,
725
+ targetMagnet,
726
+ targetLine,
727
+ 'target'
728
+ );
729
+ } else {
730
+ targetPoint = targetAnchor;
731
+ }
732
+
733
+ return {
734
+ source: sourcePoint,
735
+ target: targetPoint
736
+ };
737
+ },
738
+
739
+ getAnchor: function(anchorDef, cellView, magnet, ref, endType) {
740
+
741
+ var isConnection = cellView.isNodeConnection(magnet);
742
+ var paperOptions = this.paper.options;
743
+ if (!anchorDef) {
744
+ if (isConnection) {
745
+ anchorDef = paperOptions.defaultLinkAnchor;
746
+ } else {
747
+ if (this.options.perpendicular) {
748
+ // Backwards compatibility
749
+ // See `manhattan` router for more details
750
+ anchorDef = { name: 'perpendicular' };
751
+ } else {
752
+ anchorDef = paperOptions.defaultAnchor;
753
+ }
754
+ }
755
+ }
756
+
757
+ if (!anchorDef) throw new Error('Anchor required.');
758
+ var anchorFn;
759
+ if (typeof anchorDef === 'function') {
760
+ anchorFn = anchorDef;
761
+ } else {
762
+ var anchorName = anchorDef.name;
763
+ var anchorNamespace = isConnection ? 'linkAnchorNamespace' : 'anchorNamespace';
764
+ anchorFn = paperOptions[anchorNamespace][anchorName];
765
+ if (typeof anchorFn !== 'function') throw new Error('Unknown anchor: ' + anchorName);
766
+ }
767
+ var anchor = anchorFn.call(
768
+ this,
769
+ cellView,
770
+ magnet,
771
+ ref,
772
+ anchorDef.args || {},
773
+ endType,
774
+ this
775
+ );
776
+ if (!anchor) return new Point();
777
+ return anchor.round(this.decimalsRounding);
778
+ },
779
+
780
+
781
+ getConnectionPoint: function(connectionPointDef, view, magnet, line, endType) {
782
+
783
+ var connectionPoint;
784
+ var anchor = line.end;
785
+ var paperOptions = this.paper.options;
786
+
787
+ if (!connectionPointDef) return anchor;
788
+ var connectionPointFn;
789
+ if (typeof connectionPointDef === 'function') {
790
+ connectionPointFn = connectionPointDef;
791
+ } else {
792
+ var connectionPointName = connectionPointDef.name;
793
+ connectionPointFn = paperOptions.connectionPointNamespace[connectionPointName];
794
+ if (typeof connectionPointFn !== 'function') throw new Error('Unknown connection point: ' + connectionPointName);
795
+ }
796
+ connectionPoint = connectionPointFn.call(this, line, view, magnet, connectionPointDef.args || {}, endType, this);
797
+ if (!connectionPoint) return anchor;
798
+ return connectionPoint.round(this.decimalsRounding);
799
+ },
800
+
801
+ // combine default label position with built-in default label position
802
+ _getDefaultLabelPositionProperty: function() {
803
+
804
+ var model = this.model;
805
+
806
+ var builtinDefaultLabel = model._builtins.defaultLabel;
807
+ var builtinDefaultLabelPosition = builtinDefaultLabel.position;
808
+
809
+ var defaultLabel = model._getDefaultLabel();
810
+ var defaultLabelPosition = this._normalizeLabelPosition(defaultLabel.position);
811
+
812
+ return merge({}, builtinDefaultLabelPosition, defaultLabelPosition);
813
+ },
814
+
815
+ // if label position is a number, normalize it to a position object
816
+ // this makes sure that label positions can be merged properly
817
+ _normalizeLabelPosition: function(labelPosition) {
818
+
819
+ if (typeof labelPosition === 'number') return { distance: labelPosition, offset: null, angle: 0, args: null };
820
+ return labelPosition;
821
+ },
822
+
823
+ // expects normalized position properties
824
+ // e.g. `this._normalizeLabelPosition(labelPosition)` and `this._getDefaultLabelPositionProperty()`
825
+ _mergeLabelPositionProperty: function(normalizedLabelPosition, normalizedDefaultLabelPosition) {
826
+
827
+ if (normalizedLabelPosition === null) return null;
828
+ if (normalizedLabelPosition === undefined) {
829
+
830
+ if (normalizedDefaultLabelPosition === null) return null;
831
+ return normalizedDefaultLabelPosition;
832
+ }
833
+
834
+ return merge({}, normalizedDefaultLabelPosition, normalizedLabelPosition);
835
+ },
836
+
837
+ updateLabelPositions: function() {
838
+
839
+ if (!this._V.labels) return this;
840
+
841
+ var path = this.path;
842
+ if (!path) return this;
843
+
844
+ // This method assumes all the label nodes are stored in the `this._labelCache` hash table
845
+ // by their indices in the `this.get('labels')` array. This is done in the `renderLabels()` method.
846
+
847
+ var model = this.model;
848
+ var labels = model.get('labels') || [];
849
+ if (!labels.length) return this;
850
+
851
+ var defaultLabelPosition = this._getDefaultLabelPositionProperty();
852
+
853
+ for (var idx = 0, n = labels.length; idx < n; idx++) {
854
+ var labelNode = this._labelCache[idx];
855
+ if (!labelNode) continue;
856
+ var label = labels[idx];
857
+ var labelPosition = this._normalizeLabelPosition(label.position);
858
+ var position = this._mergeLabelPositionProperty(labelPosition, defaultLabelPosition);
859
+ var transformationMatrix = this._getLabelTransformationMatrix(position);
860
+ labelNode.setAttribute('transform', V.matrixToTransformString(transformationMatrix));
861
+ this._cleanLabelMatrices(idx);
862
+ }
863
+
864
+ return this;
865
+ },
866
+
867
+ _cleanLabelMatrices: function(index) {
868
+ // Clean magnetMatrix for all nodes of the label.
869
+ // Cached BoundingRect does not need to updated when the position changes
870
+ // TODO: this doesn't work for labels with XML String markups.
871
+ const { metrics, _labelSelectors } = this;
872
+ const selectors = _labelSelectors[index];
873
+ if (!selectors) return;
874
+ for (let selector in selectors) {
875
+ const { id } = selectors[selector];
876
+ if (id && (id in metrics)) delete metrics[id].magnetMatrix;
877
+ }
878
+ },
879
+
880
+ updateEndProperties: function(endType) {
881
+
882
+ const { model, paper } = this;
883
+ const endViewProperty = `${endType}View`;
884
+ const endDef = model.get(endType);
885
+ const endId = endDef && endDef.id;
886
+
887
+ if (!endId) {
888
+ // the link end is a point ~ rect 0x0
889
+ this[endViewProperty] = null;
890
+ this.updateEndMagnet(endType);
891
+ return true;
892
+ }
893
+
894
+ const endModel = paper.getModelById(endId);
895
+ if (!endModel) throw new Error('LinkView: invalid ' + endType + ' cell.');
896
+
897
+ const endView = endModel.findView(paper);
898
+ if (!endView) {
899
+ // A view for a model should always exist
900
+ return false;
901
+ }
902
+
903
+ this[endViewProperty] = endView;
904
+ this.updateEndMagnet(endType);
905
+ return true;
906
+ },
907
+
908
+ updateEndMagnet: function(endType) {
909
+
910
+ const endMagnetProperty = `${endType}Magnet`;
911
+ const endView = this.getEndView(endType);
912
+ if (endView) {
913
+ let connectedMagnet = endView.getMagnetFromLinkEnd(this.model.get(endType));
914
+ if (connectedMagnet === endView.el) connectedMagnet = null;
915
+ this[endMagnetProperty] = connectedMagnet;
916
+ } else {
917
+ this[endMagnetProperty] = null;
918
+ }
919
+ },
920
+
921
+ _getLabelPositionProperty: function(idx) {
922
+
923
+ return (this.model.label(idx).position || {});
924
+ },
925
+
926
+ _getLabelPositionAngle: function(idx) {
927
+
928
+ var labelPosition = this._getLabelPositionProperty(idx);
929
+ return (labelPosition.angle || 0);
930
+ },
931
+
932
+ _getLabelPositionArgs: function(idx) {
933
+
934
+ var labelPosition = this._getLabelPositionProperty(idx);
935
+ return labelPosition.args;
936
+ },
937
+
938
+ _getDefaultLabelPositionArgs: function() {
939
+
940
+ var defaultLabel = this.model._getDefaultLabel();
941
+ var defaultLabelPosition = defaultLabel.position || {};
942
+ return defaultLabelPosition.args;
943
+ },
944
+
945
+ // merge default label position args into label position args
946
+ // keep `undefined` or `null` because `{}` means something else
947
+ _mergeLabelPositionArgs: function(labelPositionArgs, defaultLabelPositionArgs) {
948
+
949
+ if (labelPositionArgs === null) return null;
950
+ if (labelPositionArgs === undefined) {
951
+
952
+ if (defaultLabelPositionArgs === null) return null;
953
+ return defaultLabelPositionArgs;
954
+ }
955
+
956
+ return merge({}, defaultLabelPositionArgs, labelPositionArgs);
957
+ },
958
+
959
+ // Add default label at given position at end of `labels` array.
960
+ // Four signatures:
961
+ // - obj, obj = point, opt
962
+ // - obj, num, obj = point, angle, opt
963
+ // - num, num, obj = x, y, opt
964
+ // - num, num, num, obj = x, y, angle, opt
965
+ // Assigns relative coordinates by default:
966
+ // `opt.absoluteDistance` forces absolute coordinates.
967
+ // `opt.reverseDistance` forces reverse absolute coordinates (if absoluteDistance = true).
968
+ // `opt.absoluteOffset` forces absolute coordinates for offset.
969
+ // Additional args:
970
+ // `opt.keepGradient` auto-adjusts the angle of the label to match path gradient at position.
971
+ // `opt.ensureLegibility` rotates labels so they are never upside-down.
972
+ addLabel: function(p1, p2, p3, p4) {
973
+
974
+ // normalize data from the four possible signatures
975
+ var localX;
976
+ var localY;
977
+ var localAngle = 0;
978
+ var localOpt;
979
+ if (typeof p1 !== 'number') {
980
+ // {x, y} object provided as first parameter
981
+ localX = p1.x;
982
+ localY = p1.y;
983
+ if (typeof p2 === 'number') {
984
+ // angle and opt provided as second and third parameters
985
+ localAngle = p2;
986
+ localOpt = p3;
987
+ } else {
988
+ // opt provided as second parameter
989
+ localOpt = p2;
990
+ }
991
+ } else {
992
+ // x and y provided as first and second parameters
993
+ localX = p1;
994
+ localY = p2;
995
+ if (typeof p3 === 'number') {
996
+ // angle and opt provided as third and fourth parameters
997
+ localAngle = p3;
998
+ localOpt = p4;
999
+ } else {
1000
+ // opt provided as third parameter
1001
+ localOpt = p3;
1002
+ }
1003
+ }
1004
+
1005
+ // merge label position arguments
1006
+ var defaultLabelPositionArgs = this._getDefaultLabelPositionArgs();
1007
+ var labelPositionArgs = localOpt;
1008
+ var positionArgs = this._mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs);
1009
+
1010
+ // append label to labels array
1011
+ var label = { position: this.getLabelPosition(localX, localY, localAngle, positionArgs) };
1012
+ var idx = -1;
1013
+ this.model.insertLabel(idx, label, localOpt);
1014
+ return idx;
1015
+ },
1016
+
1017
+ // Add a new vertex at calculated index to the `vertices` array.
1018
+ addVertex: function(x, y, opt) {
1019
+
1020
+ // accept input in form `{ x, y }, opt` or `x, y, opt`
1021
+ var isPointProvided = (typeof x !== 'number');
1022
+ var localX = isPointProvided ? x.x : x;
1023
+ var localY = isPointProvided ? x.y : y;
1024
+ var localOpt = isPointProvided ? y : opt;
1025
+
1026
+ var vertex = { x: localX, y: localY };
1027
+ var idx = this.getVertexIndex(localX, localY);
1028
+ this.model.insertVertex(idx, vertex, localOpt);
1029
+ return idx;
1030
+ },
1031
+
1032
+ // Send a token (an SVG element, usually a circle) along the connection path.
1033
+ // Example: `link.findView(paper).sendToken(V('circle', { r: 7, fill: 'green' }).node)`
1034
+ // `opt.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`.
1035
+ // `opt.direction` is optional and it determines whether the token goes from source to target or other way round (`reverse`)
1036
+ // `opt.connection` is an optional selector to the connection path.
1037
+ // `callback` is optional and is a function to be called once the token reaches the target.
1038
+ sendToken: function(token, opt, callback) {
1039
+
1040
+ function onAnimationEnd(vToken, callback) {
1041
+ return function() {
1042
+ vToken.remove();
1043
+ if (typeof callback === 'function') {
1044
+ callback();
1045
+ }
1046
+ };
1047
+ }
1048
+
1049
+ var duration, isReversed, selector;
1050
+ if (isObject(opt)) {
1051
+ duration = opt.duration;
1052
+ isReversed = (opt.direction === 'reverse');
1053
+ selector = opt.connection;
1054
+ } else {
1055
+ // Backwards compatibility
1056
+ duration = opt;
1057
+ isReversed = false;
1058
+ selector = null;
1059
+ }
1060
+
1061
+ duration = duration || 1000;
1062
+
1063
+ var animationAttributes = {
1064
+ dur: duration + 'ms',
1065
+ repeatCount: 1,
1066
+ calcMode: 'linear',
1067
+ fill: 'freeze'
1068
+ };
1069
+
1070
+ if (isReversed) {
1071
+ animationAttributes.keyPoints = '1;0';
1072
+ animationAttributes.keyTimes = '0;1';
1073
+ }
1074
+
1075
+ var vToken = V(token);
1076
+ var connection;
1077
+ if (typeof selector === 'string') {
1078
+ // Use custom connection path.
1079
+ connection = this.findNode(selector);
1080
+ } else {
1081
+ // Select connection path automatically.
1082
+ var cache = this._V;
1083
+ connection = (cache.connection) ? cache.connection.node : this.el.querySelector('path');
1084
+ }
1085
+
1086
+ if (!(connection instanceof SVGPathElement)) {
1087
+ throw new Error('dia.LinkView: token animation requires a valid connection path.');
1088
+ }
1089
+
1090
+ vToken
1091
+ .appendTo(this.paper.cells)
1092
+ .animateAlongPath(animationAttributes, connection);
1093
+
1094
+ setTimeout(onAnimationEnd(vToken, callback), duration);
1095
+ },
1096
+
1097
+ findRoute: function(vertices) {
1098
+
1099
+ vertices || (vertices = []);
1100
+
1101
+ var namespace = this.paper.options.routerNamespace || routers;
1102
+ var router = this.model.router();
1103
+ var defaultRouter = this.paper.options.defaultRouter;
1104
+
1105
+ if (!router) {
1106
+ if (defaultRouter) router = defaultRouter;
1107
+ else return vertices.map(Point); // no router specified
1108
+ }
1109
+
1110
+ var routerFn = isFunction(router) ? router : namespace[router.name];
1111
+ if (!isFunction(routerFn)) {
1112
+ throw new Error('dia.LinkView: unknown router: "' + router.name + '".');
1113
+ }
1114
+
1115
+ var args = router.args || {};
1116
+
1117
+ var route = routerFn.call(
1118
+ this, // context
1119
+ vertices, // vertices
1120
+ args, // options
1121
+ this // linkView
1122
+ );
1123
+
1124
+ if (!route) return vertices.map(Point);
1125
+ return route;
1126
+ },
1127
+
1128
+ // Return the `d` attribute value of the `<path>` element representing the link
1129
+ // between `source` and `target`.
1130
+ findPath: function(route, sourcePoint, targetPoint) {
1131
+
1132
+ var namespace = this.paper.options.connectorNamespace || connectors;
1133
+ var connector = this.model.connector();
1134
+ var defaultConnector = this.paper.options.defaultConnector;
1135
+
1136
+ if (!connector) {
1137
+ connector = defaultConnector || {};
1138
+ }
1139
+
1140
+ var connectorFn = isFunction(connector) ? connector : namespace[connector.name];
1141
+ if (!isFunction(connectorFn)) {
1142
+ throw new Error('dia.LinkView: unknown connector: "' + connector.name + '".');
1143
+ }
1144
+
1145
+ var args = clone(connector.args || {});
1146
+ args.raw = true; // Request raw g.Path as the result.
1147
+
1148
+ var path = connectorFn.call(
1149
+ this, // context
1150
+ sourcePoint, // start point
1151
+ targetPoint, // end point
1152
+ route, // vertices
1153
+ args, // options
1154
+ this // linkView
1155
+ );
1156
+
1157
+ if (typeof path === 'string') {
1158
+ // Backwards compatibility for connectors not supporting `raw` option.
1159
+ path = new Path(V.normalizePathData(path));
1160
+ }
1161
+
1162
+ return path;
1163
+ },
1164
+
1165
+ // Public API.
1166
+ // -----------
1167
+
1168
+ getConnection: function() {
1169
+
1170
+ var path = this.path;
1171
+ if (!path) return null;
1172
+
1173
+ return path.clone();
1174
+ },
1175
+
1176
+ getSerializedConnection: function() {
1177
+
1178
+ var path = this.path;
1179
+ if (!path) return null;
1180
+
1181
+ var metrics = this.metrics;
1182
+ if (metrics.hasOwnProperty('data')) return metrics.data;
1183
+ var data = path.serialize();
1184
+ metrics.data = data;
1185
+ return data;
1186
+ },
1187
+
1188
+ getConnectionSubdivisions: function() {
1189
+
1190
+ var path = this.path;
1191
+ if (!path) return null;
1192
+
1193
+ var metrics = this.metrics;
1194
+ if (metrics.hasOwnProperty('segmentSubdivisions')) return metrics.segmentSubdivisions;
1195
+ var subdivisions = path.getSegmentSubdivisions();
1196
+ metrics.segmentSubdivisions = subdivisions;
1197
+ return subdivisions;
1198
+ },
1199
+
1200
+ getConnectionLength: function() {
1201
+
1202
+ var path = this.path;
1203
+ if (!path) return 0;
1204
+
1205
+ var metrics = this.metrics;
1206
+ if (metrics.hasOwnProperty('length')) return metrics.length;
1207
+ var length = path.length({ segmentSubdivisions: this.getConnectionSubdivisions() });
1208
+ metrics.length = length;
1209
+ return length;
1210
+ },
1211
+
1212
+ getPointAtLength: function(length) {
1213
+
1214
+ var path = this.path;
1215
+ if (!path) return null;
1216
+
1217
+ return path.pointAtLength(length, { segmentSubdivisions: this.getConnectionSubdivisions() });
1218
+ },
1219
+
1220
+ getPointAtRatio: function(ratio) {
1221
+
1222
+ var path = this.path;
1223
+ if (!path) return null;
1224
+ if (isPercentage(ratio)) ratio = parseFloat(ratio) / 100;
1225
+ return path.pointAt(ratio, { segmentSubdivisions: this.getConnectionSubdivisions() });
1226
+ },
1227
+
1228
+ getTangentAtLength: function(length) {
1229
+
1230
+ var path = this.path;
1231
+ if (!path) return null;
1232
+
1233
+ return path.tangentAtLength(length, { segmentSubdivisions: this.getConnectionSubdivisions() });
1234
+ },
1235
+
1236
+ getTangentAtRatio: function(ratio) {
1237
+
1238
+ var path = this.path;
1239
+ if (!path) return null;
1240
+
1241
+ return path.tangentAt(ratio, { segmentSubdivisions: this.getConnectionSubdivisions() });
1242
+ },
1243
+
1244
+ getClosestPoint: function(point) {
1245
+
1246
+ var path = this.path;
1247
+ if (!path) return null;
1248
+
1249
+ return path.closestPoint(point, { segmentSubdivisions: this.getConnectionSubdivisions() });
1250
+ },
1251
+
1252
+ getClosestPointLength: function(point) {
1253
+
1254
+ var path = this.path;
1255
+ if (!path) return null;
1256
+
1257
+ return path.closestPointLength(point, { segmentSubdivisions: this.getConnectionSubdivisions() });
1258
+ },
1259
+
1260
+ getClosestPointRatio: function(point) {
1261
+
1262
+ var path = this.path;
1263
+ if (!path) return null;
1264
+
1265
+ return path.closestPointNormalizedLength(point, { segmentSubdivisions: this.getConnectionSubdivisions() });
1266
+ },
1267
+
1268
+ // Get label position object based on two provided coordinates, x and y.
1269
+ // (Used behind the scenes when user moves labels around.)
1270
+ // Two signatures:
1271
+ // - num, num, obj = x, y, options
1272
+ // - num, num, num, obj = x, y, angle, options
1273
+ // Accepts distance/offset options = `absoluteDistance: boolean`, `reverseDistance: boolean`, `absoluteOffset: boolean`
1274
+ // - `absoluteOffset` is necessary in order to move beyond connection endpoints
1275
+ // Additional options = `keepGradient: boolean`, `ensureLegibility: boolean`
1276
+ getLabelPosition: function(x, y, p3, p4) {
1277
+
1278
+ var position = {};
1279
+
1280
+ // normalize data from the two possible signatures
1281
+ var localAngle = 0;
1282
+ var localOpt;
1283
+ if (typeof p3 === 'number') {
1284
+ // angle and opt provided as third and fourth argument
1285
+ localAngle = p3;
1286
+ localOpt = p4;
1287
+ } else {
1288
+ // opt provided as third argument
1289
+ localOpt = p3;
1290
+ }
1291
+
1292
+ // save localOpt as `args` of the position object that is passed along
1293
+ if (localOpt) position.args = localOpt;
1294
+
1295
+ // identify distance/offset settings
1296
+ var isDistanceRelative = !(localOpt && localOpt.absoluteDistance); // relative by default
1297
+ var isDistanceAbsoluteReverse = (localOpt && localOpt.absoluteDistance && localOpt.reverseDistance); // non-reverse by default
1298
+ var isOffsetAbsolute = localOpt && localOpt.absoluteOffset; // offset is non-absolute by default
1299
+
1300
+ // find closest point t
1301
+ var path = this.path;
1302
+ var pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() };
1303
+ var labelPoint = new Point(x, y);
1304
+ var t = path.closestPointT(labelPoint, pathOpt);
1305
+
1306
+ // DISTANCE:
1307
+ var labelDistance = path.lengthAtT(t, pathOpt);
1308
+ if (isDistanceRelative) labelDistance = (labelDistance / this.getConnectionLength()) || 0; // fix to prevent NaN for 0 length
1309
+ if (isDistanceAbsoluteReverse) labelDistance = (-1 * (this.getConnectionLength() - labelDistance)) || 1; // fix for end point (-0 => 1)
1310
+ position.distance = labelDistance;
1311
+
1312
+ // OFFSET:
1313
+ // use absolute offset if:
1314
+ // - opt.absoluteOffset is true,
1315
+ // - opt.absoluteOffset is not true but there is no tangent
1316
+ var tangent;
1317
+ if (!isOffsetAbsolute) tangent = path.tangentAtT(t);
1318
+ var labelOffset;
1319
+ if (tangent) {
1320
+ labelOffset = tangent.pointOffset(labelPoint);
1321
+ } else {
1322
+ var closestPoint = path.pointAtT(t);
1323
+ var labelOffsetDiff = labelPoint.difference(closestPoint);
1324
+ labelOffset = { x: labelOffsetDiff.x, y: labelOffsetDiff.y };
1325
+ }
1326
+ position.offset = labelOffset;
1327
+
1328
+ // ANGLE:
1329
+ position.angle = localAngle;
1330
+
1331
+ return position;
1332
+ },
1333
+
1334
+ _getLabelTransformationMatrix: function(labelPosition) {
1335
+
1336
+ var labelDistance;
1337
+ var labelAngle = 0;
1338
+ var args = {};
1339
+ if (typeof labelPosition === 'number') {
1340
+ labelDistance = labelPosition;
1341
+ } else if (typeof labelPosition.distance === 'number') {
1342
+ args = labelPosition.args || {};
1343
+ labelDistance = labelPosition.distance;
1344
+ labelAngle = labelPosition.angle || 0;
1345
+ } else {
1346
+ throw new Error('dia.LinkView: invalid label position distance.');
1347
+ }
1348
+
1349
+ var isDistanceRelative = ((labelDistance > 0) && (labelDistance <= 1));
1350
+
1351
+ var labelOffset = 0;
1352
+ var labelOffsetCoordinates = { x: 0, y: 0 };
1353
+ if (labelPosition.offset) {
1354
+ var positionOffset = labelPosition.offset;
1355
+ if (typeof positionOffset === 'number') labelOffset = positionOffset;
1356
+ if (positionOffset.x) labelOffsetCoordinates.x = positionOffset.x;
1357
+ if (positionOffset.y) labelOffsetCoordinates.y = positionOffset.y;
1358
+ }
1359
+
1360
+ var isOffsetAbsolute = ((labelOffsetCoordinates.x !== 0) || (labelOffsetCoordinates.y !== 0) || labelOffset === 0);
1361
+
1362
+ var isKeepGradient = args.keepGradient;
1363
+ var isEnsureLegibility = args.ensureLegibility;
1364
+
1365
+ var path = this.path;
1366
+ var pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() };
1367
+
1368
+ var distance = isDistanceRelative ? (labelDistance * this.getConnectionLength()) : labelDistance;
1369
+ var tangent = path.tangentAtLength(distance, pathOpt);
1370
+
1371
+ var translation;
1372
+ var angle = labelAngle;
1373
+ if (tangent) {
1374
+ if (isOffsetAbsolute) {
1375
+ translation = tangent.start.clone();
1376
+ translation.offset(labelOffsetCoordinates);
1377
+ } else {
1378
+ var normal = tangent.clone();
1379
+ normal.rotate(tangent.start, -90);
1380
+ normal.setLength(labelOffset);
1381
+ translation = normal.end;
1382
+ }
1383
+
1384
+ if (isKeepGradient) {
1385
+ angle = (tangent.angle() + labelAngle);
1386
+ if (isEnsureLegibility) {
1387
+ angle = normalizeAngle(((angle + 90) % 180) - 90);
1388
+ }
1389
+ }
1390
+
1391
+ } else {
1392
+ // fallback - the connection has zero length
1393
+ translation = path.start.clone();
1394
+ if (isOffsetAbsolute) translation.offset(labelOffsetCoordinates);
1395
+ }
1396
+
1397
+ return V.createSVGMatrix()
1398
+ .translate(translation.x, translation.y)
1399
+ .rotate(angle);
1400
+ },
1401
+
1402
+ getLabelCoordinates: function(labelPosition) {
1403
+
1404
+ var transformationMatrix = this._getLabelTransformationMatrix(labelPosition);
1405
+ return new Point(transformationMatrix.e, transformationMatrix.f);
1406
+ },
1407
+
1408
+ getVertexIndex: function(x, y) {
1409
+
1410
+ var model = this.model;
1411
+ var vertices = model.vertices();
1412
+
1413
+ var vertexLength = this.getClosestPointLength(new Point(x, y));
1414
+
1415
+ var idx = 0;
1416
+ for (var n = vertices.length; idx < n; idx++) {
1417
+ var currentVertex = vertices[idx];
1418
+ var currentVertexLength = this.getClosestPointLength(currentVertex);
1419
+ if (vertexLength < currentVertexLength) break;
1420
+ }
1421
+
1422
+ return idx;
1423
+ },
1424
+
1425
+ // Interaction. The controller part.
1426
+ // ---------------------------------
1427
+
1428
+ notifyPointerdown(evt, x, y) {
1429
+ CellView.prototype.pointerdown.call(this, evt, x, y);
1430
+ this.notify('link:pointerdown', evt, x, y);
1431
+ },
1432
+
1433
+ notifyPointermove(evt, x, y) {
1434
+ CellView.prototype.pointermove.call(this, evt, x, y);
1435
+ this.notify('link:pointermove', evt, x, y);
1436
+ },
1437
+
1438
+ notifyPointerup(evt, x, y) {
1439
+ this.notify('link:pointerup', evt, x, y);
1440
+ CellView.prototype.pointerup.call(this, evt, x, y);
1441
+ },
1442
+
1443
+ pointerdblclick: function(evt, x, y) {
1444
+
1445
+ CellView.prototype.pointerdblclick.apply(this, arguments);
1446
+ this.notify('link:pointerdblclick', evt, x, y);
1447
+ },
1448
+
1449
+ pointerclick: function(evt, x, y) {
1450
+
1451
+ CellView.prototype.pointerclick.apply(this, arguments);
1452
+ this.notify('link:pointerclick', evt, x, y);
1453
+ },
1454
+
1455
+ contextmenu: function(evt, x, y) {
1456
+
1457
+ CellView.prototype.contextmenu.apply(this, arguments);
1458
+ this.notify('link:contextmenu', evt, x, y);
1459
+ },
1460
+
1461
+ pointerdown: function(evt, x, y) {
1462
+
1463
+ this.notifyPointerdown(evt, x, y);
1464
+ this.dragStart(evt, x, y);
1465
+ },
1466
+
1467
+ pointermove: function(evt, x, y) {
1468
+
1469
+ // Backwards compatibility
1470
+ var dragData = this._dragData;
1471
+ if (dragData) this.eventData(evt, dragData);
1472
+
1473
+ var data = this.eventData(evt);
1474
+ switch (data.action) {
1475
+
1476
+ case 'label-move':
1477
+ this.dragLabel(evt, x, y);
1478
+ break;
1479
+
1480
+ case 'arrowhead-move':
1481
+ this.dragArrowhead(evt, x, y);
1482
+ break;
1483
+
1484
+ case 'move':
1485
+ this.drag(evt, x, y);
1486
+ break;
1487
+ }
1488
+
1489
+ // Backwards compatibility
1490
+ if (dragData) assign(dragData, this.eventData(evt));
1491
+
1492
+ this.notifyPointermove(evt, x, y);
1493
+ },
1494
+
1495
+ pointerup: function(evt, x, y) {
1496
+
1497
+ // Backwards compatibility
1498
+ var dragData = this._dragData;
1499
+ if (dragData) {
1500
+ this.eventData(evt, dragData);
1501
+ this._dragData = null;
1502
+ }
1503
+
1504
+ var data = this.eventData(evt);
1505
+ switch (data.action) {
1506
+
1507
+ case 'label-move':
1508
+ this.dragLabelEnd(evt, x, y);
1509
+ break;
1510
+
1511
+ case 'arrowhead-move':
1512
+ this.dragArrowheadEnd(evt, x, y);
1513
+ break;
1514
+
1515
+ case 'move':
1516
+ this.dragEnd(evt, x, y);
1517
+ }
1518
+
1519
+ this.notifyPointerup(evt, x, y);
1520
+ this.checkMouseleave(evt);
1521
+ },
1522
+
1523
+ mouseover: function(evt) {
1524
+
1525
+ CellView.prototype.mouseover.apply(this, arguments);
1526
+ this.notify('link:mouseover', evt);
1527
+ },
1528
+
1529
+ mouseout: function(evt) {
1530
+
1531
+ CellView.prototype.mouseout.apply(this, arguments);
1532
+ this.notify('link:mouseout', evt);
1533
+ },
1534
+
1535
+ mouseenter: function(evt) {
1536
+
1537
+ CellView.prototype.mouseenter.apply(this, arguments);
1538
+ this.notify('link:mouseenter', evt);
1539
+ },
1540
+
1541
+ mouseleave: function(evt) {
1542
+
1543
+ CellView.prototype.mouseleave.apply(this, arguments);
1544
+ this.notify('link:mouseleave', evt);
1545
+ },
1546
+
1547
+ mousewheel: function(evt, x, y, delta) {
1548
+
1549
+ CellView.prototype.mousewheel.apply(this, arguments);
1550
+ this.notify('link:mousewheel', evt, x, y, delta);
1551
+ },
1552
+
1553
+ onlabel: function(evt, x, y) {
1554
+
1555
+ this.notifyPointerdown(evt, x, y);
1556
+
1557
+ this.dragLabelStart(evt, x, y);
1558
+
1559
+ var stopPropagation = this.eventData(evt).stopPropagation;
1560
+ if (stopPropagation) evt.stopPropagation();
1561
+ },
1562
+
1563
+ // Drag Start Handlers
1564
+
1565
+ dragLabelStart: function(evt, x, y) {
1566
+
1567
+ if (this.can('labelMove')) {
1568
+
1569
+ if (this.isDefaultInteractionPrevented(evt)) return;
1570
+
1571
+ var labelNode = evt.currentTarget;
1572
+ var labelIdx = parseInt(labelNode.getAttribute('label-idx'), 10);
1573
+
1574
+ var defaultLabelPosition = this._getDefaultLabelPositionProperty();
1575
+ var initialLabelPosition = this._normalizeLabelPosition(this._getLabelPositionProperty(labelIdx));
1576
+ var position = this._mergeLabelPositionProperty(initialLabelPosition, defaultLabelPosition);
1577
+
1578
+ var coords = this.getLabelCoordinates(position);
1579
+ var dx = coords.x - x; // how much needs to be added to cursor x to get to label x
1580
+ var dy = coords.y - y; // how much needs to be added to cursor y to get to label y
1581
+
1582
+ var positionAngle = this._getLabelPositionAngle(labelIdx);
1583
+ var labelPositionArgs = this._getLabelPositionArgs(labelIdx);
1584
+ var defaultLabelPositionArgs = this._getDefaultLabelPositionArgs();
1585
+ var positionArgs = this._mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs);
1586
+
1587
+ this.eventData(evt, {
1588
+ action: 'label-move',
1589
+ labelIdx: labelIdx,
1590
+ dx: dx,
1591
+ dy: dy,
1592
+ positionAngle: positionAngle,
1593
+ positionArgs: positionArgs,
1594
+ stopPropagation: true
1595
+ });
1596
+
1597
+ } else {
1598
+
1599
+ // Backwards compatibility:
1600
+ // If labels can't be dragged no default action is triggered.
1601
+ this.eventData(evt, { stopPropagation: true });
1602
+ }
1603
+
1604
+ this.paper.delegateDragEvents(this, evt.data);
1605
+ },
1606
+
1607
+ dragArrowheadStart: function(evt, x, y) {
1608
+
1609
+ if (!this.can('arrowheadMove')) return;
1610
+
1611
+ var arrowheadNode = evt.target;
1612
+ var arrowheadType = arrowheadNode.getAttribute('end');
1613
+ var data = this.startArrowheadMove(arrowheadType, { ignoreBackwardsCompatibility: true });
1614
+
1615
+ this.eventData(evt, data);
1616
+ },
1617
+
1618
+ dragStart: function(evt, x, y) {
1619
+
1620
+ if (this.isDefaultInteractionPrevented(evt)) return;
1621
+
1622
+ if (!this.can('linkMove')) return;
1623
+
1624
+ this.eventData(evt, {
1625
+ action: 'move',
1626
+ dx: x,
1627
+ dy: y
1628
+ });
1629
+ },
1630
+
1631
+ // Drag Handlers
1632
+ dragLabel: function(evt, x, y) {
1633
+
1634
+ var data = this.eventData(evt);
1635
+ var label = { position: this.getLabelPosition((x + data.dx), (y + data.dy), data.positionAngle, data.positionArgs) };
1636
+ if (this.paper.options.snapLabels) delete label.position.offset;
1637
+ // The `touchmove' events are not fired
1638
+ // when the original event target is removed from the DOM.
1639
+ // The labels are currently re-rendered completely when only
1640
+ // the position changes. This is why we need to make sure that
1641
+ // the label is updated synchronously.
1642
+ // TODO: replace `touchmove` with `pointermove` (breaking change).
1643
+ const setOptions = { ui: true };
1644
+ if (this.paper.isAsync() && evt.type === 'touchmove') {
1645
+ setOptions.async = false;
1646
+ }
1647
+ this.model.label(data.labelIdx, label, setOptions);
1648
+ },
1649
+
1650
+ dragArrowhead: function(evt, x, y) {
1651
+ if (this.paper.options.snapLinks) {
1652
+ const isSnapped = this._snapArrowhead(evt, x, y);
1653
+ if (!isSnapped && this.paper.options.snapLinksSelf) {
1654
+ this._snapArrowheadSelf(evt, x, y);
1655
+ }
1656
+ } else {
1657
+ if (this.paper.options.snapLinksSelf) {
1658
+ this._snapArrowheadSelf(evt, x, y);
1659
+ } else {
1660
+ this._connectArrowhead(this.getEventTarget(evt), x, y, this.eventData(evt));
1661
+ }
1662
+ }
1663
+ },
1664
+
1665
+ drag: function(evt, x, y) {
1666
+
1667
+ var data = this.eventData(evt);
1668
+ this.model.translate(x - data.dx, y - data.dy, { ui: true });
1669
+ this.eventData(evt, {
1670
+ dx: x,
1671
+ dy: y
1672
+ });
1673
+ },
1674
+
1675
+ // Drag End Handlers
1676
+
1677
+ dragLabelEnd: function() {
1678
+ // noop
1679
+ },
1680
+
1681
+ dragArrowheadEnd: function(evt, x, y) {
1682
+
1683
+ var data = this.eventData(evt);
1684
+ var paper = this.paper;
1685
+
1686
+ if (paper.options.snapLinks) {
1687
+ this._snapArrowheadEnd(data);
1688
+ } else {
1689
+ this._connectArrowheadEnd(data, x, y);
1690
+ }
1691
+
1692
+ if (!paper.linkAllowed(this)) {
1693
+ // If the changed link is not allowed, revert to its previous state.
1694
+ this._disallow(data);
1695
+ } else {
1696
+ this._finishEmbedding(data);
1697
+ this._notifyConnectEvent(data, evt);
1698
+ }
1699
+
1700
+ this._afterArrowheadMove(data);
1701
+ },
1702
+
1703
+ dragEnd: function() {
1704
+ // noop
1705
+ },
1706
+
1707
+ _disallow: function(data) {
1708
+
1709
+ switch (data.whenNotAllowed) {
1710
+
1711
+ case 'remove':
1712
+ this.model.remove({ ui: true });
1713
+ break;
1714
+
1715
+ case 'revert':
1716
+ default:
1717
+ this.model.set(data.arrowhead, data.initialEnd, { ui: true });
1718
+ break;
1719
+ }
1720
+ },
1721
+
1722
+ _finishEmbedding: function(data) {
1723
+
1724
+ // Reparent the link if embedding is enabled
1725
+ if (this.paper.options.embeddingMode && this.model.reparent()) {
1726
+ // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()).
1727
+ data.z = null;
1728
+ }
1729
+ },
1730
+
1731
+ _notifyConnectEvent: function(data, evt) {
1732
+
1733
+ var arrowhead = data.arrowhead;
1734
+ var initialEnd = data.initialEnd;
1735
+ var currentEnd = this.model.prop(arrowhead);
1736
+ var endChanged = currentEnd && !Link.endsEqual(initialEnd, currentEnd);
1737
+ if (endChanged) {
1738
+ var paper = this.paper;
1739
+ if (initialEnd.id) {
1740
+ this.notify('link:disconnect', evt, paper.findViewByModel(initialEnd.id), data.initialMagnet, arrowhead);
1741
+ }
1742
+ if (currentEnd.id) {
1743
+ this.notify('link:connect', evt, paper.findViewByModel(currentEnd.id), data.magnetUnderPointer, arrowhead);
1744
+ }
1745
+ }
1746
+ },
1747
+
1748
+ _snapToPoints: function(snapPoint, points, radius) {
1749
+ let closestPointX = null;
1750
+ let closestDistanceX = Infinity;
1751
+
1752
+ let closestPointY = null;
1753
+ let closestDistanceY = Infinity;
1754
+
1755
+ let x = snapPoint.x;
1756
+ let y = snapPoint.y;
1757
+
1758
+ for (let i = 0; i < points.length; i++) {
1759
+ const distX = Math.abs(points[i].x - snapPoint.x);
1760
+ if (distX < closestDistanceX) {
1761
+ closestDistanceX = distX;
1762
+ closestPointX = points[i];
1763
+ }
1764
+
1765
+ const distY = Math.abs(points[i].y - snapPoint.y);
1766
+ if (distY < closestDistanceY) {
1767
+ closestDistanceY = distY;
1768
+ closestPointY = points[i];
1769
+ }
1770
+ }
1771
+
1772
+ if (closestDistanceX < radius) {
1773
+ x = closestPointX.x;
1774
+ }
1775
+ if (closestDistanceY < radius) {
1776
+ y = closestPointY.y;
1777
+ }
1778
+
1779
+ return { x, y };
1780
+ },
1781
+
1782
+ _snapArrowheadSelf: function(evt, x, y) {
1783
+
1784
+ const { paper, model } = this;
1785
+ const { snapLinksSelf } = paper.options;
1786
+ const data = this.eventData(evt);
1787
+ const radius = snapLinksSelf.radius || 20;
1788
+
1789
+ const anchor = this.getEndAnchor(data.arrowhead === 'source' ? 'target' : 'source');
1790
+ const vertices = model.vertices();
1791
+ const points = [anchor, ...vertices];
1792
+
1793
+ const snapPoint = this._snapToPoints({ x: x, y: y }, points, radius);
1794
+
1795
+ const point = paper.localToClientPoint(snapPoint);
1796
+ this._connectArrowhead(document.elementFromPoint(point.x, point.y), snapPoint.x, snapPoint.y, this.eventData(evt));
1797
+ },
1798
+
1799
+ _snapArrowhead: function(evt, x, y) {
1800
+
1801
+ const { paper } = this;
1802
+ const { snapLinks, connectionStrategy } = paper.options;
1803
+ const data = this.eventData(evt);
1804
+ let isSnapped = false;
1805
+ // checking view in close area of the pointer
1806
+
1807
+ var r = snapLinks.radius || 50;
1808
+ var viewsInArea = paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r });
1809
+
1810
+ var prevClosestView = data.closestView || null;
1811
+ var prevClosestMagnet = data.closestMagnet || null;
1812
+ var prevMagnetProxy = data.magnetProxy || null;
1813
+
1814
+ data.closestView = data.closestMagnet = data.magnetProxy = null;
1815
+
1816
+ var minDistance = Number.MAX_VALUE;
1817
+ var pointer = new Point(x, y);
1818
+
1819
+ viewsInArea.forEach(function(view) {
1820
+ const candidates = [];
1821
+ // skip connecting to the element in case '.': { magnet: false } attribute present
1822
+ if (view.el.getAttribute('magnet') !== 'false') {
1823
+ candidates.push({
1824
+ bbox: view.model.getBBox(),
1825
+ magnet: view.el
1826
+ });
1827
+ }
1828
+
1829
+ view.$('[magnet]').toArray().forEach(magnet => {
1830
+ candidates.push({
1831
+ bbox: view.getNodeBBox(magnet),
1832
+ magnet
1833
+ });
1834
+ });
1835
+
1836
+ candidates.forEach(candidate => {
1837
+ const { magnet, bbox } = candidate;
1838
+ // find distance from the center of the model to pointer coordinates
1839
+ const distance = bbox.center().squaredDistance(pointer);
1840
+ // the connection is looked up in a circle area by `distance < r`
1841
+ if (distance < minDistance) {
1842
+ const isAlreadyValidated = prevClosestMagnet === magnet;
1843
+ if (isAlreadyValidated || paper.options.validateConnection.apply(
1844
+ paper, data.validateConnectionArgs(view, (view.el === magnet) ? null : magnet)
1845
+ )) {
1846
+ minDistance = distance;
1847
+ data.closestView = view;
1848
+ data.closestMagnet = magnet;
1849
+ }
1850
+ }
1851
+ });
1852
+
1853
+ }, this);
1854
+
1855
+ var end;
1856
+ var magnetProxy = null;
1857
+ var closestView = data.closestView;
1858
+ var closestMagnet = data.closestMagnet;
1859
+ if (closestMagnet) {
1860
+ magnetProxy = data.magnetProxy = closestView.findProxyNode(closestMagnet, 'highlighter');
1861
+ }
1862
+ var endType = data.arrowhead;
1863
+ var newClosestMagnet = (prevClosestMagnet !== closestMagnet);
1864
+ if (prevClosestView && newClosestMagnet) {
1865
+ prevClosestView.unhighlight(prevMagnetProxy, {
1866
+ connecting: true,
1867
+ snapping: true
1868
+ });
1869
+ }
1870
+
1871
+ if (closestView) {
1872
+ const { prevEnd, prevX, prevY } = data;
1873
+ data.prevX = x;
1874
+ data.prevY = y;
1875
+ isSnapped = true;
1876
+
1877
+ if (!newClosestMagnet) {
1878
+ if (typeof connectionStrategy !== 'function' || (prevX === x && prevY === y)) {
1879
+ // the magnet has not changed and the link's end does not depend on the x and y
1880
+ return isSnapped;
1881
+ }
1882
+ }
1883
+
1884
+ end = closestView.getLinkEnd(closestMagnet, x, y, this.model, endType);
1885
+ if (!newClosestMagnet && isEqual(prevEnd, end)) {
1886
+ // the source/target json has not changed
1887
+ return isSnapped;
1888
+ }
1889
+
1890
+ data.prevEnd = end;
1891
+
1892
+ if (newClosestMagnet) {
1893
+ closestView.highlight(magnetProxy, {
1894
+ connecting: true,
1895
+ snapping: true
1896
+ });
1897
+ }
1898
+
1899
+ } else {
1900
+
1901
+ end = { x: x, y: y };
1902
+ }
1903
+
1904
+ this.model.set(endType, end || { x: x, y: y }, { ui: true });
1905
+
1906
+ if (prevClosestView) {
1907
+ this.notify('link:snap:disconnect', evt, prevClosestView, prevClosestMagnet, endType);
1908
+ }
1909
+ if (closestView) {
1910
+ this.notify('link:snap:connect', evt, closestView, closestMagnet, endType);
1911
+ }
1912
+
1913
+ return isSnapped;
1914
+ },
1915
+
1916
+ _snapArrowheadEnd: function(data) {
1917
+
1918
+ // Finish off link snapping.
1919
+ // Everything except view unhighlighting was already done on pointermove.
1920
+ var closestView = data.closestView;
1921
+ var closestMagnet = data.closestMagnet;
1922
+ if (closestView && closestMagnet) {
1923
+
1924
+ closestView.unhighlight(data.magnetProxy, { connecting: true, snapping: true });
1925
+ data.magnetUnderPointer = closestView.findMagnet(closestMagnet);
1926
+ }
1927
+
1928
+ data.closestView = data.closestMagnet = null;
1929
+ },
1930
+
1931
+ _connectArrowhead: function(target, x, y, data) {
1932
+
1933
+ // checking views right under the pointer
1934
+ const { paper, model } = this;
1935
+
1936
+ if (data.eventTarget !== target) {
1937
+ // Unhighlight the previous view under pointer if there was one.
1938
+ if (data.magnetProxy) {
1939
+ data.viewUnderPointer.unhighlight(data.magnetProxy, {
1940
+ connecting: true
1941
+ });
1942
+ }
1943
+
1944
+ const viewUnderPointer = data.viewUnderPointer = paper.findView(target);
1945
+ if (viewUnderPointer) {
1946
+ // If we found a view that is under the pointer, we need to find the closest
1947
+ // magnet based on the real target element of the event.
1948
+ const magnetUnderPointer = data.magnetUnderPointer = viewUnderPointer.findMagnet(target);
1949
+ const magnetProxy = data.magnetProxy = viewUnderPointer.findProxyNode(magnetUnderPointer, 'highlighter');
1950
+
1951
+ if (magnetUnderPointer && this.paper.options.validateConnection.apply(
1952
+ paper,
1953
+ data.validateConnectionArgs(viewUnderPointer, magnetUnderPointer)
1954
+ )) {
1955
+ // If there was no magnet found, do not highlight anything and assume there
1956
+ // is no view under pointer we're interested in reconnecting to.
1957
+ // This can only happen if the overall element has the attribute `'.': { magnet: false }`.
1958
+ if (magnetProxy) {
1959
+ viewUnderPointer.highlight(magnetProxy, {
1960
+ connecting: true
1961
+ });
1962
+ }
1963
+ } else {
1964
+ // This type of connection is not valid. Disregard this magnet.
1965
+ data.magnetUnderPointer = null;
1966
+ data.magnetProxy = null;
1967
+ }
1968
+ } else {
1969
+ // Make sure we'll unset previous magnet.
1970
+ data.magnetUnderPointer = null;
1971
+ data.magnetProxy = null;
1972
+ }
1973
+ }
1974
+
1975
+ data.eventTarget = target;
1976
+
1977
+ model.set(data.arrowhead, { x: x, y: y }, { ui: true });
1978
+ },
1979
+
1980
+ _connectArrowheadEnd: function(data = {}, x, y) {
1981
+
1982
+ const { model } = this;
1983
+ const { viewUnderPointer, magnetUnderPointer, magnetProxy, arrowhead } = data;
1984
+
1985
+ if (!magnetUnderPointer || !magnetProxy || !viewUnderPointer) return;
1986
+
1987
+ viewUnderPointer.unhighlight(magnetProxy, { connecting: true });
1988
+
1989
+ // The link end is taken from the magnet under the pointer, not the proxy.
1990
+ const end = viewUnderPointer.getLinkEnd(magnetUnderPointer, x, y, model, arrowhead);
1991
+ model.set(arrowhead, end, { ui: true });
1992
+ },
1993
+
1994
+ _beforeArrowheadMove: function(data) {
1995
+
1996
+ data.z = this.model.get('z');
1997
+ this.model.toFront();
1998
+
1999
+ // Let the pointer propagate through the link view elements so that
2000
+ // the `evt.target` is another element under the pointer, not the link itself.
2001
+ var style = this.el.style;
2002
+ data.pointerEvents = style.pointerEvents;
2003
+ style.pointerEvents = 'none';
2004
+
2005
+ if (this.paper.options.markAvailable) {
2006
+ this._markAvailableMagnets(data);
2007
+ }
2008
+ },
2009
+
2010
+ _afterArrowheadMove: function(data) {
2011
+
2012
+ if (data.z !== null) {
2013
+ this.model.set('z', data.z, { ui: true });
2014
+ data.z = null;
2015
+ }
2016
+
2017
+ // Put `pointer-events` back to its original value. See `_beforeArrowheadMove()` for explanation.
2018
+ this.el.style.pointerEvents = data.pointerEvents;
2019
+
2020
+ if (this.paper.options.markAvailable) {
2021
+ this._unmarkAvailableMagnets(data);
2022
+ }
2023
+ },
2024
+
2025
+ _createValidateConnectionArgs: function(arrowhead) {
2026
+ // It makes sure the arguments for validateConnection have the following form:
2027
+ // (source view, source magnet, target view, target magnet and link view)
2028
+ var args = [];
2029
+
2030
+ args[4] = arrowhead;
2031
+ args[5] = this;
2032
+
2033
+ var oppositeArrowhead;
2034
+ var i = 0;
2035
+ var j = 0;
2036
+
2037
+ if (arrowhead === 'source') {
2038
+ i = 2;
2039
+ oppositeArrowhead = 'target';
2040
+ } else {
2041
+ j = 2;
2042
+ oppositeArrowhead = 'source';
2043
+ }
2044
+
2045
+ var end = this.model.get(oppositeArrowhead);
2046
+
2047
+ if (end.id) {
2048
+ var view = args[i] = this.paper.findViewByModel(end.id);
2049
+ var magnet = view.getMagnetFromLinkEnd(end);
2050
+ if (magnet === view.el) magnet = undefined;
2051
+ args[i + 1] = magnet;
2052
+ }
2053
+
2054
+ function validateConnectionArgs(cellView, magnet) {
2055
+ args[j] = cellView;
2056
+ args[j + 1] = cellView.el === magnet ? undefined : magnet;
2057
+ return args;
2058
+ }
2059
+
2060
+ return validateConnectionArgs;
2061
+ },
2062
+
2063
+ _markAvailableMagnets: function(data) {
2064
+
2065
+ function isMagnetAvailable(view, magnet) {
2066
+ var paper = view.paper;
2067
+ var validate = paper.options.validateConnection;
2068
+ return validate.apply(paper, this.validateConnectionArgs(view, magnet));
2069
+ }
2070
+
2071
+ var paper = this.paper;
2072
+ var elements = paper.model.getCells();
2073
+ data.marked = {};
2074
+
2075
+ for (var i = 0, n = elements.length; i < n; i++) {
2076
+ var view = elements[i].findView(paper);
2077
+
2078
+ if (!view) {
2079
+ continue;
2080
+ }
2081
+
2082
+ var magnets = Array.prototype.slice.call(view.el.querySelectorAll('[magnet]'));
2083
+ if (view.el.getAttribute('magnet') !== 'false') {
2084
+ // Element wrapping group is also a magnet
2085
+ magnets.push(view.el);
2086
+ }
2087
+
2088
+ var availableMagnets = magnets.filter(isMagnetAvailable.bind(data, view));
2089
+
2090
+ if (availableMagnets.length > 0) {
2091
+ // highlight all available magnets
2092
+ for (var j = 0, m = availableMagnets.length; j < m; j++) {
2093
+ view.highlight(availableMagnets[j], { magnetAvailability: true });
2094
+ }
2095
+ // highlight the entire view
2096
+ view.highlight(null, { elementAvailability: true });
2097
+
2098
+ data.marked[view.model.id] = availableMagnets;
2099
+ }
2100
+ }
2101
+ },
2102
+
2103
+ _unmarkAvailableMagnets: function(data) {
2104
+
2105
+ var markedKeys = Object.keys(data.marked);
2106
+ var id;
2107
+ var markedMagnets;
2108
+
2109
+ for (var i = 0, n = markedKeys.length; i < n; i++) {
2110
+ id = markedKeys[i];
2111
+ markedMagnets = data.marked[id];
2112
+
2113
+ var view = this.paper.findViewByModel(id);
2114
+ if (view) {
2115
+ for (var j = 0, m = markedMagnets.length; j < m; j++) {
2116
+ view.unhighlight(markedMagnets[j], { magnetAvailability: true });
2117
+ }
2118
+ view.unhighlight(null, { elementAvailability: true });
2119
+ }
2120
+ }
2121
+
2122
+ data.marked = null;
2123
+ },
2124
+
2125
+ startArrowheadMove: function(end, opt) {
2126
+
2127
+ opt || (opt = {});
2128
+
2129
+ // Allow to delegate events from an another view to this linkView in order to trigger arrowhead
2130
+ // move without need to click on the actual arrowhead dom element.
2131
+ var data = {
2132
+ action: 'arrowhead-move',
2133
+ arrowhead: end,
2134
+ whenNotAllowed: opt.whenNotAllowed || 'revert',
2135
+ initialMagnet: this[end + 'Magnet'] || (this[end + 'View'] ? this[end + 'View'].el : null),
2136
+ initialEnd: clone(this.model.get(end)),
2137
+ validateConnectionArgs: this._createValidateConnectionArgs(end)
2138
+ };
2139
+
2140
+ this._beforeArrowheadMove(data);
2141
+
2142
+ if (opt.ignoreBackwardsCompatibility !== true) {
2143
+ this._dragData = data;
2144
+ }
2145
+
2146
+ return data;
2147
+ },
2148
+
2149
+ // Lifecycle methods
2150
+
2151
+ onMount: function() {
2152
+ CellView.prototype.onMount.apply(this, arguments);
2153
+ this.mountLabels();
2154
+ },
2155
+
2156
+ onDetach: function() {
2157
+ CellView.prototype.onDetach.apply(this, arguments);
2158
+ this.unmountLabels();
2159
+ },
2160
+
2161
+ onRemove: function() {
2162
+ CellView.prototype.onRemove.apply(this, arguments);
2163
+ this.unmountLabels();
2164
+ }
2165
+
2166
+ }, {
2167
+
2168
+ Flags: Flags,
2169
+ });
2170
+
2171
+ Object.defineProperty(LinkView.prototype, 'sourceBBox', {
2172
+
2173
+ enumerable: true,
2174
+
2175
+ get: function() {
2176
+ var sourceView = this.sourceView;
2177
+ if (!sourceView) {
2178
+ var sourceDef = this.model.source();
2179
+ return new Rect(sourceDef.x, sourceDef.y);
2180
+ }
2181
+ var sourceMagnet = this.sourceMagnet;
2182
+ if (sourceView.isNodeConnection(sourceMagnet)) {
2183
+ return new Rect(this.sourceAnchor);
2184
+ }
2185
+ return sourceView.getNodeBBox(sourceMagnet || sourceView.el);
2186
+ }
2187
+
2188
+ });
2189
+
2190
+ Object.defineProperty(LinkView.prototype, 'targetBBox', {
2191
+
2192
+ enumerable: true,
2193
+
2194
+ get: function() {
2195
+ var targetView = this.targetView;
2196
+ if (!targetView) {
2197
+ var targetDef = this.model.target();
2198
+ return new Rect(targetDef.x, targetDef.y);
2199
+ }
2200
+ var targetMagnet = this.targetMagnet;
2201
+ if (targetView.isNodeConnection(targetMagnet)) {
2202
+ return new Rect(this.targetAnchor);
2203
+ }
2204
+ return targetView.getNodeBBox(targetMagnet || targetView.el);
2205
+ }
2206
+ });
2207
+