@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,3171 @@
1
+ import V from '../V/index.mjs';
2
+ import {
3
+ isNumber,
4
+ assign,
5
+ nextFrame,
6
+ isObject,
7
+ cancelFrame,
8
+ defaults,
9
+ defaultsDeep,
10
+ addClassNamePrefix,
11
+ normalizeSides,
12
+ isFunction,
13
+ isPlainObject,
14
+ getByPath,
15
+ sortElements,
16
+ isString,
17
+ guid,
18
+ normalizeEvent,
19
+ normalizeWheel,
20
+ cap,
21
+ debounce,
22
+ omit,
23
+ result,
24
+ camelCase,
25
+ cloneDeep,
26
+ invoke,
27
+ hashCode,
28
+ filter as _filter,
29
+ parseDOMJSON,
30
+ toArray,
31
+ has
32
+ } from '../util/index.mjs';
33
+ import { ViewBase } from '../mvc/ViewBase.mjs';
34
+ import { Rect, Point, toRad } from '../g/index.mjs';
35
+ import { View, views } from '../mvc/index.mjs';
36
+ import { CellView } from './CellView.mjs';
37
+ import { ElementView } from './ElementView.mjs';
38
+ import { LinkView } from './LinkView.mjs';
39
+ import { Cell } from './Cell.mjs';
40
+ import { Graph } from './Graph.mjs';
41
+ import { LayersNames, PaperLayer } from './PaperLayer.mjs';
42
+ import * as highlighters from '../highlighters/index.mjs';
43
+ import * as linkAnchors from '../linkAnchors/index.mjs';
44
+ import * as connectionPoints from '../connectionPoints/index.mjs';
45
+ import * as anchors from '../anchors/index.mjs';
46
+
47
+ import $ from '../mvc/Dom/index.mjs';
48
+ import { GridLayer } from './layers/GridLayer.mjs';
49
+
50
+ const sortingTypes = {
51
+ NONE: 'sorting-none',
52
+ APPROX: 'sorting-approximate',
53
+ EXACT: 'sorting-exact'
54
+ };
55
+
56
+ const WHEEL_CAP = 50;
57
+ const WHEEL_WAIT_MS = 20;
58
+ const MOUNT_BATCH_SIZE = 1000;
59
+ const UPDATE_BATCH_SIZE = Infinity;
60
+ const MIN_PRIORITY = 9007199254740991; // Number.MAX_SAFE_INTEGER
61
+
62
+ const HighlightingTypes = CellView.Highlighting;
63
+
64
+ const defaultHighlighting = {
65
+ [HighlightingTypes.DEFAULT]: {
66
+ name: 'stroke',
67
+ options: {
68
+ padding: 3
69
+ }
70
+ },
71
+ [HighlightingTypes.MAGNET_AVAILABILITY]: {
72
+ name: 'addClass',
73
+ options: {
74
+ className: 'available-magnet'
75
+ }
76
+ },
77
+ [HighlightingTypes.ELEMENT_AVAILABILITY]: {
78
+ name: 'addClass',
79
+ options: {
80
+ className: 'available-cell'
81
+ }
82
+ }
83
+ };
84
+
85
+ const defaultLayers = [{
86
+ name: LayersNames.GRID,
87
+ }, {
88
+ name: LayersNames.BACK,
89
+ }, {
90
+ name: LayersNames.CELLS,
91
+ }, {
92
+ name: LayersNames.LABELS,
93
+ }, {
94
+ name: LayersNames.FRONT
95
+ }, {
96
+ name: LayersNames.TOOLS
97
+ }];
98
+
99
+ export const Paper = View.extend({
100
+
101
+ className: 'paper',
102
+
103
+ options: {
104
+
105
+ width: 800,
106
+ height: 600,
107
+ gridSize: 1,
108
+ // Whether or not to draw the grid lines on the paper's DOM element.
109
+ // e.g drawGrid: true, drawGrid: { color: 'red', thickness: 2 }
110
+ drawGrid: false,
111
+ // If not set, the size of the visual grid is the same as the `gridSize`.
112
+ drawGridSize: null,
113
+
114
+ // Whether or not to draw the background on the paper's DOM element.
115
+ // e.g. background: { color: 'lightblue', image: '/paper-background.png', repeat: 'flip-xy' }
116
+ background: false,
117
+
118
+ elementView: ElementView,
119
+ linkView: LinkView,
120
+ snapLabels: false, // false, true
121
+ snapLinks: false, // false, true, { radius: value }
122
+ snapLinksSelf: false, // false, true, { radius: value }
123
+
124
+ // Should the link labels be rendered into its own layer?
125
+ // `false` - the labels are part of the links
126
+ // `true` - the labels are appended to LayersName.LABELS
127
+ // [LayersName] - the labels are appended to the layer specified
128
+ labelsLayer: false,
129
+
130
+ // When set to FALSE, an element may not have more than 1 link with the same source and target element.
131
+ multiLinks: true,
132
+
133
+ // For adding custom guard logic.
134
+ guard: function(evt, view) {
135
+
136
+ // FALSE means the event isn't guarded.
137
+ return false;
138
+ },
139
+
140
+ highlighting: defaultHighlighting,
141
+
142
+ // Prevent the default context menu from being displayed.
143
+ preventContextMenu: true,
144
+
145
+ // Prevent the default action for blank:pointer<action>.
146
+ preventDefaultBlankAction: true,
147
+
148
+ // Prevent the default action for cell:pointer<action>.
149
+ preventDefaultViewAction: true,
150
+
151
+ // Restrict the translation of elements by given bounding box.
152
+ // Option accepts a boolean:
153
+ // true - the translation is restricted to the paper area
154
+ // false - no restrictions
155
+ // A method:
156
+ // restrictTranslate: function(elementView) {
157
+ // var parentId = elementView.model.get('parent');
158
+ // return parentId && this.model.getCell(parentId).getBBox();
159
+ // },
160
+ // Or a bounding box:
161
+ // restrictTranslate: { x: 10, y: 10, width: 790, height: 590 }
162
+ restrictTranslate: false,
163
+
164
+ // Marks all available magnets with 'available-magnet' class name and all available cells with
165
+ // 'available-cell' class name. Marks them when dragging a link is started and unmark
166
+ // when the dragging is stopped.
167
+ markAvailable: false,
168
+
169
+ // Defines what link model is added to the graph after an user clicks on an active magnet.
170
+ // Value could be the mvc.model or a function returning the mvc.model
171
+ // defaultLink: (elementView, magnet) => {
172
+ // return condition ? new customLink1() : new customLink2()
173
+ // }
174
+ defaultLink: function() {
175
+ // Do not create hard dependency on the joint.shapes.standard namespace (by importing the standard.Link model directly)
176
+ const { cellNamespace } = this.model.get('cells');
177
+ const ctor = getByPath(cellNamespace, ['standard', 'Link']);
178
+ if (!ctor) throw new Error('dia.Paper: no default link model found. Use `options.defaultLink` to specify a default link model.');
179
+ return new ctor();
180
+ },
181
+
182
+ // A connector that is used by links with no connector defined on the model.
183
+ // e.g. { name: 'rounded', args: { radius: 5 }} or a function
184
+ defaultConnector: { name: 'normal' },
185
+
186
+ // A router that is used by links with no router defined on the model.
187
+ // e.g. { name: 'oneSide', args: { padding: 10 }} or a function
188
+ defaultRouter: { name: 'normal' },
189
+
190
+ defaultAnchor: { name: 'center' },
191
+
192
+ defaultLinkAnchor: { name: 'connectionRatio' },
193
+
194
+ defaultConnectionPoint: { name: 'boundary' },
195
+
196
+ /* CONNECTING */
197
+
198
+ connectionStrategy: null,
199
+
200
+ // Check whether to add a new link to the graph when user clicks on an a magnet.
201
+ validateMagnet: function(_cellView, magnet, _evt) {
202
+ return magnet.getAttribute('magnet') !== 'passive';
203
+ },
204
+
205
+ // Check whether to allow or disallow the link connection while an arrowhead end (source/target)
206
+ // being changed.
207
+ validateConnection: function(cellViewS, _magnetS, cellViewT, _magnetT, end, _linkView) {
208
+ return (end === 'target' ? cellViewT : cellViewS) instanceof ElementView;
209
+ },
210
+
211
+ /* EMBEDDING */
212
+
213
+ // Enables embedding. Re-parent the dragged element with elements under it and makes sure that
214
+ // all links and elements are visible taken the level of embedding into account.
215
+ embeddingMode: false,
216
+
217
+ // Check whether to allow or disallow the element embedding while an element being translated.
218
+ validateEmbedding: function(childView, parentView) {
219
+ // by default all elements can be in relation child-parent
220
+ return true;
221
+ },
222
+
223
+ // Check whether to allow or disallow an embedded element to be unembedded / to become a root.
224
+ validateUnembedding: function(childView) {
225
+ // by default all elements can become roots
226
+ return true;
227
+ },
228
+
229
+ // Determines the way how a cell finds a suitable parent when it's dragged over the paper.
230
+ // The cell with the highest z-index (visually on the top) will be chosen.
231
+ findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft'
232
+
233
+ // If enabled only the element on the very front is taken into account for the embedding.
234
+ // If disabled the elements under the dragged view are tested one by one
235
+ // (from front to back) until a valid parent found.
236
+ frontParentOnly: true,
237
+
238
+ // Interactive flags. See online docs for the complete list of interactive flags.
239
+ interactive: {
240
+ labelMove: false
241
+ },
242
+
243
+ // When set to true the links can be pinned to the paper.
244
+ // i.e. link source/target can be a point e.g. link.get('source') ==> { x: 100, y: 100 };
245
+ linkPinning: true,
246
+
247
+ // Custom validation after an interaction with a link ends.
248
+ // Recognizes a function. If `false` is returned, the link is disallowed (removed or reverted)
249
+ // (linkView, paper) => boolean
250
+ allowLink: null,
251
+
252
+ // Allowed number of mousemove events after which the pointerclick event will be still triggered.
253
+ clickThreshold: 0,
254
+
255
+ // Number of required mousemove events before the first pointermove event will be triggered.
256
+ moveThreshold: 0,
257
+
258
+ // Number of required mousemove events before a link is created out of the magnet.
259
+ // Or string `onleave` so the link is created when the pointer leaves the magnet
260
+ magnetThreshold: 0,
261
+
262
+ // Rendering Options
263
+
264
+ sorting: sortingTypes.APPROX,
265
+
266
+ frozen: false,
267
+
268
+ autoFreeze: false,
269
+
270
+ // no docs yet
271
+ onViewUpdate: function(view, flag, priority, opt, paper) {
272
+ // Do not update connected links when:
273
+ // 1. the view was just inserted (added to the graph and rendered)
274
+ // 2. the view was just mounted (added back to the paper by viewport function)
275
+ // 3. the change was marked as `isolate`.
276
+ // 4. the view model was just removed from the graph
277
+ if ((flag & (view.FLAG_INSERT | view.FLAG_REMOVE)) || opt.mounting || opt.isolate) return;
278
+ paper.requestConnectedLinksUpdate(view, priority, opt);
279
+ },
280
+
281
+ // no docs yet
282
+ onViewPostponed: function(view, flag, paper) {
283
+ return paper.forcePostponedViewUpdate(view, flag);
284
+ },
285
+
286
+ beforeRender: null, // function(opt, paper) { },
287
+
288
+ afterRender: null, // function(stats, opt, paper) {
289
+
290
+ viewport: null,
291
+
292
+ // Default namespaces
293
+
294
+ cellViewNamespace: null,
295
+
296
+ routerNamespace: null,
297
+
298
+ connectorNamespace: null,
299
+
300
+ highlighterNamespace: highlighters,
301
+
302
+ anchorNamespace: anchors,
303
+
304
+ linkAnchorNamespace: linkAnchors,
305
+
306
+ connectionPointNamespace: connectionPoints,
307
+
308
+ overflow: false
309
+ },
310
+
311
+ events: {
312
+ 'dblclick': 'pointerdblclick',
313
+ 'dbltap': 'pointerdblclick',
314
+ 'contextmenu': 'contextmenu',
315
+ 'mousedown': 'pointerdown',
316
+ 'touchstart': 'pointerdown',
317
+ 'mouseover': 'mouseover',
318
+ 'mouseout': 'mouseout',
319
+ 'mouseenter': 'mouseenter',
320
+ 'mouseleave': 'mouseleave',
321
+ 'wheel': 'mousewheel',
322
+ 'mouseenter .joint-cell': 'mouseenter',
323
+ 'mouseleave .joint-cell': 'mouseleave',
324
+ 'mouseenter .joint-tools': 'mouseenter',
325
+ 'mouseleave .joint-tools': 'mouseleave',
326
+ 'dblclick .joint-cell [magnet]': 'magnetpointerdblclick',
327
+ 'contextmenu .joint-cell [magnet]': 'magnetcontextmenu',
328
+ 'mousedown .joint-link .label': 'onlabel', // interaction with link label
329
+ 'touchstart .joint-link .label': 'onlabel',
330
+ 'dragstart .joint-cell image': 'onImageDragStart' // firefox fix
331
+ },
332
+
333
+ documentEvents: {
334
+ 'mousemove': 'pointermove',
335
+ 'touchmove': 'pointermove',
336
+ 'mouseup': 'pointerup',
337
+ 'touchend': 'pointerup',
338
+ 'touchcancel': 'pointerup'
339
+ },
340
+
341
+ /* CSS within the SVG document
342
+ * 1. Adding vector-effect: non-scaling-stroke; to prevent the stroke width from scaling for
343
+ * elements that use the `scalable` group.
344
+ */
345
+ stylesheet: /*css*/`
346
+ .joint-element .scalable * {
347
+ vector-effect: non-scaling-stroke;
348
+ }
349
+ `,
350
+
351
+ svg: null,
352
+ viewport: null,
353
+ defs: null,
354
+ tools: null,
355
+ layers: null,
356
+
357
+ // For storing the current transformation matrix (CTM) of the paper's viewport.
358
+ _viewportMatrix: null,
359
+ // For verifying whether the CTM is up-to-date. The viewport transform attribute
360
+ // could have been manipulated directly.
361
+ _viewportTransformString: null,
362
+ // Updates data (priorities, unmounted views etc.)
363
+ _updates: null,
364
+ // Paper Layers
365
+ _layers: null,
366
+
367
+ SORT_DELAYING_BATCHES: ['add', 'to-front', 'to-back'],
368
+ UPDATE_DELAYING_BATCHES: ['translate'],
369
+ // If you interact with these elements,
370
+ // the default interaction such as `element move` is prevented.
371
+ FORM_CONTROL_TAG_NAMES: ['TEXTAREA', 'INPUT', 'BUTTON', 'SELECT', 'OPTION'] ,
372
+ // If you interact with these elements, the events are not propagated to the paper
373
+ // i.e. paper events such as `element:pointerdown` are not triggered.
374
+ GUARDED_TAG_NAMES: [
375
+ // Guard <select> for consistency. When you click on it:
376
+ // Chrome: triggers `pointerdown`, `pointerup`, `pointerclick` to open
377
+ // Firefox: triggers `pointerdown` on open, `pointerup` (and `pointerclick` only if you haven't moved).
378
+ // on close. However, if you open and then close by clicking elsewhere on the page,
379
+ // no other event is triggered.
380
+ // Safari: when you open it, it triggers `pointerdown`. That's it.
381
+ 'SELECT',
382
+ ],
383
+ MIN_SCALE: 1e-6,
384
+
385
+ init: function() {
386
+
387
+ const { options } = this;
388
+ if (!options.cellViewNamespace) {
389
+ /* eslint-disable no-undef */
390
+ options.cellViewNamespace = typeof joint !== 'undefined' && has(joint, 'shapes') ? joint.shapes : null;
391
+ /* eslint-enable no-undef */
392
+ }
393
+
394
+ const model = this.model = options.model || new Graph;
395
+
396
+ // Layers (SVGGroups)
397
+ this._layers = {};
398
+
399
+ this.cloneOptions();
400
+ this.render();
401
+ this._setDimensions();
402
+ this.startListening();
403
+
404
+ // Hash of all cell views.
405
+ this._views = {};
406
+
407
+ // Mouse wheel events buffer
408
+ this._mw_evt_buffer = {
409
+ event: null,
410
+ deltas: [],
411
+ };
412
+
413
+ // Render existing cells in the graph
414
+ this.resetViews(model.attributes.cells.models);
415
+ // Start the Rendering Loop
416
+ if (!this.isFrozen() && this.isAsync()) this.updateViewsAsync();
417
+ },
418
+
419
+ _resetUpdates: function() {
420
+ return this._updates = {
421
+ id: null,
422
+ priorities: [{}, {}, {}],
423
+ unmountedCids: [],
424
+ mountedCids: [],
425
+ unmounted: {},
426
+ mounted: {},
427
+ count: 0,
428
+ keyFrozen: false,
429
+ freezeKey: null,
430
+ sort: false,
431
+ disabled: false,
432
+ idle: false
433
+ };
434
+ },
435
+
436
+ startListening: function() {
437
+ var model = this.model;
438
+ this.listenTo(model, 'add', this.onCellAdded)
439
+ .listenTo(model, 'remove', this.onCellRemoved)
440
+ .listenTo(model, 'change', this.onCellChange)
441
+ .listenTo(model, 'reset', this.onGraphReset)
442
+ .listenTo(model, 'sort', this.onGraphSort)
443
+ .listenTo(model, 'batch:stop', this.onGraphBatchStop);
444
+ this.on('cell:highlight', this.onCellHighlight)
445
+ .on('cell:unhighlight', this.onCellUnhighlight)
446
+ .on('transform', this.update);
447
+ },
448
+
449
+ onCellAdded: function(cell, _, opt) {
450
+ var position = opt.position;
451
+ if (this.isAsync() || !isNumber(position)) {
452
+ this.renderView(cell, opt);
453
+ } else {
454
+ if (opt.maxPosition === position) this.freeze({ key: 'addCells' });
455
+ this.renderView(cell, opt);
456
+ if (position === 0) this.unfreeze({ key: 'addCells' });
457
+ }
458
+ },
459
+
460
+ onCellRemoved: function(cell, _, opt) {
461
+ const view = this.findViewByModel(cell);
462
+ if (view) this.requestViewUpdate(view, view.FLAG_REMOVE, view.UPDATE_PRIORITY, opt);
463
+ },
464
+
465
+ onCellChange: function(cell, opt) {
466
+ if (cell === this.model.attributes.cells) return;
467
+ if (cell.hasChanged('z') && this.options.sorting === sortingTypes.APPROX) {
468
+ const view = this.findViewByModel(cell);
469
+ if (view) this.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
470
+ }
471
+ },
472
+
473
+ onGraphReset: function(collection, opt) {
474
+ this.resetLayers();
475
+ this.resetViews(collection.models, opt);
476
+ },
477
+
478
+ onGraphSort: function() {
479
+ if (this.model.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return;
480
+ this.sortViews();
481
+ },
482
+
483
+ onGraphBatchStop: function(data) {
484
+ if (this.isFrozen()) return;
485
+ var name = data && data.batchName;
486
+ var graph = this.model;
487
+ if (!this.isAsync()) {
488
+ var updateDelayingBatches = this.UPDATE_DELAYING_BATCHES;
489
+ if (updateDelayingBatches.includes(name) && !graph.hasActiveBatch(updateDelayingBatches)) {
490
+ this.updateViews(data);
491
+ }
492
+ }
493
+ var sortDelayingBatches = this.SORT_DELAYING_BATCHES;
494
+ if (sortDelayingBatches.includes(name) && !graph.hasActiveBatch(sortDelayingBatches)) {
495
+ this.sortViews();
496
+ }
497
+ },
498
+
499
+ cloneOptions: function() {
500
+
501
+ const { options } = this;
502
+ const {
503
+ defaultConnector,
504
+ defaultRouter,
505
+ defaultConnectionPoint,
506
+ defaultAnchor,
507
+ defaultLinkAnchor,
508
+ highlighting,
509
+ cellViewNamespace,
510
+ interactive
511
+ } = options;
512
+
513
+ // Default cellView namespace for ES5
514
+ /* eslint-disable no-undef */
515
+ if (!cellViewNamespace && typeof joint !== 'undefined' && has(joint, 'shapes')) {
516
+ options.cellViewNamespace = joint.shapes;
517
+ }
518
+ /* eslint-enable no-undef */
519
+
520
+ // Here if a function was provided, we can not clone it, as this would result in loosing the function.
521
+ // If the default is used, the cloning is necessary in order to prevent modifying the options on prototype.
522
+ if (!isFunction(defaultConnector)) {
523
+ options.defaultConnector = cloneDeep(defaultConnector);
524
+ }
525
+ if (!isFunction(defaultRouter)) {
526
+ options.defaultRouter = cloneDeep(defaultRouter);
527
+ }
528
+ if (!isFunction(defaultConnectionPoint)) {
529
+ options.defaultConnectionPoint = cloneDeep(defaultConnectionPoint);
530
+ }
531
+ if (!isFunction(defaultAnchor)) {
532
+ options.defaultAnchor = cloneDeep(defaultAnchor);
533
+ }
534
+ if (!isFunction(defaultLinkAnchor)) {
535
+ options.defaultLinkAnchor = cloneDeep(defaultLinkAnchor);
536
+ }
537
+ if (isPlainObject(interactive)) {
538
+ options.interactive = assign({}, interactive);
539
+ }
540
+ if (isPlainObject(highlighting)) {
541
+ // Return the default highlighting options into the user specified options.
542
+ options.highlighting = defaultsDeep({}, highlighting, defaultHighlighting);
543
+ }
544
+ },
545
+
546
+ children: function() {
547
+ var ns = V.namespace;
548
+ return [{
549
+ namespaceURI: ns.xhtml,
550
+ tagName: 'div',
551
+ className: addClassNamePrefix('paper-background'),
552
+ selector: 'background',
553
+ style: {
554
+ position: 'absolute',
555
+ inset: 0
556
+ }
557
+ }, {
558
+ namespaceURI: ns.svg,
559
+ tagName: 'svg',
560
+ attributes: {
561
+ 'width': '100%',
562
+ 'height': '100%',
563
+ 'xmlns:xlink': ns.xlink
564
+ },
565
+ selector: 'svg',
566
+ style: {
567
+ position: 'absolute',
568
+ inset: 0
569
+ },
570
+ children: [{
571
+ // Append `<defs>` element to the SVG document. This is useful for filters and gradients.
572
+ // It's desired to have the defs defined before the viewport (e.g. to make a PDF document pick up defs properly).
573
+ tagName: 'defs',
574
+ selector: 'defs'
575
+ }, {
576
+ tagName: 'g',
577
+ className: addClassNamePrefix('layers'),
578
+ selector: 'layers'
579
+ }]
580
+ }];
581
+ },
582
+
583
+ hasLayerView(layerName) {
584
+ return (layerName in this._layers);
585
+ },
586
+
587
+ getLayerView(layerName) {
588
+ const { _layers } = this;
589
+ if (layerName in _layers) return _layers[layerName];
590
+ throw new Error(`dia.Paper: Unknown layer "${layerName}"`);
591
+ },
592
+
593
+ getLayerNode(layerName) {
594
+ return this.getLayerView(layerName).el;
595
+ },
596
+
597
+ render: function() {
598
+
599
+ this.renderChildren();
600
+ const { el, childNodes, options, stylesheet } = this;
601
+ const { svg, defs, layers } = childNodes;
602
+
603
+ el.style.position = 'relative';
604
+ svg.style.overflow = options.overflow ? 'visible' : 'hidden';
605
+
606
+ this.svg = svg;
607
+ this.defs = defs;
608
+ this.layers = layers;
609
+
610
+ this.renderLayers();
611
+
612
+ V.ensureId(svg);
613
+
614
+ this.addStylesheet(stylesheet);
615
+
616
+ if (options.background) {
617
+ this.drawBackground(options.background);
618
+ }
619
+
620
+ if (options.drawGrid) {
621
+ this.setGrid(options.drawGrid);
622
+ }
623
+
624
+ return this;
625
+ },
626
+
627
+ addStylesheet: function(css) {
628
+ if (!css) return;
629
+ V(this.svg).prepend(V.createSVGStyle(css));
630
+ },
631
+
632
+ createLayer(name) {
633
+ switch (name) {
634
+ case LayersNames.GRID:
635
+ return new GridLayer({ name, paper: this, patterns: this.constructor.gridPatterns });
636
+ default:
637
+ return new PaperLayer({ name });
638
+ }
639
+ },
640
+
641
+ renderLayers: function(layers = defaultLayers) {
642
+ this.removeLayers();
643
+ // TODO: Layers to be read from the graph `layers` attribute
644
+ layers.forEach(({ name, sorted }) => {
645
+ const layerView = this.createLayer(name);
646
+ this.layers.appendChild(layerView.el);
647
+ this._layers[name] = layerView;
648
+ });
649
+ // Throws an exception if doesn't exist
650
+ const cellsLayerView = this.getLayerView(LayersNames.CELLS);
651
+ const toolsLayerView = this.getLayerView(LayersNames.TOOLS);
652
+ const labelsLayerView = this.getLayerView(LayersNames.LABELS);
653
+ // backwards compatibility
654
+ this.tools = toolsLayerView.el;
655
+ this.cells = this.viewport = cellsLayerView.el;
656
+ // user-select: none;
657
+ cellsLayerView.vel.addClass(addClassNamePrefix('viewport'));
658
+ labelsLayerView.vel.addClass(addClassNamePrefix('viewport'));
659
+ cellsLayerView.el.style.webkitUserSelect = 'none';
660
+ cellsLayerView.el.style.userSelect = 'none';
661
+ labelsLayerView.el.style.webkitUserSelect = 'none';
662
+ labelsLayerView.el.style.userSelect = 'none';
663
+ },
664
+
665
+ removeLayers: function() {
666
+ const { _layers } = this;
667
+ Object.keys(_layers).forEach(name => {
668
+ _layers[name].remove();
669
+ delete _layers[name];
670
+ });
671
+ },
672
+
673
+ resetLayers: function() {
674
+ const { _layers } = this;
675
+ Object.keys(_layers).forEach(name => {
676
+ _layers[name].removePivots();
677
+ });
678
+ },
679
+
680
+ update: function() {
681
+
682
+ if (this._background) {
683
+ this.updateBackgroundImage(this._background);
684
+ }
685
+
686
+ return this;
687
+ },
688
+
689
+ scale: function(sx, sy, data) {
690
+ const ctm = this.matrix();
691
+ // getter
692
+ if (sx === undefined) {
693
+ return V.matrixToScale(ctm);
694
+ }
695
+ // setter
696
+ if (sy === undefined) {
697
+ sy = sx;
698
+ }
699
+ sx = Math.max(sx || 0, this.MIN_SCALE);
700
+ sy = Math.max(sy || 0, this.MIN_SCALE);
701
+ ctm.a = sx;
702
+ ctm.d = sy;
703
+ this.matrix(ctm, data);
704
+ return this;
705
+ },
706
+
707
+ scaleUniformAtPoint: function(scale, point, data) {
708
+ const { a: sx, d: sy, e: tx, f: ty } = this.matrix();
709
+ scale = Math.max(scale || 0, this.MIN_SCALE);
710
+ if (scale === sx && scale === sy) {
711
+ // The scale is the same as the current one.
712
+ return this;
713
+ }
714
+ const matrix = V.createSVGMatrix()
715
+ .translate(
716
+ tx - point.x * (scale - sx),
717
+ ty - point.y * (scale - sy)
718
+ )
719
+ .scale(scale, scale);
720
+ this.matrix(matrix, data);
721
+ return this;
722
+ },
723
+
724
+ translate: function(tx, ty, data) {
725
+ const ctm = this.matrix();
726
+ // getter
727
+ if (tx === undefined) {
728
+ return V.matrixToTranslate(ctm);
729
+ }
730
+ // setter
731
+ tx || (tx = 0);
732
+ ty || (ty = 0);
733
+ if (ctm.e === tx && ctm.f === ty) return this;
734
+ ctm.e = tx;
735
+ ctm.f = ty;
736
+ this.matrix(ctm, data);
737
+ return this;
738
+ },
739
+
740
+ matrix: function(ctm, data = {}) {
741
+
742
+ var viewport = this.layers;
743
+
744
+ // Getter:
745
+ if (ctm === undefined) {
746
+
747
+ var transformString = viewport.getAttribute('transform');
748
+
749
+ if ((this._viewportTransformString || null) === transformString) {
750
+ // It's ok to return the cached matrix. The transform attribute has not changed since
751
+ // the matrix was stored.
752
+ ctm = this._viewportMatrix;
753
+ } else {
754
+ // The viewport transform attribute has changed. Measure the matrix and cache again.
755
+ ctm = viewport.getCTM();
756
+ this._viewportMatrix = ctm;
757
+ this._viewportTransformString = transformString;
758
+ }
759
+
760
+ // Clone the cached current transformation matrix.
761
+ // If no matrix previously stored the identity matrix is returned.
762
+ return V.createSVGMatrix(ctm);
763
+ }
764
+
765
+ // Setter:
766
+ const prev = this.matrix();
767
+ const current = V.createSVGMatrix(ctm);
768
+ const currentTransformString = this._viewportTransformString;
769
+ const ctmString = V.matrixToTransformString(current);
770
+ if (ctmString === currentTransformString) {
771
+ // The new transform string is the same as the current one.
772
+ // No need to update the transform attribute.
773
+ return this;
774
+ }
775
+ if (!currentTransformString && V.matrixToTransformString() === ctmString) {
776
+ // The current transform string is empty and the new one is an identity matrix.
777
+ // No need to update the transform attribute.
778
+ return this;
779
+ }
780
+
781
+ const { a, d, e, f } = current;
782
+
783
+ viewport.setAttribute('transform', ctmString);
784
+ this._viewportMatrix = current;
785
+ this._viewportTransformString = viewport.getAttribute('transform');
786
+
787
+ // scale event
788
+ if (a !== prev.a || d !== prev.d) {
789
+ this.trigger('scale', a, d, data);
790
+ }
791
+
792
+ // translate event
793
+ if (e !== prev.e || f !== prev.f) {
794
+ this.trigger('translate', e, f, data);
795
+ }
796
+
797
+ this.trigger('transform', current, data);
798
+ return this;
799
+ },
800
+
801
+ clientMatrix: function() {
802
+
803
+ return V.createSVGMatrix(this.cells.getScreenCTM());
804
+ },
805
+
806
+ requestConnectedLinksUpdate: function(view, priority, opt) {
807
+ if (view instanceof CellView) {
808
+ var model = view.model;
809
+ var links = this.model.getConnectedLinks(model);
810
+ for (var j = 0, n = links.length; j < n; j++) {
811
+ var link = links[j];
812
+ var linkView = this.findViewByModel(link);
813
+ if (!linkView) continue;
814
+ var flagLabels = ['UPDATE'];
815
+ if (link.getTargetCell() === model) flagLabels.push('TARGET');
816
+ if (link.getSourceCell() === model) flagLabels.push('SOURCE');
817
+ var nextPriority = Math.max(priority + 1, linkView.UPDATE_PRIORITY);
818
+ this.scheduleViewUpdate(linkView, linkView.getFlag(flagLabels), nextPriority, opt);
819
+ }
820
+ }
821
+ },
822
+
823
+ forcePostponedViewUpdate: function(view, flag) {
824
+ if (!view || !(view instanceof CellView)) return false;
825
+ var model = view.model;
826
+ if (model.isElement()) return false;
827
+ if ((flag & view.getFlag(['SOURCE', 'TARGET'])) === 0) {
828
+ var dumpOptions = { silent: true };
829
+ // LinkView is waiting for the target or the source cellView to be rendered
830
+ // This can happen when the cells are not in the viewport.
831
+ var sourceFlag = 0;
832
+ var sourceView = this.findViewByModel(model.getSourceCell());
833
+ if (sourceView && !this.isViewMounted(sourceView)) {
834
+ sourceFlag = this.dumpView(sourceView, dumpOptions);
835
+ view.updateEndMagnet('source');
836
+ }
837
+ var targetFlag = 0;
838
+ var targetView = this.findViewByModel(model.getTargetCell());
839
+ if (targetView && !this.isViewMounted(targetView)) {
840
+ targetFlag = this.dumpView(targetView, dumpOptions);
841
+ view.updateEndMagnet('target');
842
+ }
843
+ if (sourceFlag === 0 && targetFlag === 0) {
844
+ // If leftover flag is 0, all view updates were done.
845
+ return !this.dumpView(view, dumpOptions);
846
+ }
847
+ }
848
+ return false;
849
+ },
850
+
851
+ requestViewUpdate: function(view, flag, priority, opt) {
852
+ opt || (opt = {});
853
+ this.scheduleViewUpdate(view, flag, priority, opt);
854
+ var isAsync = this.isAsync();
855
+ if (this.isFrozen() || (isAsync && opt.async !== false)) return;
856
+ if (this.model.hasActiveBatch(this.UPDATE_DELAYING_BATCHES)) return;
857
+ var stats = this.updateViews(opt);
858
+ if (isAsync) this.notifyAfterRender(stats, opt);
859
+ },
860
+
861
+ scheduleViewUpdate: function(view, type, priority, opt) {
862
+ const { _updates: updates, options } = this;
863
+ if (updates.idle) {
864
+ if (options.autoFreeze) {
865
+ updates.idle = false;
866
+ this.unfreeze();
867
+ }
868
+ }
869
+ const { FLAG_REMOVE, FLAG_INSERT, UPDATE_PRIORITY, cid } = view;
870
+ let priorityUpdates = updates.priorities[priority];
871
+ if (!priorityUpdates) priorityUpdates = updates.priorities[priority] = {};
872
+ // Move higher priority updates to this priority
873
+ if (priority > UPDATE_PRIORITY) {
874
+ // Not the default priority for this view. It's most likely a link view
875
+ // connected to another link view, which triggered the update.
876
+ // TODO: If there is an update scheduled with a lower priority already, we should
877
+ // change the requested priority to the lowest one. Does not seem to be critical
878
+ // right now, as it "only" results in multiple updates on the same view.
879
+ for (let i = priority - 1; i >= UPDATE_PRIORITY; i--) {
880
+ const prevPriorityUpdates = updates.priorities[i];
881
+ if (!prevPriorityUpdates || !(cid in prevPriorityUpdates)) continue;
882
+ priorityUpdates[cid] |= prevPriorityUpdates[cid];
883
+ delete prevPriorityUpdates[cid];
884
+ }
885
+ }
886
+ let currentType = priorityUpdates[cid] || 0;
887
+ // Prevent cycling
888
+ if ((currentType & type) === type) return;
889
+ if (!currentType) updates.count++;
890
+ if (type & FLAG_REMOVE && currentType & FLAG_INSERT) {
891
+ // When a view is removed we need to remove the insert flag as this is a reinsert
892
+ priorityUpdates[cid] ^= FLAG_INSERT;
893
+ } else if (type & FLAG_INSERT && currentType & FLAG_REMOVE) {
894
+ // When a view is added we need to remove the remove flag as this is view was previously removed
895
+ priorityUpdates[cid] ^= FLAG_REMOVE;
896
+ }
897
+ priorityUpdates[cid] |= type;
898
+ const viewUpdateFn = options.onViewUpdate;
899
+ if (typeof viewUpdateFn === 'function') viewUpdateFn.call(this, view, type, priority, opt || {}, this);
900
+ },
901
+
902
+ dumpViewUpdate: function(view) {
903
+ if (!view) return 0;
904
+ var updates = this._updates;
905
+ var cid = view.cid;
906
+ var priorityUpdates = updates.priorities[view.UPDATE_PRIORITY];
907
+ var flag = this.registerMountedView(view) | priorityUpdates[cid];
908
+ delete priorityUpdates[cid];
909
+ return flag;
910
+ },
911
+
912
+ dumpView: function(view, opt = {}) {
913
+ const flag = this.dumpViewUpdate(view);
914
+ if (!flag) return 0;
915
+ const shouldNotify = !opt.silent;
916
+ if (shouldNotify) this.notifyBeforeRender(opt);
917
+ const leftover = this.updateView(view, flag, opt);
918
+ if (shouldNotify) {
919
+ const stats = { updated: 1, priority: view.UPDATE_PRIORITY };
920
+ this.notifyAfterRender(stats, opt);
921
+ }
922
+ return leftover;
923
+ },
924
+
925
+ updateView: function(view, flag, opt) {
926
+ if (!view) return 0;
927
+ const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT, model } = view;
928
+ if (view instanceof CellView) {
929
+ if (flag & FLAG_REMOVE) {
930
+ this.removeView(model);
931
+ return 0;
932
+ }
933
+ if (flag & FLAG_INSERT) {
934
+ const isInitialInsert = !!(flag & FLAG_INIT);
935
+ if (isInitialInsert) {
936
+ flag ^= FLAG_INIT;
937
+ }
938
+ this.insertView(view, isInitialInsert);
939
+ flag ^= FLAG_INSERT;
940
+ }
941
+ }
942
+ if (!flag) return 0;
943
+ return view.confirmUpdate(flag, opt || {});
944
+ },
945
+
946
+ requireView: function(model, opt) {
947
+ var view = this.findViewByModel(model);
948
+ if (!view) return null;
949
+ this.dumpView(view, opt);
950
+ return view;
951
+ },
952
+
953
+ registerUnmountedView: function(view) {
954
+ var cid = view.cid;
955
+ var updates = this._updates;
956
+ if (cid in updates.unmounted) return 0;
957
+ var flag = updates.unmounted[cid] |= view.FLAG_INSERT;
958
+ updates.unmountedCids.push(cid);
959
+ delete updates.mounted[cid];
960
+ return flag;
961
+ },
962
+
963
+ registerMountedView: function(view) {
964
+ var cid = view.cid;
965
+ var updates = this._updates;
966
+ if (cid in updates.mounted) return 0;
967
+ updates.mounted[cid] = true;
968
+ updates.mountedCids.push(cid);
969
+ var flag = updates.unmounted[cid] || 0;
970
+ delete updates.unmounted[cid];
971
+ return flag;
972
+ },
973
+
974
+ isViewMounted: function(view) {
975
+ if (!view) return false;
976
+ var cid = view.cid;
977
+ var updates = this._updates;
978
+ return (cid in updates.mounted);
979
+ },
980
+
981
+ dumpViews: function(opt) {
982
+ var passingOpt = defaults({}, opt, { viewport: null });
983
+ this.checkViewport(passingOpt);
984
+ this.updateViews(passingOpt);
985
+ },
986
+
987
+ // Synchronous views update
988
+ updateViews: function(opt) {
989
+ this.notifyBeforeRender(opt);
990
+ let batchStats;
991
+ let updateCount = 0;
992
+ let batchCount = 0;
993
+ let priority = MIN_PRIORITY;
994
+ do {
995
+ batchCount++;
996
+ batchStats = this.updateViewsBatch(opt);
997
+ updateCount += batchStats.updated;
998
+ priority = Math.min(batchStats.priority, priority);
999
+ } while (!batchStats.empty);
1000
+ const stats = { updated: updateCount, batches: batchCount, priority };
1001
+ this.notifyAfterRender(stats, opt);
1002
+ return stats;
1003
+ },
1004
+
1005
+ hasScheduledUpdates: function() {
1006
+ const priorities = this._updates.priorities;
1007
+ const priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
1008
+ let i = priorityIndexes.length;
1009
+ while (i > 0 && i--) {
1010
+ // a faster way how to check if an object is empty
1011
+ for (let _key in priorities[priorityIndexes[i]]) return true;
1012
+ }
1013
+ return false;
1014
+ },
1015
+
1016
+ updateViewsAsync: function(opt, data) {
1017
+ opt || (opt = {});
1018
+ data || (data = { processed: 0, priority: MIN_PRIORITY });
1019
+ const { _updates: updates, options } = this;
1020
+ const id = updates.id;
1021
+ if (id) {
1022
+ cancelFrame(id);
1023
+ if (data.processed === 0 && this.hasScheduledUpdates()) {
1024
+ this.notifyBeforeRender(opt);
1025
+ }
1026
+ const stats = this.updateViewsBatch(opt);
1027
+ const passingOpt = defaults({}, opt, {
1028
+ mountBatchSize: MOUNT_BATCH_SIZE - stats.mounted,
1029
+ unmountBatchSize: MOUNT_BATCH_SIZE - stats.unmounted
1030
+ });
1031
+ const checkStats = this.checkViewport(passingOpt);
1032
+ const unmountCount = checkStats.unmounted;
1033
+ const mountCount = checkStats.mounted;
1034
+ let processed = data.processed;
1035
+ const total = updates.count;
1036
+ if (stats.updated > 0) {
1037
+ // Some updates have been just processed
1038
+ processed += stats.updated + stats.unmounted;
1039
+ stats.processed = processed;
1040
+ data.priority = Math.min(stats.priority, data.priority);
1041
+ if (stats.empty && mountCount === 0) {
1042
+ stats.unmounted += unmountCount;
1043
+ stats.mounted += mountCount;
1044
+ stats.priority = data.priority;
1045
+ this.notifyAfterRender(stats, opt);
1046
+ data.processed = 0;
1047
+ data.priority = MIN_PRIORITY;
1048
+ updates.count = 0;
1049
+ } else {
1050
+ data.processed = processed;
1051
+ }
1052
+ } else {
1053
+ if (!updates.idle) {
1054
+ if (options.autoFreeze) {
1055
+ this.freeze();
1056
+ updates.idle = true;
1057
+ this.trigger('render:idle', opt);
1058
+ }
1059
+ }
1060
+ }
1061
+ // Progress callback
1062
+ const progressFn = opt.progress;
1063
+ if (total && typeof progressFn === 'function') {
1064
+ progressFn.call(this, stats.empty, processed, total, stats, this);
1065
+ }
1066
+ // The current frame could have been canceled in a callback
1067
+ if (updates.id !== id) return;
1068
+ }
1069
+ if (updates.disabled) {
1070
+ throw new Error('dia.Paper: can not unfreeze the paper after it was removed');
1071
+ }
1072
+ updates.id = nextFrame(this.updateViewsAsync, this, opt, data);
1073
+ },
1074
+
1075
+ notifyBeforeRender: function(opt = {}) {
1076
+ let beforeFn = opt.beforeRender;
1077
+ if (typeof beforeFn !== 'function') {
1078
+ beforeFn = this.options.beforeRender;
1079
+ if (typeof beforeFn !== 'function') return;
1080
+ }
1081
+ beforeFn.call(this, opt, this);
1082
+ },
1083
+
1084
+ notifyAfterRender: function(stats, opt = {}) {
1085
+ let afterFn = opt.afterRender;
1086
+ if (typeof afterFn !== 'function') {
1087
+ afterFn = this.options.afterRender;
1088
+ }
1089
+ if (typeof afterFn === 'function') {
1090
+ afterFn.call(this, stats, opt, this);
1091
+ }
1092
+ this.trigger('render:done', stats, opt);
1093
+ },
1094
+
1095
+ updateViewsBatch: function(opt) {
1096
+ opt || (opt = {});
1097
+ var batchSize = opt.batchSize || UPDATE_BATCH_SIZE;
1098
+ var updates = this._updates;
1099
+ var updateCount = 0;
1100
+ var postponeCount = 0;
1101
+ var unmountCount = 0;
1102
+ var mountCount = 0;
1103
+ var maxPriority = MIN_PRIORITY;
1104
+ var empty = true;
1105
+ var options = this.options;
1106
+ var priorities = updates.priorities;
1107
+ var viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
1108
+ if (typeof viewportFn !== 'function') viewportFn = null;
1109
+ var postponeViewFn = options.onViewPostponed;
1110
+ if (typeof postponeViewFn !== 'function') postponeViewFn = null;
1111
+ var priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
1112
+ main: for (var i = 0, n = priorityIndexes.length; i < n; i++) {
1113
+ var priority = +priorityIndexes[i];
1114
+ var priorityUpdates = priorities[priority];
1115
+ for (var cid in priorityUpdates) {
1116
+ if (updateCount >= batchSize) {
1117
+ empty = false;
1118
+ break main;
1119
+ }
1120
+ var view = views[cid];
1121
+ if (!view) {
1122
+ // This should not occur
1123
+ delete priorityUpdates[cid];
1124
+ continue;
1125
+ }
1126
+ var currentFlag = priorityUpdates[cid];
1127
+ if ((currentFlag & view.FLAG_REMOVE) === 0) {
1128
+ // We should never check a view for viewport if we are about to remove the view
1129
+ var isDetached = cid in updates.unmounted;
1130
+ if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, !isDetached, this)) {
1131
+ // Unmount View
1132
+ if (!isDetached) {
1133
+ this.registerUnmountedView(view);
1134
+ this.detachView(view);
1135
+ }
1136
+ updates.unmounted[cid] |= currentFlag;
1137
+ delete priorityUpdates[cid];
1138
+ unmountCount++;
1139
+ continue;
1140
+ }
1141
+ // Mount View
1142
+ if (isDetached) {
1143
+ currentFlag |= view.FLAG_INSERT;
1144
+ mountCount++;
1145
+ }
1146
+ currentFlag |= this.registerMountedView(view);
1147
+ }
1148
+ var leftoverFlag = this.updateView(view, currentFlag, opt);
1149
+ if (leftoverFlag > 0) {
1150
+ // View update has not finished completely
1151
+ priorityUpdates[cid] = leftoverFlag;
1152
+ if (!postponeViewFn || !postponeViewFn.call(this, view, leftoverFlag, this) || priorityUpdates[cid]) {
1153
+ postponeCount++;
1154
+ empty = false;
1155
+ continue;
1156
+ }
1157
+ }
1158
+ if (maxPriority > priority) maxPriority = priority;
1159
+ updateCount++;
1160
+ delete priorityUpdates[cid];
1161
+ }
1162
+ }
1163
+ return {
1164
+ priority: maxPriority,
1165
+ updated: updateCount,
1166
+ postponed: postponeCount,
1167
+ unmounted: unmountCount,
1168
+ mounted: mountCount,
1169
+ empty: empty
1170
+ };
1171
+ },
1172
+
1173
+ getUnmountedViews: function() {
1174
+ const updates = this._updates;
1175
+ const unmountedCids = Object.keys(updates.unmounted);
1176
+ const n = unmountedCids.length;
1177
+ const unmountedViews = new Array(n);
1178
+ for (var i = 0; i < n; i++) {
1179
+ unmountedViews[i] = views[unmountedCids[i]];
1180
+ }
1181
+ return unmountedViews;
1182
+ },
1183
+
1184
+ getMountedViews: function() {
1185
+ const updates = this._updates;
1186
+ const mountedCids = Object.keys(updates.mounted);
1187
+ const n = mountedCids.length;
1188
+ const mountedViews = new Array(n);
1189
+ for (var i = 0; i < n; i++) {
1190
+ mountedViews[i] = views[mountedCids[i]];
1191
+ }
1192
+ return mountedViews;
1193
+ },
1194
+
1195
+ checkUnmountedViews: function(viewportFn, opt) {
1196
+ opt || (opt = {});
1197
+ var mountCount = 0;
1198
+ if (typeof viewportFn !== 'function') viewportFn = null;
1199
+ var batchSize = 'mountBatchSize' in opt ? opt.mountBatchSize : Infinity;
1200
+ var updates = this._updates;
1201
+ var unmountedCids = updates.unmountedCids;
1202
+ var unmounted = updates.unmounted;
1203
+ for (var i = 0, n = Math.min(unmountedCids.length, batchSize); i < n; i++) {
1204
+ var cid = unmountedCids[i];
1205
+ if (!(cid in unmounted)) continue;
1206
+ var view = views[cid];
1207
+ if (!view) continue;
1208
+ if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, false, this)) {
1209
+ // Push at the end of all unmounted ids, so this can be check later again
1210
+ unmountedCids.push(cid);
1211
+ continue;
1212
+ }
1213
+ mountCount++;
1214
+ var flag = this.registerMountedView(view);
1215
+ if (flag) this.scheduleViewUpdate(view, flag, view.UPDATE_PRIORITY, { mounting: true });
1216
+ }
1217
+ // Get rid of views, that have been mounted
1218
+ unmountedCids.splice(0, i);
1219
+ return mountCount;
1220
+ },
1221
+
1222
+ checkMountedViews: function(viewportFn, opt) {
1223
+ opt || (opt = {});
1224
+ var unmountCount = 0;
1225
+ if (typeof viewportFn !== 'function') return unmountCount;
1226
+ var batchSize = 'unmountBatchSize' in opt ? opt.unmountBatchSize : Infinity;
1227
+ var updates = this._updates;
1228
+ var mountedCids = updates.mountedCids;
1229
+ var mounted = updates.mounted;
1230
+ for (var i = 0, n = Math.min(mountedCids.length, batchSize); i < n; i++) {
1231
+ var cid = mountedCids[i];
1232
+ if (!(cid in mounted)) continue;
1233
+ var view = views[cid];
1234
+ if (!view) continue;
1235
+ if (!view.DETACHABLE || viewportFn.call(this, view, true, this)) {
1236
+ // Push at the end of all mounted ids, so this can be check later again
1237
+ mountedCids.push(cid);
1238
+ continue;
1239
+ }
1240
+ unmountCount++;
1241
+ var flag = this.registerUnmountedView(view);
1242
+ if (flag) this.detachView(view);
1243
+ }
1244
+ // Get rid of views, that have been unmounted
1245
+ mountedCids.splice(0, i);
1246
+ return unmountCount;
1247
+ },
1248
+
1249
+ checkViewVisibility: function(cellView, opt = {}) {
1250
+ let viewportFn = 'viewport' in opt ? opt.viewport : this.options.viewport;
1251
+ if (typeof viewportFn !== 'function') viewportFn = null;
1252
+ const updates = this._updates;
1253
+ const { mounted, unmounted } = updates;
1254
+ const visible = !cellView.DETACHABLE || !viewportFn || viewportFn.call(this, cellView, false, this);
1255
+
1256
+ let isUnmounted = false;
1257
+ let isMounted = false;
1258
+
1259
+ if (cellView.cid in mounted && !visible) {
1260
+ const flag = this.registerUnmountedView(cellView);
1261
+ if (flag) this.detachView(cellView);
1262
+ const i = updates.mountedCids.indexOf(cellView.cid);
1263
+ updates.mountedCids.splice(i, 1);
1264
+ isUnmounted = true;
1265
+ }
1266
+
1267
+ if (!isUnmounted && cellView.cid in unmounted && visible) {
1268
+ const i = updates.unmountedCids.indexOf(cellView.cid);
1269
+ updates.unmountedCids.splice(i, 1);
1270
+ var flag = this.registerMountedView(cellView);
1271
+ if (flag) this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, { mounting: true });
1272
+ isMounted = true;
1273
+ }
1274
+
1275
+ return {
1276
+ mounted: isMounted ? 1 : 0,
1277
+ unmounted: isUnmounted ? 1 : 0
1278
+ };
1279
+ },
1280
+
1281
+ checkViewport: function(opt) {
1282
+ var passingOpt = defaults({}, opt, {
1283
+ mountBatchSize: Infinity,
1284
+ unmountBatchSize: Infinity
1285
+ });
1286
+ var viewportFn = 'viewport' in passingOpt ? passingOpt.viewport : this.options.viewport;
1287
+ var unmountedCount = this.checkMountedViews(viewportFn, passingOpt);
1288
+ if (unmountedCount > 0) {
1289
+ // Do not check views, that have been just unmounted and pushed at the end of the cids array
1290
+ var unmountedCids = this._updates.unmountedCids;
1291
+ passingOpt.mountBatchSize = Math.min(unmountedCids.length - unmountedCount, passingOpt.mountBatchSize);
1292
+ }
1293
+ var mountedCount = this.checkUnmountedViews(viewportFn, passingOpt);
1294
+ return {
1295
+ mounted: mountedCount,
1296
+ unmounted: unmountedCount
1297
+ };
1298
+ },
1299
+
1300
+ freeze: function(opt) {
1301
+ opt || (opt = {});
1302
+ var updates = this._updates;
1303
+ var key = opt.key;
1304
+ var isFrozen = this.options.frozen;
1305
+ var freezeKey = updates.freezeKey;
1306
+ if (key && key !== freezeKey) {
1307
+ // key passed, but the paper is already freezed with another key
1308
+ if (isFrozen && freezeKey) return;
1309
+ updates.freezeKey = key;
1310
+ updates.keyFrozen = isFrozen;
1311
+ }
1312
+ this.options.frozen = true;
1313
+ var id = updates.id;
1314
+ updates.id = null;
1315
+ if (this.isAsync() && id) cancelFrame(id);
1316
+ },
1317
+
1318
+ unfreeze: function(opt) {
1319
+ opt || (opt = {});
1320
+ var updates = this._updates;
1321
+ var key = opt.key;
1322
+ var freezeKey = updates.freezeKey;
1323
+ // key passed, but the paper is already freezed with another key
1324
+ if (key && freezeKey && key !== freezeKey) return;
1325
+ updates.freezeKey = null;
1326
+ // key passed, but the paper is already freezed
1327
+ if (key && key === freezeKey && updates.keyFrozen) return;
1328
+ if (this.isAsync()) {
1329
+ this.freeze();
1330
+ this.updateViewsAsync(opt);
1331
+ } else {
1332
+ this.updateViews(opt);
1333
+ }
1334
+ this.options.frozen = updates.keyFrozen = false;
1335
+ if (updates.sort) {
1336
+ this.sortViews();
1337
+ updates.sort = false;
1338
+ }
1339
+ },
1340
+
1341
+ isAsync: function() {
1342
+ return !!this.options.async;
1343
+ },
1344
+
1345
+ isFrozen: function() {
1346
+ return !!this.options.frozen;
1347
+ },
1348
+
1349
+ isExactSorting: function() {
1350
+ return this.options.sorting === sortingTypes.EXACT;
1351
+ },
1352
+
1353
+ onRemove: function() {
1354
+
1355
+ this.freeze();
1356
+ this._updates.disabled = true;
1357
+ //clean up all DOM elements/views to prevent memory leaks
1358
+ this.removeLayers();
1359
+ this.removeViews();
1360
+ },
1361
+
1362
+ getComputedSize: function() {
1363
+
1364
+ var options = this.options;
1365
+ var w = options.width;
1366
+ var h = options.height;
1367
+ if (!isNumber(w)) w = this.el.clientWidth;
1368
+ if (!isNumber(h)) h = this.el.clientHeight;
1369
+ return { width: w, height: h };
1370
+ },
1371
+
1372
+ setDimensions: function(width, height, data = {}) {
1373
+ const { options } = this;
1374
+ const { width: currentWidth, height: currentHeight } = options;
1375
+ let w = (width === undefined) ? currentWidth : width;
1376
+ let h = (height === undefined) ? currentHeight : height;
1377
+ if (currentWidth === w && currentHeight === h) return;
1378
+ options.width = w;
1379
+ options.height = h;
1380
+ this._setDimensions();
1381
+ const computedSize = this.getComputedSize();
1382
+ this.trigger('resize', computedSize.width, computedSize.height, data);
1383
+ },
1384
+
1385
+ _setDimensions: function() {
1386
+ const { options } = this;
1387
+ let w = options.width;
1388
+ let h = options.height;
1389
+ if (isNumber(w)) w = `${Math.round(w)}px`;
1390
+ if (isNumber(h)) h = `${Math.round(h)}px`;
1391
+ this.$el.css({
1392
+ width: (w === null) ? '' : w,
1393
+ height: (h === null) ? '' : h
1394
+ });
1395
+ },
1396
+
1397
+ // Expand/shrink the paper to fit the content.
1398
+ // Alternatively signature function(opt)
1399
+ fitToContent: function(gridWidth, gridHeight, padding, opt) {
1400
+
1401
+ if (isObject(gridWidth)) {
1402
+ // first parameter is an option object
1403
+ opt = gridWidth;
1404
+ } else {
1405
+ // Support for a deprecated signature
1406
+ opt = assign({ gridWidth, gridHeight, padding }, opt);
1407
+ }
1408
+
1409
+ const { x, y, width, height } = this.getFitToContentArea(opt);
1410
+ const { sx, sy } = this.scale();
1411
+
1412
+ this.translate(-x * sx, -y * sy, opt);
1413
+ this.setDimensions(width * sx, height * sy, opt);
1414
+
1415
+ return new Rect(x, y, width, height);
1416
+ },
1417
+
1418
+ getFitToContentArea: function(opt = {}) {
1419
+
1420
+ // Calculate the paper size to accommodate all the graph's elements.
1421
+
1422
+ const gridWidth = opt.gridWidth || 1;
1423
+ const gridHeight = opt.gridHeight || 1;
1424
+ const padding = normalizeSides(opt.padding || 0);
1425
+
1426
+ const minWidth = Math.max(opt.minWidth || 0, gridWidth);
1427
+ const minHeight = Math.max(opt.minHeight || 0, gridHeight);
1428
+ const maxWidth = opt.maxWidth || Number.MAX_VALUE;
1429
+ const maxHeight = opt.maxHeight || Number.MAX_VALUE;
1430
+ const newOrigin = opt.allowNewOrigin;
1431
+
1432
+ const area = ('contentArea' in opt) ? new Rect(opt.contentArea) : this.getContentArea(opt);
1433
+ const { sx, sy } = this.scale();
1434
+ area.x *= sx;
1435
+ area.y *= sy;
1436
+ area.width *= sx;
1437
+ area.height *= sy;
1438
+
1439
+ let calcWidth = Math.ceil((area.width + area.x) / gridWidth);
1440
+ let calcHeight = Math.ceil((area.height + area.y) / gridHeight);
1441
+ if (!opt.allowNegativeBottomRight) {
1442
+ calcWidth = Math.max(calcWidth, 1);
1443
+ calcHeight = Math.max(calcHeight, 1);
1444
+ }
1445
+ calcWidth *= gridWidth;
1446
+ calcHeight *= gridHeight;
1447
+
1448
+ let tx = 0;
1449
+ if ((newOrigin === 'negative' && area.x < 0) || (newOrigin === 'positive' && area.x >= 0) || newOrigin === 'any') {
1450
+ tx = Math.ceil(-area.x / gridWidth) * gridWidth;
1451
+ tx += padding.left;
1452
+ calcWidth += tx;
1453
+ }
1454
+
1455
+ let ty = 0;
1456
+ if ((newOrigin === 'negative' && area.y < 0) || (newOrigin === 'positive' && area.y >= 0) || newOrigin === 'any') {
1457
+ ty = Math.ceil(-area.y / gridHeight) * gridHeight;
1458
+ ty += padding.top;
1459
+ calcHeight += ty;
1460
+ }
1461
+
1462
+ calcWidth += padding.right;
1463
+ calcHeight += padding.bottom;
1464
+
1465
+ // Make sure the resulting width and height are greater than minimum.
1466
+ calcWidth = Math.max(calcWidth, minWidth);
1467
+ calcHeight = Math.max(calcHeight, minHeight);
1468
+
1469
+ // Make sure the resulting width and height are lesser than maximum.
1470
+ calcWidth = Math.min(calcWidth, maxWidth);
1471
+ calcHeight = Math.min(calcHeight, maxHeight);
1472
+
1473
+ return new Rect(-tx / sx, -ty / sy, calcWidth / sx, calcHeight / sy);
1474
+ },
1475
+
1476
+ transformToFitContent: function(opt) {
1477
+ opt || (opt = {});
1478
+
1479
+ let contentBBox, contentLocalOrigin;
1480
+ if ('contentArea' in opt) {
1481
+ const contentArea = opt.contentArea;
1482
+ contentBBox = this.localToPaperRect(contentArea);
1483
+ contentLocalOrigin = new Point(contentArea);
1484
+ } else {
1485
+ contentBBox = this.getContentBBox(opt);
1486
+ contentLocalOrigin = this.paperToLocalPoint(contentBBox);
1487
+ }
1488
+
1489
+ if (!contentBBox.width || !contentBBox.height) return;
1490
+
1491
+ defaults(opt, {
1492
+ padding: 0,
1493
+ preserveAspectRatio: true,
1494
+ scaleGrid: null,
1495
+ minScale: 0,
1496
+ maxScale: Number.MAX_VALUE,
1497
+ verticalAlign: 'top',
1498
+ horizontalAlign: 'left',
1499
+ //minScaleX
1500
+ //minScaleY
1501
+ //maxScaleX
1502
+ //maxScaleY
1503
+ //fittingBBox
1504
+ });
1505
+
1506
+ const padding = normalizeSides(opt.padding);
1507
+
1508
+ const minScaleX = opt.minScaleX || opt.minScale;
1509
+ const maxScaleX = opt.maxScaleX || opt.maxScale;
1510
+ const minScaleY = opt.minScaleY || opt.minScale;
1511
+ const maxScaleY = opt.maxScaleY || opt.maxScale;
1512
+
1513
+ let fittingBBox;
1514
+ if (opt.fittingBBox) {
1515
+ fittingBBox = opt.fittingBBox;
1516
+ } else {
1517
+ const currentTranslate = this.translate();
1518
+ const computedSize = this.getComputedSize();
1519
+ fittingBBox = {
1520
+ x: currentTranslate.tx,
1521
+ y: currentTranslate.ty,
1522
+ width: computedSize.width,
1523
+ height: computedSize.height
1524
+ };
1525
+ }
1526
+
1527
+ fittingBBox = new Rect(fittingBBox).moveAndExpand({
1528
+ x: padding.left,
1529
+ y: padding.top,
1530
+ width: -padding.left - padding.right,
1531
+ height: -padding.top - padding.bottom
1532
+ });
1533
+
1534
+ const ctm = this.matrix();
1535
+ const { a: sx, d: sy, e: tx, f: ty } = ctm;
1536
+
1537
+ let newSx = fittingBBox.width / contentBBox.width * sx;
1538
+ let newSy = fittingBBox.height / contentBBox.height * sy;
1539
+
1540
+ if (opt.preserveAspectRatio) {
1541
+ newSx = newSy = Math.min(newSx, newSy);
1542
+ }
1543
+
1544
+ // snap scale to a grid
1545
+ if (opt.scaleGrid) {
1546
+
1547
+ const gridSize = opt.scaleGrid;
1548
+
1549
+ newSx = gridSize * Math.floor(newSx / gridSize);
1550
+ newSy = gridSize * Math.floor(newSy / gridSize);
1551
+ }
1552
+
1553
+ // scale min/max boundaries
1554
+ newSx = Math.min(maxScaleX, Math.max(minScaleX, newSx));
1555
+ newSy = Math.min(maxScaleY, Math.max(minScaleY, newSy));
1556
+
1557
+ const scaleDiff = {
1558
+ x: newSx / sx,
1559
+ y: newSy / sy
1560
+ };
1561
+
1562
+ let newOx = fittingBBox.x - contentLocalOrigin.x * newSx - tx;
1563
+ let newOy = fittingBBox.y - contentLocalOrigin.y * newSy - ty;
1564
+
1565
+ switch (opt.verticalAlign) {
1566
+ case 'middle':
1567
+ newOy = newOy + (fittingBBox.height - contentBBox.height * scaleDiff.y) / 2;
1568
+ break;
1569
+ case 'bottom':
1570
+ newOy = newOy + (fittingBBox.height - contentBBox.height * scaleDiff.y);
1571
+ break;
1572
+ case 'top':
1573
+ default:
1574
+ break;
1575
+ }
1576
+
1577
+ switch (opt.horizontalAlign) {
1578
+ case 'middle':
1579
+ newOx = newOx + (fittingBBox.width - contentBBox.width * scaleDiff.x) / 2;
1580
+ break;
1581
+ case 'right':
1582
+ newOx = newOx + (fittingBBox.width - contentBBox.width * scaleDiff.x);
1583
+ break;
1584
+ case 'left':
1585
+ default:
1586
+ break;
1587
+ }
1588
+
1589
+ ctm.a = newSx;
1590
+ ctm.d = newSy;
1591
+ ctm.e = newOx;
1592
+ ctm.f = newOy;
1593
+ this.matrix(ctm, opt);
1594
+ },
1595
+
1596
+ scaleContentToFit: function(opt) {
1597
+ this.transformToFitContent(opt);
1598
+ },
1599
+
1600
+ // Return the dimensions of the content area in local units (without transformations).
1601
+ getContentArea: function(opt) {
1602
+
1603
+ if (opt && opt.useModelGeometry) {
1604
+ return this.model.getBBox() || new Rect();
1605
+ }
1606
+
1607
+ return V(this.cells).getBBox();
1608
+ },
1609
+
1610
+ // Return the dimensions of the content bbox in the paper units (as it appears on screen).
1611
+ getContentBBox: function(opt) {
1612
+
1613
+ return this.localToPaperRect(this.getContentArea(opt));
1614
+ },
1615
+
1616
+ // Returns a geometry rectangle representing the entire
1617
+ // paper area (coordinates from the left paper border to the right one
1618
+ // and the top border to the bottom one).
1619
+ getArea: function() {
1620
+
1621
+ return this.paperToLocalRect(this.getComputedSize());
1622
+ },
1623
+
1624
+ getRestrictedArea: function(...args) {
1625
+
1626
+ const { restrictTranslate } = this.options;
1627
+
1628
+ let restrictedArea;
1629
+ if (isFunction(restrictTranslate)) {
1630
+ // A method returning a bounding box
1631
+ restrictedArea = restrictTranslate.apply(this, args);
1632
+ } else if (restrictTranslate === true) {
1633
+ // The paper area
1634
+ restrictedArea = this.getArea();
1635
+ } else if (!restrictTranslate) {
1636
+ // falsy value
1637
+ restrictedArea = null;
1638
+ } else {
1639
+ // any other value
1640
+ restrictedArea = new Rect(restrictTranslate);
1641
+ }
1642
+
1643
+ return restrictedArea;
1644
+ },
1645
+
1646
+ createViewForModel: function(cell) {
1647
+
1648
+ const { options } = this;
1649
+ // A class taken from the paper options.
1650
+ var optionalViewClass;
1651
+
1652
+ // A default basic class (either dia.ElementView or dia.LinkView)
1653
+ var defaultViewClass;
1654
+
1655
+ // A special class defined for this model in the corresponding namespace.
1656
+ // e.g. joint.shapes.standard.Rectangle searches for joint.shapes.standard.RectangleView
1657
+ var namespace = options.cellViewNamespace;
1658
+ var type = cell.get('type') + 'View';
1659
+ var namespaceViewClass = getByPath(namespace, type, '.');
1660
+
1661
+ if (cell.isLink()) {
1662
+ optionalViewClass = options.linkView;
1663
+ defaultViewClass = LinkView;
1664
+ } else {
1665
+ optionalViewClass = options.elementView;
1666
+ defaultViewClass = ElementView;
1667
+ }
1668
+
1669
+ // a) the paper options view is a class (deprecated)
1670
+ // 1. search the namespace for a view
1671
+ // 2. if no view was found, use view from the paper options
1672
+ // b) the paper options view is a function
1673
+ // 1. call the function from the paper options
1674
+ // 2. if no view was return, search the namespace for a view
1675
+ // 3. if no view was found, use the default
1676
+ var ViewClass = (optionalViewClass.prototype instanceof ViewBase)
1677
+ ? namespaceViewClass || optionalViewClass
1678
+ : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass;
1679
+
1680
+ return new ViewClass({
1681
+ model: cell,
1682
+ interactive: options.interactive,
1683
+ labelsLayer: options.labelsLayer === true ? LayersNames.LABELS : options.labelsLayer
1684
+ });
1685
+ },
1686
+
1687
+ removeView: function(cell) {
1688
+
1689
+ const { id } = cell;
1690
+ const { _views, _updates } = this;
1691
+ const view = _views[id];
1692
+ if (view) {
1693
+ var { cid } = view;
1694
+ const { mounted, unmounted } = _updates;
1695
+ view.remove();
1696
+ delete _views[id];
1697
+ delete mounted[cid];
1698
+ delete unmounted[cid];
1699
+ }
1700
+ return view;
1701
+ },
1702
+
1703
+ renderView: function(cell, opt) {
1704
+
1705
+ const { id } = cell;
1706
+ const views = this._views;
1707
+ let view, flag;
1708
+ let create = true;
1709
+ if (id in views) {
1710
+ view = views[id];
1711
+ if (view.model === cell) {
1712
+ flag = view.FLAG_INSERT;
1713
+ create = false;
1714
+ } else {
1715
+ // The view for this `id` already exist.
1716
+ // The cell is a new instance of the model with identical id
1717
+ // We simply remove the existing view and create a new one
1718
+ this.removeView(cell);
1719
+ }
1720
+ }
1721
+ if (create) {
1722
+ view = views[id] = this.createViewForModel(cell);
1723
+ view.paper = this;
1724
+ flag = this.registerUnmountedView(view) | this.FLAG_INIT | view.getFlag(result(view, 'initFlag'));
1725
+ }
1726
+ this.requestViewUpdate(view, flag, view.UPDATE_PRIORITY, opt);
1727
+ return view;
1728
+ },
1729
+
1730
+ onImageDragStart: function() {
1731
+ // This is the only way to prevent image dragging in Firefox that works.
1732
+ // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help.
1733
+
1734
+ return false;
1735
+ },
1736
+
1737
+ resetViews: function(cells, opt) {
1738
+ opt || (opt = {});
1739
+ cells || (cells = []);
1740
+ this._resetUpdates();
1741
+ // clearing views removes any event listeners
1742
+ this.removeViews();
1743
+ // Allows to unfreeze normally while in the idle state using autoFreeze option
1744
+ const key = this.options.autoFreeze ? null : 'reset';
1745
+ this.freeze({ key });
1746
+ for (var i = 0, n = cells.length; i < n; i++) {
1747
+ this.renderView(cells[i], opt);
1748
+ }
1749
+ this.unfreeze({ key });
1750
+ this.sortViews();
1751
+ },
1752
+
1753
+ removeViews: function() {
1754
+
1755
+ invoke(this._views, 'remove');
1756
+
1757
+ this._views = {};
1758
+ },
1759
+
1760
+ sortViews: function() {
1761
+
1762
+ if (!this.isExactSorting()) {
1763
+ // noop
1764
+ return;
1765
+ }
1766
+ if (this.isFrozen()) {
1767
+ // sort views once unfrozen
1768
+ this._updates.sort = true;
1769
+ return;
1770
+ }
1771
+ this.sortViewsExact();
1772
+ },
1773
+
1774
+ sortViewsExact: function() {
1775
+
1776
+ // Run insertion sort algorithm in order to efficiently sort DOM elements according to their
1777
+ // associated model `z` attribute.
1778
+
1779
+ var cellNodes = Array.from(this.cells.childNodes).filter(node => node.getAttribute('model-id'));
1780
+ var cells = this.model.get('cells');
1781
+
1782
+ sortElements(cellNodes, function(a, b) {
1783
+ var cellA = cells.get(a.getAttribute('model-id'));
1784
+ var cellB = cells.get(b.getAttribute('model-id'));
1785
+ var zA = cellA.attributes.z || 0;
1786
+ var zB = cellB.attributes.z || 0;
1787
+ return (zA === zB) ? 0 : (zA < zB) ? -1 : 1;
1788
+ });
1789
+ },
1790
+
1791
+ insertView: function(view, isInitialInsert) {
1792
+ const layerView = this.getLayerView(LayersNames.CELLS);
1793
+ const { el, model } = view;
1794
+ switch (this.options.sorting) {
1795
+ case sortingTypes.APPROX:
1796
+ layerView.insertSortedNode(el, model.get('z'));
1797
+ break;
1798
+ case sortingTypes.EXACT:
1799
+ default:
1800
+ layerView.insertNode(el);
1801
+ break;
1802
+ }
1803
+ view.onMount(isInitialInsert);
1804
+ },
1805
+
1806
+ detachView(view) {
1807
+ view.unmount();
1808
+ view.onDetach();
1809
+ },
1810
+
1811
+ // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also
1812
+ // be a selector or a jQuery object.
1813
+ findView: function($el) {
1814
+
1815
+ var el = isString($el)
1816
+ ? this.cells.querySelector($el)
1817
+ : $el instanceof $ ? $el[0] : $el;
1818
+
1819
+ var id = this.findAttribute('model-id', el);
1820
+ if (id) return this._views[id];
1821
+
1822
+ return undefined;
1823
+ },
1824
+
1825
+ // Find a view for a model `cell`. `cell` can also be a string or number representing a model `id`.
1826
+ findViewByModel: function(cell) {
1827
+
1828
+ var id = (isString(cell) || isNumber(cell)) ? cell : (cell && cell.id);
1829
+
1830
+ return this._views[id];
1831
+ },
1832
+
1833
+ // Find all views at given point
1834
+ findViewsFromPoint: function(p) {
1835
+
1836
+ p = new Point(p);
1837
+
1838
+ var views = this.model.getElements().map(this.findViewByModel, this);
1839
+
1840
+ return views.filter(function(view) {
1841
+ return view && view.vel.getBBox({ target: this.cells }).containsPoint(p);
1842
+ }, this);
1843
+ },
1844
+
1845
+ // Find all views in given area
1846
+ findViewsInArea: function(rect, opt) {
1847
+
1848
+ opt = defaults(opt || {}, { strict: false });
1849
+ rect = new Rect(rect);
1850
+
1851
+ var views = this.model.getElements().map(this.findViewByModel, this);
1852
+ var method = opt.strict ? 'containsRect' : 'intersect';
1853
+
1854
+ return views.filter(function(view) {
1855
+ return view && rect[method](view.vel.getBBox({ target: this.cells }));
1856
+ }, this);
1857
+ },
1858
+
1859
+ removeTools: function() {
1860
+ this.dispatchToolsEvent('remove');
1861
+ return this;
1862
+ },
1863
+
1864
+ hideTools: function() {
1865
+ this.dispatchToolsEvent('hide');
1866
+ return this;
1867
+ },
1868
+
1869
+ showTools: function() {
1870
+ this.dispatchToolsEvent('show');
1871
+ return this;
1872
+ },
1873
+
1874
+ dispatchToolsEvent: function(event, ...args) {
1875
+ if (typeof event !== 'string') return;
1876
+ this.trigger('tools:event', event, ...args);
1877
+ },
1878
+
1879
+
1880
+ getModelById: function(id) {
1881
+
1882
+ return this.model.getCell(id);
1883
+ },
1884
+
1885
+ snapToGrid: function(x, y) {
1886
+
1887
+ // Convert global coordinates to the local ones of the `viewport`. Otherwise,
1888
+ // improper transformation would be applied when the viewport gets transformed (scaled/rotated).
1889
+ return this.clientToLocalPoint(x, y).snapToGrid(this.options.gridSize);
1890
+ },
1891
+
1892
+ localToPaperPoint: function(x, y) {
1893
+ // allow `x` to be a point and `y` undefined
1894
+ var localPoint = new Point(x, y);
1895
+ var paperPoint = V.transformPoint(localPoint, this.matrix());
1896
+ return paperPoint;
1897
+ },
1898
+
1899
+ localToPaperRect: function(x, y, width, height) {
1900
+ // allow `x` to be a rectangle and rest arguments undefined
1901
+ var localRect = new Rect(x, y, width, height);
1902
+ var paperRect = V.transformRect(localRect, this.matrix());
1903
+ return paperRect;
1904
+ },
1905
+
1906
+ paperToLocalPoint: function(x, y) {
1907
+ // allow `x` to be a point and `y` undefined
1908
+ var paperPoint = new Point(x, y);
1909
+ var localPoint = V.transformPoint(paperPoint, this.matrix().inverse());
1910
+ return localPoint;
1911
+ },
1912
+
1913
+ paperToLocalRect: function(x, y, width, height) {
1914
+ // allow `x` to be a rectangle and rest arguments undefined
1915
+ var paperRect = new Rect(x, y, width, height);
1916
+ var localRect = V.transformRect(paperRect, this.matrix().inverse());
1917
+ return localRect;
1918
+ },
1919
+
1920
+ localToClientPoint: function(x, y) {
1921
+ // allow `x` to be a point and `y` undefined
1922
+ var localPoint = new Point(x, y);
1923
+ var clientPoint = V.transformPoint(localPoint, this.clientMatrix());
1924
+ return clientPoint;
1925
+ },
1926
+
1927
+ localToClientRect: function(x, y, width, height) {
1928
+ // allow `x` to be a point and `y` undefined
1929
+ var localRect = new Rect(x, y, width, height);
1930
+ var clientRect = V.transformRect(localRect, this.clientMatrix());
1931
+ return clientRect;
1932
+ },
1933
+
1934
+ // Transform client coordinates to the paper local coordinates.
1935
+ // Useful when you have a mouse event object and you'd like to get coordinates
1936
+ // inside the paper that correspond to `evt.clientX` and `evt.clientY` point.
1937
+ // Example: var localPoint = paper.clientToLocalPoint({ x: evt.clientX, y: evt.clientY });
1938
+ clientToLocalPoint: function(x, y) {
1939
+ // allow `x` to be a point and `y` undefined
1940
+ var clientPoint = new Point(x, y);
1941
+ var localPoint = V.transformPoint(clientPoint, this.clientMatrix().inverse());
1942
+ return localPoint;
1943
+ },
1944
+
1945
+ clientToLocalRect: function(x, y, width, height) {
1946
+ // allow `x` to be a point and `y` undefined
1947
+ var clientRect = new Rect(x, y, width, height);
1948
+ var localRect = V.transformRect(clientRect, this.clientMatrix().inverse());
1949
+ return localRect;
1950
+ },
1951
+
1952
+ localToPagePoint: function(x, y) {
1953
+
1954
+ return this.localToPaperPoint(x, y).offset(this.pageOffset());
1955
+ },
1956
+
1957
+ localToPageRect: function(x, y, width, height) {
1958
+
1959
+ return this.localToPaperRect(x, y, width, height).offset(this.pageOffset());
1960
+ },
1961
+
1962
+ pageToLocalPoint: function(x, y) {
1963
+
1964
+ var pagePoint = new Point(x, y);
1965
+ var paperPoint = pagePoint.difference(this.pageOffset());
1966
+ return this.paperToLocalPoint(paperPoint);
1967
+ },
1968
+
1969
+ pageToLocalRect: function(x, y, width, height) {
1970
+
1971
+ var pageOffset = this.pageOffset();
1972
+ var paperRect = new Rect(x, y, width, height);
1973
+ paperRect.x -= pageOffset.x;
1974
+ paperRect.y -= pageOffset.y;
1975
+ return this.paperToLocalRect(paperRect);
1976
+ },
1977
+
1978
+ clientOffset: function() {
1979
+
1980
+ var clientRect = this.svg.getBoundingClientRect();
1981
+ return new Point(clientRect.left, clientRect.top);
1982
+ },
1983
+
1984
+ pageOffset: function() {
1985
+
1986
+ return this.clientOffset().offset(window.scrollX, window.scrollY);
1987
+ },
1988
+
1989
+ linkAllowed: function(linkView) {
1990
+
1991
+ if (!(linkView instanceof LinkView)) {
1992
+ throw new Error('Must provide a linkView.');
1993
+ }
1994
+
1995
+ var link = linkView.model;
1996
+ var paperOptions = this.options;
1997
+ var graph = this.model;
1998
+ var ns = graph.constructor.validations;
1999
+
2000
+ if (!paperOptions.multiLinks) {
2001
+ if (!ns.multiLinks.call(this, graph, link)) return false;
2002
+ }
2003
+
2004
+ if (!paperOptions.linkPinning) {
2005
+ // Link pinning is not allowed and the link is not connected to the target.
2006
+ if (!ns.linkPinning.call(this, graph, link)) return false;
2007
+ }
2008
+
2009
+ if (typeof paperOptions.allowLink === 'function') {
2010
+ if (!paperOptions.allowLink.call(this, linkView, this)) return false;
2011
+ }
2012
+
2013
+ return true;
2014
+ },
2015
+
2016
+ getDefaultLink: function(cellView, magnet) {
2017
+
2018
+ return isFunction(this.options.defaultLink)
2019
+ // default link is a function producing link model
2020
+ ? this.options.defaultLink.call(this, cellView, magnet)
2021
+ // default link is the mvc model
2022
+ : this.options.defaultLink.clone();
2023
+ },
2024
+
2025
+ // Cell highlighting.
2026
+ // ------------------
2027
+
2028
+ resolveHighlighter: function(opt = {}) {
2029
+
2030
+ let { highlighter: highlighterDef, type } = opt;
2031
+ const { highlighting,highlighterNamespace } = this.options;
2032
+
2033
+ /*
2034
+ Expecting opt.highlighter to have the following structure:
2035
+ {
2036
+ name: 'highlighter-name',
2037
+ options: {
2038
+ some: 'value'
2039
+ }
2040
+ }
2041
+ */
2042
+ if (highlighterDef === undefined) {
2043
+
2044
+ // Is highlighting disabled?
2045
+ if (!highlighting) return false;
2046
+ // check for built-in types
2047
+ if (type) {
2048
+ highlighterDef = highlighting[type];
2049
+ // Is a specific type highlight disabled?
2050
+ if (highlighterDef === false) return false;
2051
+ }
2052
+ if (!highlighterDef) {
2053
+ // Type not defined use default highlight
2054
+ highlighterDef = highlighting['default'];
2055
+ }
2056
+ }
2057
+
2058
+ // Do nothing if opt.highlighter is falsy.
2059
+ // This allows the case to not highlight cell(s) in certain cases.
2060
+ // For example, if you want to NOT highlight when embedding elements
2061
+ // or use a custom highlighter.
2062
+ if (!highlighterDef) return false;
2063
+
2064
+ // Allow specifying a highlighter by name.
2065
+ if (isString(highlighterDef)) {
2066
+ highlighterDef = {
2067
+ name: highlighterDef
2068
+ };
2069
+ }
2070
+
2071
+ const name = highlighterDef.name;
2072
+ const highlighter = highlighterNamespace[name];
2073
+
2074
+ // Highlighter validation
2075
+ if (!highlighter) {
2076
+ throw new Error('Unknown highlighter ("' + name + '")');
2077
+ }
2078
+ if (typeof highlighter.highlight !== 'function') {
2079
+ throw new Error('Highlighter ("' + name + '") is missing required highlight() method');
2080
+ }
2081
+ if (typeof highlighter.unhighlight !== 'function') {
2082
+ throw new Error('Highlighter ("' + name + '") is missing required unhighlight() method');
2083
+ }
2084
+
2085
+ return {
2086
+ highlighter,
2087
+ options: highlighterDef.options || {},
2088
+ name
2089
+ };
2090
+ },
2091
+
2092
+ onCellHighlight: function(cellView, magnetEl, opt) {
2093
+ const highlighterDescriptor = this.resolveHighlighter(opt);
2094
+ if (!highlighterDescriptor) return;
2095
+ const { highlighter, options } = highlighterDescriptor;
2096
+ highlighter.highlight(cellView, magnetEl, options);
2097
+ },
2098
+
2099
+ onCellUnhighlight: function(cellView, magnetEl, opt) {
2100
+ const highlighterDescriptor = this.resolveHighlighter(opt);
2101
+ if (!highlighterDescriptor) return;
2102
+ const { highlighter, options } = highlighterDescriptor;
2103
+ highlighter.unhighlight(cellView, magnetEl, options);
2104
+ },
2105
+
2106
+ // Interaction.
2107
+ // ------------
2108
+
2109
+ pointerdblclick: function(evt) {
2110
+
2111
+ evt.preventDefault();
2112
+
2113
+ // magnetpointerdblclick can stop propagation
2114
+
2115
+ evt = normalizeEvent(evt);
2116
+
2117
+ var view = this.findView(evt.target);
2118
+ if (this.guard(evt, view)) return;
2119
+
2120
+ var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2121
+
2122
+ if (view) {
2123
+ view.pointerdblclick(evt, localPoint.x, localPoint.y);
2124
+
2125
+ } else {
2126
+ this.trigger('blank:pointerdblclick', evt, localPoint.x, localPoint.y);
2127
+ }
2128
+ },
2129
+
2130
+ pointerclick: function(evt) {
2131
+
2132
+ // magnetpointerclick can stop propagation
2133
+
2134
+ var data = this.eventData(evt);
2135
+ // Trigger event only if mouse has not moved.
2136
+ if (data.mousemoved <= this.options.clickThreshold) {
2137
+
2138
+ evt = normalizeEvent(evt);
2139
+
2140
+ var view = this.findView(evt.target);
2141
+ if (this.guard(evt, view)) return;
2142
+
2143
+ var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2144
+
2145
+ if (view) {
2146
+ view.pointerclick(evt, localPoint.x, localPoint.y);
2147
+
2148
+ } else {
2149
+ this.trigger('blank:pointerclick', evt, localPoint.x, localPoint.y);
2150
+ }
2151
+ }
2152
+ },
2153
+
2154
+ contextmenu: function(evt) {
2155
+
2156
+ if (this.options.preventContextMenu) evt.preventDefault();
2157
+
2158
+ if (this.contextMenuFired) {
2159
+ this.contextMenuFired = false;
2160
+ return;
2161
+ }
2162
+
2163
+ evt = normalizeEvent(evt);
2164
+
2165
+ this.contextMenuTrigger(evt);
2166
+ },
2167
+
2168
+ contextMenuTrigger: function(evt) {
2169
+ var view = this.findView(evt.target);
2170
+ if (this.guard(evt, view)) return;
2171
+
2172
+ var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2173
+
2174
+ if (view) {
2175
+ view.contextmenu(evt, localPoint.x, localPoint.y);
2176
+
2177
+ } else {
2178
+ this.trigger('blank:contextmenu', evt, localPoint.x, localPoint.y);
2179
+ }
2180
+ },
2181
+
2182
+ pointerdown: function(evt) {
2183
+
2184
+ evt = normalizeEvent(evt);
2185
+
2186
+ const { target, button } = evt;
2187
+ const view = this.findView(target);
2188
+ const isContextMenu = (button === 2);
2189
+
2190
+ if (view) {
2191
+
2192
+ if (!isContextMenu && this.guard(evt, view)) return;
2193
+
2194
+ const isTargetFormNode = this.FORM_CONTROL_TAG_NAMES.includes(target.tagName);
2195
+
2196
+ if (this.options.preventDefaultViewAction && !isTargetFormNode) {
2197
+ // If the target is a form element, we do not want to prevent the default action.
2198
+ // For example, we want to be able to select text in a text input or
2199
+ // to be able to click on a checkbox.
2200
+ evt.preventDefault();
2201
+ }
2202
+
2203
+ if (isTargetFormNode) {
2204
+ // If the target is a form element, we do not want to start dragging the element.
2205
+ // For example, we want to be able to select text by dragging the mouse.
2206
+ view.preventDefaultInteraction(evt);
2207
+ }
2208
+
2209
+ // Custom event
2210
+ const eventEvt = this.customEventTrigger(evt, view);
2211
+ if (eventEvt) {
2212
+ // `onevent` could have stopped propagation
2213
+ if (eventEvt.isPropagationStopped()) return;
2214
+
2215
+ evt.data = eventEvt.data;
2216
+ }
2217
+
2218
+ // Element magnet
2219
+ const magnetNode = target.closest('[magnet]');
2220
+ if (magnetNode && view.el !== magnetNode && view.el.contains(magnetNode)) {
2221
+ const magnetEvt = normalizeEvent(new $.Event(evt.originalEvent, {
2222
+ data: evt.data,
2223
+ // Originally the event listener was attached to the magnet element.
2224
+ currentTarget: magnetNode
2225
+ }));
2226
+ this.onmagnet(magnetEvt);
2227
+ if (magnetEvt.isDefaultPrevented()) {
2228
+ evt.preventDefault();
2229
+ }
2230
+ // `onmagnet` stops propagation when `addLinkFromMagnet` is allowed
2231
+ if (magnetEvt.isPropagationStopped()) {
2232
+ // `magnet:pointermove` and `magnet:pointerup` events must be fired
2233
+ if (isContextMenu) return;
2234
+ this.delegateDragEvents(view, magnetEvt.data);
2235
+ return;
2236
+ }
2237
+ evt.data = magnetEvt.data;
2238
+ }
2239
+ }
2240
+
2241
+ if (isContextMenu) {
2242
+ this.contextMenuFired = true;
2243
+ const contextmenuEvt = new $.Event(evt.originalEvent, { type: 'contextmenu', data: evt.data });
2244
+ this.contextMenuTrigger(contextmenuEvt);
2245
+ } else {
2246
+ const localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2247
+ if (view) {
2248
+ view.pointerdown(evt, localPoint.x, localPoint.y);
2249
+ } else {
2250
+ if (this.options.preventDefaultBlankAction) {
2251
+ evt.preventDefault();
2252
+ }
2253
+ this.trigger('blank:pointerdown', evt, localPoint.x, localPoint.y);
2254
+ }
2255
+
2256
+ this.delegateDragEvents(view, evt.data);
2257
+ }
2258
+
2259
+ },
2260
+
2261
+ pointermove: function(evt) {
2262
+
2263
+ // mouse moved counter
2264
+ var data = this.eventData(evt);
2265
+ if (!data.mousemoved) {
2266
+ data.mousemoved = 0;
2267
+ // Make sure that events like `mouseenter` and `mouseleave` are
2268
+ // not triggered while the user is dragging a cellView.
2269
+ this.undelegateEvents();
2270
+ // Note: the events are undelegated after the first `pointermove` event.
2271
+ // Not on `pointerdown` to make sure that `dbltap` is recognized.
2272
+ }
2273
+
2274
+ var mousemoved = ++data.mousemoved;
2275
+
2276
+ if (mousemoved <= this.options.moveThreshold) return;
2277
+
2278
+ evt = normalizeEvent(evt);
2279
+
2280
+ var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2281
+
2282
+ var view = data.sourceView;
2283
+ if (view) {
2284
+ view.pointermove(evt, localPoint.x, localPoint.y);
2285
+ } else {
2286
+ this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y);
2287
+ }
2288
+
2289
+ this.eventData(evt, data);
2290
+ },
2291
+
2292
+ pointerup: function(evt) {
2293
+
2294
+ this.undelegateDocumentEvents();
2295
+
2296
+ var normalizedEvt = normalizeEvent(evt);
2297
+
2298
+ var localPoint = this.snapToGrid(normalizedEvt.clientX, normalizedEvt.clientY);
2299
+
2300
+ var view = this.eventData(evt).sourceView;
2301
+ if (view) {
2302
+ view.pointerup(normalizedEvt, localPoint.x, localPoint.y);
2303
+ } else {
2304
+ this.trigger('blank:pointerup', normalizedEvt, localPoint.x, localPoint.y);
2305
+ }
2306
+
2307
+ if (!normalizedEvt.isPropagationStopped()) {
2308
+ this.pointerclick(new $.Event(evt.originalEvent, { type: 'click', data: evt.data }));
2309
+ }
2310
+
2311
+ this.delegateEvents();
2312
+ },
2313
+
2314
+ mouseover: function(evt) {
2315
+
2316
+ evt = normalizeEvent(evt);
2317
+
2318
+ var view = this.findView(evt.target);
2319
+ if (this.guard(evt, view)) return;
2320
+
2321
+ if (view) {
2322
+ view.mouseover(evt);
2323
+
2324
+ } else {
2325
+ if (this.el === evt.target) return; // prevent border of paper from triggering this
2326
+ this.trigger('blank:mouseover', evt);
2327
+ }
2328
+ },
2329
+
2330
+ mouseout: function(evt) {
2331
+
2332
+ evt = normalizeEvent(evt);
2333
+
2334
+ var view = this.findView(evt.target);
2335
+ if (this.guard(evt, view)) return;
2336
+
2337
+ if (view) {
2338
+ view.mouseout(evt);
2339
+
2340
+ } else {
2341
+ if (this.el === evt.target) return; // prevent border of paper from triggering this
2342
+ this.trigger('blank:mouseout', evt);
2343
+ }
2344
+ },
2345
+
2346
+ mouseenter: function(evt) {
2347
+
2348
+ evt = normalizeEvent(evt);
2349
+
2350
+ const {
2351
+ target, // The EventTarget the pointing device entered to
2352
+ relatedTarget, // The EventTarget the pointing device exited from
2353
+ currentTarget // The EventTarget on which the event listener was registered
2354
+ } = evt;
2355
+ const view = this.findView(target);
2356
+ if (this.guard(evt, view)) return;
2357
+ const relatedView = this.findView(relatedTarget);
2358
+ if (view) {
2359
+ if (relatedView === view) {
2360
+ // Mouse left a cell tool
2361
+ return;
2362
+ }
2363
+ view.mouseenter(evt);
2364
+ if (this.el.contains(relatedTarget)) {
2365
+ // The pointer remains inside the paper.
2366
+ return;
2367
+ }
2368
+ }
2369
+ if (relatedView) {
2370
+ return;
2371
+ }
2372
+ // prevent double `mouseenter` event if the `relatedTarget` is outside the paper
2373
+ // (mouseenter method would be fired twice)
2374
+ if (currentTarget === this.el) {
2375
+ // `paper` (more descriptive), not `blank`
2376
+ this.trigger('paper:mouseenter', evt);
2377
+ }
2378
+ },
2379
+
2380
+ mouseleave: function(evt) {
2381
+
2382
+ evt = normalizeEvent(evt);
2383
+
2384
+ const {
2385
+ target, // The EventTarget the pointing device exited from
2386
+ relatedTarget, // The EventTarget the pointing device entered to
2387
+ currentTarget // The EventTarget on which the event listener was registered
2388
+ } = evt;
2389
+ const view = this.findView(target);
2390
+ if (this.guard(evt, view)) return;
2391
+ const relatedView = this.findView(relatedTarget);
2392
+ if (view) {
2393
+ if (relatedView === view) {
2394
+ // Mouse entered a cell tool
2395
+ return;
2396
+ }
2397
+ view.mouseleave(evt);
2398
+ if (this.el.contains(relatedTarget)) {
2399
+ // The pointer has exited a cellView. The pointer is still inside of the paper.
2400
+ return;
2401
+ }
2402
+ }
2403
+ if (relatedView) {
2404
+ // The pointer has entered a new cellView
2405
+ return;
2406
+ }
2407
+ // prevent double `mouseleave` event if the `relatedTarget` is outside the paper
2408
+ // (mouseleave method would be fired twice)
2409
+ if (currentTarget === this.el) {
2410
+ // There is no cellView under the pointer, nor the blank area of the paper
2411
+ this.trigger('paper:mouseleave', evt);
2412
+ }
2413
+ },
2414
+
2415
+ _processMouseWheelEvtBuf: debounce(function() {
2416
+ const { event, deltas } = this._mw_evt_buffer;
2417
+ const deltaY = deltas.reduce((acc, deltaY) => acc + cap(deltaY, WHEEL_CAP), 0);
2418
+
2419
+ const scale = Math.pow(0.995, deltaY); // 1.005 for inverted pinch/zoom
2420
+ const { x, y } = this.clientToLocalPoint(event.clientX, event.clientY);
2421
+ this.trigger('paper:pinch', event, x, y, scale);
2422
+
2423
+ this._mw_evt_buffer = {
2424
+ event: null,
2425
+ deltas: [],
2426
+ };
2427
+ }, WHEEL_WAIT_MS, { maxWait: WHEEL_WAIT_MS }),
2428
+
2429
+ mousewheel: function(evt) {
2430
+
2431
+ evt = normalizeEvent(evt);
2432
+
2433
+ const view = this.findView(evt.target);
2434
+ if (this.guard(evt, view)) return;
2435
+
2436
+ const originalEvent = evt.originalEvent;
2437
+ const localPoint = this.snapToGrid(originalEvent.clientX, originalEvent.clientY);
2438
+ const { deltaX, deltaY } = normalizeWheel(originalEvent);
2439
+
2440
+ const pinchHandlers = this._events['paper:pinch'];
2441
+
2442
+ // Touchpad devices will send a fake CTRL press when a pinch is performed
2443
+ //
2444
+ // We also check if there are any subscribers to paper:pinch event. If there are none,
2445
+ // just skip the entire block of code (we don't want to blindly call
2446
+ // .preventDefault() if we really don't have to).
2447
+ if (evt.ctrlKey && pinchHandlers && pinchHandlers.length > 0) {
2448
+ // This is a pinch gesture, it's safe to assume that we must call .preventDefault()
2449
+ originalEvent.preventDefault();
2450
+ this._mw_evt_buffer.event = evt;
2451
+ this._mw_evt_buffer.deltas.push(deltaY);
2452
+ this._processMouseWheelEvtBuf();
2453
+ } else {
2454
+ const delta = Math.max(-1, Math.min(1, originalEvent.wheelDelta));
2455
+ if (view) {
2456
+ view.mousewheel(evt, localPoint.x, localPoint.y, delta);
2457
+
2458
+ } else {
2459
+ this.trigger('blank:mousewheel', evt, localPoint.x, localPoint.y, delta);
2460
+ }
2461
+
2462
+ this.trigger('paper:pan', evt, deltaX, deltaY);
2463
+ }
2464
+ },
2465
+
2466
+ onevent: function(evt) {
2467
+
2468
+ var eventNode = evt.currentTarget;
2469
+ var eventName = eventNode.getAttribute('event');
2470
+ if (eventName) {
2471
+ var view = this.findView(eventNode);
2472
+ if (view) {
2473
+
2474
+ evt = normalizeEvent(evt);
2475
+ if (this.guard(evt, view)) return;
2476
+
2477
+ var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2478
+ view.onevent(evt, eventName, localPoint.x, localPoint.y);
2479
+ }
2480
+ }
2481
+ },
2482
+
2483
+ magnetEvent: function(evt, handler) {
2484
+
2485
+ var magnetNode = evt.currentTarget;
2486
+ var magnetValue = magnetNode.getAttribute('magnet');
2487
+ if (magnetValue) {
2488
+ var view = this.findView(magnetNode);
2489
+ if (view) {
2490
+ evt = normalizeEvent(evt);
2491
+ if (this.guard(evt, view)) return;
2492
+ var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2493
+ handler.call(this, view, evt, magnetNode, localPoint.x, localPoint.y);
2494
+ }
2495
+ }
2496
+ },
2497
+
2498
+ onmagnet: function(evt) {
2499
+
2500
+ if (evt.button === 2) {
2501
+ this.contextMenuFired = true;
2502
+ this.magnetContextMenuFired = true;
2503
+ const contextmenuEvt = new $.Event(evt.originalEvent, {
2504
+ type: 'contextmenu',
2505
+ data: evt.data,
2506
+ currentTarget: evt.currentTarget,
2507
+ });
2508
+ this.magnetContextMenuTrigger(contextmenuEvt);
2509
+ if (contextmenuEvt.isPropagationStopped()) {
2510
+ evt.stopPropagation();
2511
+ }
2512
+ } else {
2513
+ this.magnetEvent(evt, function(view, evt, _, x, y) {
2514
+ view.onmagnet(evt, x, y);
2515
+ });
2516
+ }
2517
+ },
2518
+
2519
+ magnetpointerdblclick: function(evt) {
2520
+
2521
+ this.magnetEvent(evt, function(view, evt, magnet, x, y) {
2522
+ view.magnetpointerdblclick(evt, magnet, x, y);
2523
+ });
2524
+ },
2525
+
2526
+ magnetcontextmenu: function(evt) {
2527
+ if (this.options.preventContextMenu) evt.preventDefault();
2528
+
2529
+ if (this.magnetContextMenuFired) {
2530
+ this.magnetContextMenuFired = false;
2531
+ return;
2532
+ }
2533
+
2534
+ this.magnetContextMenuTrigger(evt);
2535
+ },
2536
+
2537
+ magnetContextMenuTrigger: function(evt) {
2538
+ this.magnetEvent(evt, function(view, evt, magnet, x, y) {
2539
+ view.magnetcontextmenu(evt, magnet, x, y);
2540
+ });
2541
+ },
2542
+
2543
+ onlabel: function(evt) {
2544
+
2545
+ var labelNode = evt.currentTarget;
2546
+
2547
+ var view = this.findView(labelNode);
2548
+ if (!view) return;
2549
+
2550
+ evt = normalizeEvent(evt);
2551
+ if (this.guard(evt, view)) return;
2552
+
2553
+ // Custom event
2554
+ const eventEvt = this.customEventTrigger(evt, view, labelNode);
2555
+ if (eventEvt) {
2556
+ // `onevent` could have stopped propagation
2557
+ if (eventEvt.isPropagationStopped()) return;
2558
+
2559
+ evt.data = eventEvt.data;
2560
+ }
2561
+
2562
+ var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2563
+ view.onlabel(evt, localPoint.x, localPoint.y);
2564
+ },
2565
+
2566
+ getPointerArgs(evt) {
2567
+ const normalizedEvt = normalizeEvent(evt);
2568
+ const { x, y } = this.snapToGrid(normalizedEvt.clientX, normalizedEvt.clientY);
2569
+ return [normalizedEvt, x, y];
2570
+ },
2571
+
2572
+ delegateDragEvents: function(view, data) {
2573
+
2574
+ data || (data = {});
2575
+ this.eventData({ data: data }, { sourceView: view || null, mousemoved: 0 });
2576
+ this.delegateDocumentEvents(null, data);
2577
+ },
2578
+
2579
+ // Guard the specified event. If the event should be ignored, guard returns `true`.
2580
+ // Otherwise, it returns `false`.
2581
+ guard: function(evt, view) {
2582
+
2583
+ if (evt.type === 'mousedown' && evt.button === 2) {
2584
+ // handled as `contextmenu` type
2585
+ return true;
2586
+ }
2587
+
2588
+ if (this.options.guard && this.options.guard(evt, view)) {
2589
+ return true;
2590
+ }
2591
+
2592
+ if (evt.data && evt.data.guarded !== undefined) {
2593
+ return evt.data.guarded;
2594
+ }
2595
+
2596
+ const { target } = evt;
2597
+
2598
+ if (this.GUARDED_TAG_NAMES.includes(target.tagName)) {
2599
+ return true;
2600
+ }
2601
+
2602
+ if (view && view.model && (view.model instanceof Cell)) {
2603
+ return false;
2604
+ }
2605
+
2606
+ if (this.el === target || this.svg.contains(target)) {
2607
+ return false;
2608
+ }
2609
+
2610
+ return true; // Event guarded. Paper should not react on it in any way.
2611
+ },
2612
+
2613
+ setGridSize: function(gridSize) {
2614
+ const { options } = this;
2615
+ options.gridSize = gridSize;
2616
+ if (options.drawGrid && !options.drawGridSize) {
2617
+ // Do not redraw the grid if the `drawGridSize` is set.
2618
+ this.getLayerView(LayersNames.GRID).renderGrid();
2619
+ }
2620
+ return this;
2621
+ },
2622
+
2623
+ setGrid: function(drawGrid) {
2624
+ this.getLayerView(LayersNames.GRID).setGrid(drawGrid);
2625
+ return this;
2626
+ },
2627
+
2628
+ updateBackgroundImage: function(opt) {
2629
+
2630
+ opt = opt || {};
2631
+
2632
+ var backgroundPosition = opt.position || 'center';
2633
+ var backgroundSize = opt.size || 'auto auto';
2634
+
2635
+ var currentScale = this.scale();
2636
+ var currentTranslate = this.translate();
2637
+
2638
+ // backgroundPosition
2639
+ if (isObject(backgroundPosition)) {
2640
+ var x = currentTranslate.tx + (currentScale.sx * (backgroundPosition.x || 0));
2641
+ var y = currentTranslate.ty + (currentScale.sy * (backgroundPosition.y || 0));
2642
+ backgroundPosition = x + 'px ' + y + 'px';
2643
+ }
2644
+
2645
+ // backgroundSize
2646
+ if (isObject(backgroundSize)) {
2647
+ backgroundSize = new Rect(backgroundSize).scale(currentScale.sx, currentScale.sy);
2648
+ backgroundSize = backgroundSize.width + 'px ' + backgroundSize.height + 'px';
2649
+ }
2650
+
2651
+ const { background } = this.childNodes;
2652
+ background.style.backgroundSize = backgroundSize;
2653
+ background.style.backgroundPosition = backgroundPosition;
2654
+ },
2655
+
2656
+ drawBackgroundImage: function(img, opt) {
2657
+
2658
+ // Clear the background image if no image provided
2659
+ if (!(img instanceof HTMLImageElement)) {
2660
+ this.childNodes.background.style.backgroundImage = '';
2661
+ return;
2662
+ }
2663
+
2664
+ if (!this._background || this._background.id !== opt.id) {
2665
+ // Draw only the last image requested (see drawBackground())
2666
+ return;
2667
+ }
2668
+
2669
+ opt = opt || {};
2670
+
2671
+ var backgroundImage;
2672
+ var backgroundSize = opt.size;
2673
+ var backgroundRepeat = opt.repeat || 'no-repeat';
2674
+ var backgroundOpacity = opt.opacity || 1;
2675
+ var backgroundQuality = Math.abs(opt.quality) || 1;
2676
+ var backgroundPattern = this.constructor.backgroundPatterns[camelCase(backgroundRepeat)];
2677
+
2678
+ if (isFunction(backgroundPattern)) {
2679
+ // 'flip-x', 'flip-y', 'flip-xy', 'watermark' and custom
2680
+ img.width *= backgroundQuality;
2681
+ img.height *= backgroundQuality;
2682
+ var canvas = backgroundPattern(img, opt);
2683
+ if (!(canvas instanceof HTMLCanvasElement)) {
2684
+ throw new Error('dia.Paper: background pattern must return an HTML Canvas instance');
2685
+ }
2686
+
2687
+ backgroundImage = canvas.toDataURL('image/png');
2688
+ backgroundRepeat = 'repeat';
2689
+ if (isObject(backgroundSize)) {
2690
+ // recalculate the tile size if an object passed in
2691
+ backgroundSize.width *= canvas.width / img.width;
2692
+ backgroundSize.height *= canvas.height / img.height;
2693
+ } else if (backgroundSize === undefined) {
2694
+ // calculate the tile size if no provided
2695
+ opt.size = {
2696
+ width: canvas.width / backgroundQuality,
2697
+ height: canvas.height / backgroundQuality
2698
+ };
2699
+ }
2700
+ } else {
2701
+ // backgroundRepeat:
2702
+ // no-repeat', 'round', 'space', 'repeat', 'repeat-x', 'repeat-y'
2703
+ backgroundImage = img.src;
2704
+ if (backgroundSize === undefined) {
2705
+ // pass the image size for the backgroundSize if no size provided
2706
+ opt.size = {
2707
+ width: img.width,
2708
+ height: img.height
2709
+ };
2710
+ }
2711
+ }
2712
+
2713
+ this.childNodes.background.style.opacity = backgroundOpacity;
2714
+ this.childNodes.background.style.backgroundRepeat = backgroundRepeat;
2715
+ this.childNodes.background.style.backgroundImage = `url(${backgroundImage})`;
2716
+
2717
+ this.updateBackgroundImage(opt);
2718
+ },
2719
+
2720
+ updateBackgroundColor: function(color) {
2721
+
2722
+ this.$el.css('backgroundColor', color || '');
2723
+ },
2724
+
2725
+ drawBackground: function(opt) {
2726
+
2727
+ opt = opt || {};
2728
+
2729
+ this.updateBackgroundColor(opt.color);
2730
+
2731
+ if (opt.image) {
2732
+ opt = this._background = cloneDeep(opt);
2733
+ guid(opt);
2734
+ var img = document.createElement('img');
2735
+ img.onload = this.drawBackgroundImage.bind(this, img, opt);
2736
+ img.src = opt.image;
2737
+ } else {
2738
+ this.drawBackgroundImage(null);
2739
+ this._background = null;
2740
+ }
2741
+
2742
+ return this;
2743
+ },
2744
+
2745
+ setInteractivity: function(value) {
2746
+
2747
+ this.options.interactive = value;
2748
+
2749
+ invoke(this._views, 'setInteractivity', value);
2750
+ },
2751
+
2752
+ // Paper definitions.
2753
+ // ------------------
2754
+
2755
+ isDefined: function(defId) {
2756
+
2757
+ return !!this.svg.getElementById(defId);
2758
+ },
2759
+
2760
+ defineFilter: function(filter) {
2761
+
2762
+ if (!isObject(filter)) {
2763
+ throw new TypeError('dia.Paper: defineFilter() requires 1. argument to be an object.');
2764
+ }
2765
+
2766
+ var filterId = filter.id;
2767
+ var name = filter.name;
2768
+ // Generate a hash code from the stringified filter definition. This gives us
2769
+ // a unique filter ID for different definitions.
2770
+ if (!filterId) {
2771
+ filterId = name + this.svg.id + hashCode(JSON.stringify(filter));
2772
+ }
2773
+ // If the filter already exists in the document,
2774
+ // we're done and we can just use it (reference it using `url()`).
2775
+ // If not, create one.
2776
+ if (!this.isDefined(filterId)) {
2777
+
2778
+ var namespace = _filter;
2779
+ var filterSVGString = namespace[name] && namespace[name](filter.args || {});
2780
+ if (!filterSVGString) {
2781
+ throw new Error('Non-existing filter ' + name);
2782
+ }
2783
+
2784
+ // SVG <filter/> attributes
2785
+ var filterAttrs = assign({
2786
+ filterUnits: 'userSpaceOnUse',
2787
+ }, filter.attrs, {
2788
+ id: filterId
2789
+ });
2790
+
2791
+ V(filterSVGString, filterAttrs).appendTo(this.defs);
2792
+ }
2793
+
2794
+ return filterId;
2795
+ },
2796
+
2797
+ defineGradient: function(gradient) {
2798
+ if (!isObject(gradient)) {
2799
+ throw new TypeError('dia.Paper: defineGradient() requires 1. argument to be an object.');
2800
+ }
2801
+ const { svg, defs } = this;
2802
+ const {
2803
+ type,
2804
+ // Generate a hash code from the stringified filter definition. This gives us
2805
+ // a unique filter ID for different definitions.
2806
+ id = type + svg.id + hashCode(JSON.stringify(gradient)),
2807
+ stops,
2808
+ attrs = {}
2809
+ } = gradient;
2810
+ // If the gradient already exists in the document,
2811
+ // we're done and we can just use it (reference it using `url()`).
2812
+ if (this.isDefined(id)) return id;
2813
+ // If not, create one.
2814
+ const stopVEls = toArray(stops).map(({ offset, color, opacity }) => {
2815
+ return V('stop').attr({
2816
+ 'offset': offset,
2817
+ 'stop-color': color,
2818
+ 'stop-opacity': Number.isFinite(opacity) ? opacity : 1
2819
+ });
2820
+ });
2821
+ const gradientVEl = V(type, attrs, stopVEls);
2822
+ gradientVEl.id = id;
2823
+ gradientVEl.appendTo(defs);
2824
+ return id;
2825
+ },
2826
+
2827
+ definePattern: function(pattern) {
2828
+ if (!isObject(pattern)) {
2829
+ throw new TypeError('dia.Paper: definePattern() requires 1. argument to be an object.');
2830
+ }
2831
+ const { svg, defs } = this;
2832
+ const {
2833
+ // Generate a hash code from the stringified filter definition. This gives us
2834
+ // a unique filter ID for different definitions.
2835
+ id = svg.id + hashCode(JSON.stringify(pattern)),
2836
+ markup,
2837
+ attrs = {}
2838
+ } = pattern;
2839
+ if (!markup) {
2840
+ throw new TypeError('dia.Paper: definePattern() requires markup.');
2841
+ }
2842
+ // If the gradient already exists in the document,
2843
+ // we're done and we can just use it (reference it using `url()`).
2844
+ if (this.isDefined(id)) return id;
2845
+ // If not, create one.
2846
+ const patternVEl = V('pattern', {
2847
+ patternUnits: 'userSpaceOnUse'
2848
+ });
2849
+ patternVEl.id = id;
2850
+ patternVEl.attr(attrs);
2851
+ if (typeof markup === 'string') {
2852
+ patternVEl.append(V(markup));
2853
+ } else {
2854
+ const { fragment } = parseDOMJSON(markup);
2855
+ patternVEl.append(fragment);
2856
+ }
2857
+ patternVEl.appendTo(defs);
2858
+ return id;
2859
+ },
2860
+
2861
+ defineMarker: function(marker) {
2862
+ if (!isObject(marker)) {
2863
+ throw new TypeError('dia.Paper: defineMarker() requires the first argument to be an object.');
2864
+ }
2865
+ const { svg, defs } = this;
2866
+ const {
2867
+ // Generate a hash code from the stringified filter definition. This gives us
2868
+ // a unique filter ID for different definitions.
2869
+ id = svg.id + hashCode(JSON.stringify(marker)),
2870
+ // user-provided markup
2871
+ // (e.g. defined when creating link via `attrs/line/sourceMarker/markup`)
2872
+ markup,
2873
+ // user-provided attributes
2874
+ // (e.g. defined when creating link via `attrs/line/sourceMarker/attrs`)
2875
+ // note: `transform` attrs are ignored by browsers
2876
+ attrs = {},
2877
+ // deprecated - use `attrs/markerUnits` instead (which has higher priority)
2878
+ markerUnits = 'userSpaceOnUse'
2879
+ } = marker;
2880
+ // If the marker already exists in the document,
2881
+ // we're done and we can just use it (reference it using `url()`).
2882
+ if (this.isDefined(id)) return id;
2883
+ // If not, create one.
2884
+ const markerVEl = V('marker', {
2885
+ orient: 'auto',
2886
+ overflow: 'visible',
2887
+ markerUnits: markerUnits
2888
+ });
2889
+ markerVEl.id = id;
2890
+ markerVEl.attr(attrs);
2891
+ let markerContentVEl;
2892
+ if (markup) {
2893
+ let markupVEl;
2894
+ if (typeof markup === 'string') {
2895
+ // Marker object has a `markup` property of type string.
2896
+ // - Construct V from the provided string.
2897
+ markupVEl = V(markup);
2898
+ // `markupVEl` is now either a single VEl, or an array of VEls.
2899
+ // - Coerce it to an array.
2900
+ markupVEl = (Array.isArray(markupVEl) ? markupVEl : [markupVEl]);
2901
+ } else {
2902
+ // Marker object has a `markup` property of type object.
2903
+ // - Construct V from the object by parsing it as DOM JSON.
2904
+ const { fragment } = parseDOMJSON(markup);
2905
+ markupVEl = V(fragment).children();
2906
+ }
2907
+ // `markupVEl` is an array with one or more VEls inside.
2908
+ // - If there are multiple VEls, wrap them in a newly-constructed <g> element
2909
+ if (markupVEl.length > 1) {
2910
+ markerContentVEl = V('g').append(markupVEl);
2911
+ } else {
2912
+ markerContentVEl = markupVEl[0];
2913
+ }
2914
+ } else {
2915
+ // Marker object is a flat structure.
2916
+ // - Construct a new V of type `marker.type`.
2917
+ const { type = 'path' } = marker;
2918
+ markerContentVEl = V(type);
2919
+ }
2920
+ // `markerContentVEl` is a single VEl.
2921
+ // Assign additional attributes to it (= context attributes + marker attributes):
2922
+ // - Attribute values are taken from non-special properties of `marker`.
2923
+ const markerAttrs = omit(marker, 'type', 'id', 'markup', 'attrs', 'markerUnits');
2924
+ const markerAttrsKeys = Object.keys(markerAttrs);
2925
+ markerAttrsKeys.forEach((key) => {
2926
+ const value = markerAttrs[key];
2927
+ const markupValue = markerContentVEl.attr(key); // value coming from markupVEl (if any) = higher priority
2928
+ if (markupValue == null) {
2929
+ // Default logic:
2930
+ markerContentVEl.attr(key, value);
2931
+ } else {
2932
+ // Properties with special logic should be added as cases to this switch block:
2933
+ switch(key) {
2934
+ case 'transform':
2935
+ // - Prepend `transform` to existing value.
2936
+ markerContentVEl.attr(key, (value + ' ' + markupValue));
2937
+ break;
2938
+ }
2939
+ }
2940
+ });
2941
+ markerContentVEl.appendTo(markerVEl);
2942
+ markerVEl.appendTo(defs);
2943
+ return id;
2944
+ },
2945
+
2946
+ customEventTrigger: function(evt, view, rootNode = view.el) {
2947
+
2948
+ const eventNode = evt.target.closest('[event]');
2949
+
2950
+ if (eventNode && rootNode !== eventNode && view.el.contains(eventNode)) {
2951
+ const eventEvt = normalizeEvent(new $.Event(evt.originalEvent, {
2952
+ data: evt.data,
2953
+ // Originally the event listener was attached to the event element.
2954
+ currentTarget: eventNode
2955
+ }));
2956
+
2957
+ this.onevent(eventEvt);
2958
+
2959
+ if (eventEvt.isDefaultPrevented()) {
2960
+ evt.preventDefault();
2961
+ }
2962
+
2963
+ return eventEvt;
2964
+ }
2965
+
2966
+ return null;
2967
+ }
2968
+
2969
+ }, {
2970
+
2971
+ sorting: sortingTypes,
2972
+
2973
+ Layers: LayersNames,
2974
+
2975
+ backgroundPatterns: {
2976
+
2977
+ flipXy: function(img) {
2978
+ // d b
2979
+ // q p
2980
+
2981
+ var canvas = document.createElement('canvas');
2982
+ var imgWidth = img.width;
2983
+ var imgHeight = img.height;
2984
+
2985
+ canvas.width = 2 * imgWidth;
2986
+ canvas.height = 2 * imgHeight;
2987
+
2988
+ var ctx = canvas.getContext('2d');
2989
+ // top-left image
2990
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
2991
+ // xy-flipped bottom-right image
2992
+ ctx.setTransform(-1, 0, 0, -1, canvas.width, canvas.height);
2993
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
2994
+ // x-flipped top-right image
2995
+ ctx.setTransform(-1, 0, 0, 1, canvas.width, 0);
2996
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
2997
+ // y-flipped bottom-left image
2998
+ ctx.setTransform(1, 0, 0, -1, 0, canvas.height);
2999
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3000
+
3001
+ return canvas;
3002
+ },
3003
+
3004
+ flipX: function(img) {
3005
+ // d b
3006
+ // d b
3007
+
3008
+ var canvas = document.createElement('canvas');
3009
+ var imgWidth = img.width;
3010
+ var imgHeight = img.height;
3011
+
3012
+ canvas.width = imgWidth * 2;
3013
+ canvas.height = imgHeight;
3014
+
3015
+ var ctx = canvas.getContext('2d');
3016
+ // left image
3017
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3018
+ // flipped right image
3019
+ ctx.translate(2 * imgWidth, 0);
3020
+ ctx.scale(-1, 1);
3021
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3022
+
3023
+ return canvas;
3024
+ },
3025
+
3026
+ flipY: function(img) {
3027
+ // d d
3028
+ // q q
3029
+
3030
+ var canvas = document.createElement('canvas');
3031
+ var imgWidth = img.width;
3032
+ var imgHeight = img.height;
3033
+
3034
+ canvas.width = imgWidth;
3035
+ canvas.height = imgHeight * 2;
3036
+
3037
+ var ctx = canvas.getContext('2d');
3038
+ // top image
3039
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3040
+ // flipped bottom image
3041
+ ctx.translate(0, 2 * imgHeight);
3042
+ ctx.scale(1, -1);
3043
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3044
+
3045
+ return canvas;
3046
+ },
3047
+
3048
+ watermark: function(img, opt) {
3049
+ // d
3050
+ // d
3051
+
3052
+ opt = opt || {};
3053
+
3054
+ var imgWidth = img.width;
3055
+ var imgHeight = img.height;
3056
+
3057
+ var canvas = document.createElement('canvas');
3058
+ canvas.width = imgWidth * 3;
3059
+ canvas.height = imgHeight * 3;
3060
+
3061
+ var ctx = canvas.getContext('2d');
3062
+ var angle = isNumber(opt.watermarkAngle) ? -opt.watermarkAngle : -20;
3063
+ var radians = toRad(angle);
3064
+ var stepX = canvas.width / 4;
3065
+ var stepY = canvas.height / 4;
3066
+
3067
+ for (var i = 0; i < 4; i++) {
3068
+ for (var j = 0; j < 4; j++) {
3069
+ if ((i + j) % 2 > 0) {
3070
+ // reset the current transformations
3071
+ ctx.setTransform(1, 0, 0, 1, (2 * i - 1) * stepX, (2 * j - 1) * stepY);
3072
+ ctx.rotate(radians);
3073
+ ctx.drawImage(img, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
3074
+ }
3075
+ }
3076
+ }
3077
+
3078
+ return canvas;
3079
+ }
3080
+ },
3081
+
3082
+ gridPatterns: {
3083
+ dot: [{
3084
+ color: '#AAAAAA',
3085
+ thickness: 1,
3086
+ markup: 'rect',
3087
+ render: function(el, opt) {
3088
+ V(el).attr({
3089
+ width: opt.thickness,
3090
+ height: opt.thickness,
3091
+ fill: opt.color
3092
+ });
3093
+ }
3094
+ }],
3095
+ fixedDot: [{
3096
+ color: '#AAAAAA',
3097
+ thickness: 1,
3098
+ markup: 'rect',
3099
+ render: function(el, opt) {
3100
+ V(el).attr({ fill: opt.color });
3101
+ },
3102
+ update: function(el, opt, paper) {
3103
+ const { sx, sy } = paper.scale();
3104
+ const width = sx <= 1 ? opt.thickness : opt.thickness / sx;
3105
+ const height = sy <= 1 ? opt.thickness : opt.thickness / sy;
3106
+ V(el).attr({ width, height });
3107
+ }
3108
+ }],
3109
+ mesh: [{
3110
+ color: '#AAAAAA',
3111
+ thickness: 1,
3112
+ markup: 'path',
3113
+ render: function(el, opt) {
3114
+
3115
+ var d;
3116
+ var width = opt.width;
3117
+ var height = opt.height;
3118
+ var thickness = opt.thickness;
3119
+
3120
+ if (width - thickness >= 0 && height - thickness >= 0) {
3121
+ d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
3122
+ } else {
3123
+ d = 'M 0 0 0 0';
3124
+ }
3125
+
3126
+ V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
3127
+ }
3128
+ }],
3129
+ doubleMesh: [{
3130
+ color: '#AAAAAA',
3131
+ thickness: 1,
3132
+ markup: 'path',
3133
+ render: function(el, opt) {
3134
+
3135
+ var d;
3136
+ var width = opt.width;
3137
+ var height = opt.height;
3138
+ var thickness = opt.thickness;
3139
+
3140
+ if (width - thickness >= 0 && height - thickness >= 0) {
3141
+ d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
3142
+ } else {
3143
+ d = 'M 0 0 0 0';
3144
+ }
3145
+
3146
+ V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
3147
+ }
3148
+ }, {
3149
+ color: '#000000',
3150
+ thickness: 3,
3151
+ scaleFactor: 4,
3152
+ markup: 'path',
3153
+ render: function(el, opt) {
3154
+
3155
+ var d;
3156
+ var width = opt.width;
3157
+ var height = opt.height;
3158
+ var thickness = opt.thickness;
3159
+
3160
+ if (width - thickness >= 0 && height - thickness >= 0) {
3161
+ d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
3162
+ } else {
3163
+ d = 'M 0 0 0 0';
3164
+ }
3165
+
3166
+ V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
3167
+ }
3168
+ }]
3169
+ }
3170
+ });
3171
+