@joint/core 4.2.0-alpha.0 → 4.2.0-beta.1

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 (54) hide show
  1. package/README.md +3 -1
  2. package/dist/geometry.js +2 -2
  3. package/dist/geometry.min.js +3 -3
  4. package/dist/joint.d.ts +595 -198
  5. package/dist/joint.js +3895 -1304
  6. package/dist/joint.min.js +3 -3
  7. package/dist/joint.nowrap.js +3895 -1304
  8. package/dist/joint.nowrap.min.js +3 -3
  9. package/dist/vectorizer.js +21 -8
  10. package/dist/vectorizer.min.js +3 -3
  11. package/dist/version.mjs +1 -1
  12. package/package.json +13 -13
  13. package/src/V/index.mjs +20 -5
  14. package/src/alg/Deque.mjs +126 -0
  15. package/src/cellTools/Boundary.mjs +15 -13
  16. package/src/cellTools/Button.mjs +7 -5
  17. package/src/cellTools/Control.mjs +37 -14
  18. package/src/cellTools/HoverConnect.mjs +5 -1
  19. package/src/cellTools/helpers.mjs +44 -3
  20. package/src/config/index.mjs +11 -1
  21. package/src/dia/Cell.mjs +96 -83
  22. package/src/dia/CellCollection.mjs +136 -0
  23. package/src/dia/CellView.mjs +6 -0
  24. package/src/dia/Element.mjs +6 -5
  25. package/src/dia/ElementView.mjs +2 -1
  26. package/src/dia/Graph.mjs +610 -317
  27. package/src/dia/GraphLayer.mjs +53 -0
  28. package/src/dia/GraphLayerCollection.mjs +313 -0
  29. package/src/dia/GraphLayerView.mjs +128 -0
  30. package/src/dia/GraphLayersController.mjs +166 -0
  31. package/src/dia/GraphTopologyIndex.mjs +222 -0
  32. package/src/dia/{layers/GridLayer.mjs → GridLayerView.mjs} +23 -16
  33. package/src/dia/HighlighterView.mjs +22 -0
  34. package/src/dia/{PaperLayer.mjs → LayerView.mjs} +52 -17
  35. package/src/dia/LegacyGraphLayerView.mjs +14 -0
  36. package/src/dia/LinkView.mjs +118 -98
  37. package/src/dia/Paper.mjs +1441 -620
  38. package/src/dia/ToolView.mjs +4 -0
  39. package/src/dia/ToolsView.mjs +14 -5
  40. package/src/dia/attributes/text.mjs +4 -2
  41. package/src/dia/index.mjs +6 -1
  42. package/src/dia/ports.mjs +213 -84
  43. package/src/dia/symbols.mjs +24 -0
  44. package/src/elementTools/HoverConnect.mjs +14 -8
  45. package/src/env/index.mjs +6 -3
  46. package/src/layout/ports/port.mjs +30 -15
  47. package/src/layout/ports/portLabel.mjs +1 -1
  48. package/src/mvc/Collection.mjs +19 -19
  49. package/src/mvc/Model.mjs +13 -10
  50. package/src/mvc/View.mjs +4 -0
  51. package/src/mvc/ViewBase.mjs +1 -1
  52. package/types/geometry.d.ts +64 -60
  53. package/types/joint.d.ts +520 -137
  54. package/types/vectorizer.d.ts +11 -1
package/src/dia/Paper.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import V from '../V/index.mjs';
2
+ import * as g from '../g/index.mjs';
2
3
  import {
3
4
  isNumber,
4
5
  assign,
@@ -12,7 +13,6 @@ import {
12
13
  isFunction,
13
14
  isPlainObject,
14
15
  getByPath,
15
- sortElements,
16
16
  isString,
17
17
  guid,
18
18
  normalizeEvent,
@@ -23,31 +23,49 @@ import {
23
23
  result,
24
24
  camelCase,
25
25
  cloneDeep,
26
+ clone,
26
27
  invoke,
27
28
  hashCode,
28
29
  filter as _filter,
29
30
  parseDOMJSON,
30
31
  toArray,
31
- has
32
+ has,
33
+ uniqueId,
32
34
  } from '../util/index.mjs';
33
35
  import { ViewBase } from '../mvc/ViewBase.mjs';
34
36
  import { Rect, Point, toRad } from '../g/index.mjs';
35
- import { View, views } from '../mvc/index.mjs';
37
+ import { View, views as viewsRegistry } from '../mvc/index.mjs';
36
38
  import { CellView } from './CellView.mjs';
37
39
  import { ElementView } from './ElementView.mjs';
38
40
  import { LinkView } from './LinkView.mjs';
39
- import { Cell } from './Cell.mjs';
40
41
  import { Graph } from './Graph.mjs';
41
- import { LayersNames, PaperLayer } from './PaperLayer.mjs';
42
+ import { LayerView } from './LayerView.mjs';
43
+ import { GraphLayerView } from './GraphLayerView.mjs';
44
+ import { LegacyGraphLayerView } from './LegacyGraphLayerView.mjs';
45
+ import { HighlighterView } from './HighlighterView.mjs';
46
+ import { Deque } from '../alg/Deque.mjs';
47
+ import {
48
+ CELL_MARKER, CELL_VIEW_MARKER, LAYER_VIEW_MARKER, GRAPH_LAYER_VIEW_MARKER
49
+ } from './symbols.mjs';
42
50
  import * as highlighters from '../highlighters/index.mjs';
43
51
  import * as linkAnchors from '../linkAnchors/index.mjs';
44
52
  import * as connectionPoints from '../connectionPoints/index.mjs';
45
53
  import * as anchors from '../anchors/index.mjs';
46
54
 
47
55
  import $ from '../mvc/Dom/index.mjs';
48
- import { GridLayer } from './layers/GridLayer.mjs';
56
+ import { GridLayerView } from './GridLayerView.mjs';
57
+
58
+ const paperLayers = {
59
+ GRID: 'grid',
60
+ BACK: 'back',
61
+ /** @deprecated */
62
+ CELLS: 'cells',
63
+ FRONT: 'front',
64
+ TOOLS: 'tools',
65
+ LABELS: 'labels'
66
+ };
49
67
 
50
- const sortingTypes = {
68
+ export const sortingTypes = {
51
69
  NONE: 'sorting-none',
52
70
  APPROX: 'sorting-approximate',
53
71
  EXACT: 'sorting-exact'
@@ -82,22 +100,223 @@ const defaultHighlighting = {
82
100
  }
83
101
  };
84
102
 
85
- const defaultLayers = [{
86
- name: LayersNames.GRID,
87
- }, {
88
- name: LayersNames.BACK,
103
+ const gridPatterns = {
104
+
105
+ dot: [{
106
+ color: '#AAAAAA',
107
+ thickness: 1,
108
+ markup: 'rect',
109
+ render: function(el, opt) {
110
+ V(el).attr({
111
+ width: opt.thickness,
112
+ height: opt.thickness,
113
+ fill: opt.color
114
+ });
115
+ }
116
+ }],
117
+
118
+ fixedDot: [{
119
+ color: '#AAAAAA',
120
+ thickness: 1,
121
+ markup: 'rect',
122
+ render: function(el, opt) {
123
+ V(el).attr({ fill: opt.color });
124
+ },
125
+ update: function(el, opt, paper) {
126
+ const { sx, sy } = paper.scale();
127
+ const width = sx <= 1 ? opt.thickness : opt.thickness / sx;
128
+ const height = sy <= 1 ? opt.thickness : opt.thickness / sy;
129
+ V(el).attr({ width, height });
130
+ }
131
+ }],
132
+
133
+ mesh: [{
134
+ color: '#AAAAAA',
135
+ thickness: 1,
136
+ markup: 'path',
137
+ render: function(el, opt) {
138
+
139
+ var d;
140
+ var width = opt.width;
141
+ var height = opt.height;
142
+ var thickness = opt.thickness;
143
+
144
+ if (width - thickness >= 0 && height - thickness >= 0) {
145
+ d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
146
+ } else {
147
+ d = 'M 0 0 0 0';
148
+ }
149
+
150
+ V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
151
+ }
152
+ }],
153
+
154
+ doubleMesh: [{
155
+ color: '#AAAAAA',
156
+ thickness: 1,
157
+ markup: 'path',
158
+ render: function(el, opt) {
159
+
160
+ var d;
161
+ var width = opt.width;
162
+ var height = opt.height;
163
+ var thickness = opt.thickness;
164
+
165
+ if (width - thickness >= 0 && height - thickness >= 0) {
166
+ d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
167
+ } else {
168
+ d = 'M 0 0 0 0';
169
+ }
170
+
171
+ V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
172
+ }
173
+ }, {
174
+ color: '#000000',
175
+ thickness: 3,
176
+ scaleFactor: 4,
177
+ markup: 'path',
178
+ render: function(el, opt) {
179
+
180
+ var d;
181
+ var width = opt.width;
182
+ var height = opt.height;
183
+ var thickness = opt.thickness;
184
+
185
+ if (width - thickness >= 0 && height - thickness >= 0) {
186
+ d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
187
+ } else {
188
+ d = 'M 0 0 0 0';
189
+ }
190
+
191
+ V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
192
+ }
193
+ }]
194
+ };
195
+
196
+ const backgroundPatterns = {
197
+
198
+ flipXy: function(img) {
199
+ // d b
200
+ // q p
201
+
202
+ var canvas = document.createElement('canvas');
203
+ var imgWidth = img.width;
204
+ var imgHeight = img.height;
205
+
206
+ canvas.width = 2 * imgWidth;
207
+ canvas.height = 2 * imgHeight;
208
+
209
+ var ctx = canvas.getContext('2d');
210
+ // top-left image
211
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
212
+ // xy-flipped bottom-right image
213
+ ctx.setTransform(-1, 0, 0, -1, canvas.width, canvas.height);
214
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
215
+ // x-flipped top-right image
216
+ ctx.setTransform(-1, 0, 0, 1, canvas.width, 0);
217
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
218
+ // y-flipped bottom-left image
219
+ ctx.setTransform(1, 0, 0, -1, 0, canvas.height);
220
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
221
+
222
+ return canvas;
223
+ },
224
+
225
+ flipX: function(img) {
226
+ // d b
227
+ // d b
228
+
229
+ var canvas = document.createElement('canvas');
230
+ var imgWidth = img.width;
231
+ var imgHeight = img.height;
232
+
233
+ canvas.width = imgWidth * 2;
234
+ canvas.height = imgHeight;
235
+
236
+ var ctx = canvas.getContext('2d');
237
+ // left image
238
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
239
+ // flipped right image
240
+ ctx.translate(2 * imgWidth, 0);
241
+ ctx.scale(-1, 1);
242
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
243
+
244
+ return canvas;
245
+ },
246
+
247
+ flipY: function(img) {
248
+ // d d
249
+ // q q
250
+
251
+ var canvas = document.createElement('canvas');
252
+ var imgWidth = img.width;
253
+ var imgHeight = img.height;
254
+
255
+ canvas.width = imgWidth;
256
+ canvas.height = imgHeight * 2;
257
+
258
+ var ctx = canvas.getContext('2d');
259
+ // top image
260
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
261
+ // flipped bottom image
262
+ ctx.translate(0, 2 * imgHeight);
263
+ ctx.scale(1, -1);
264
+ ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
265
+
266
+ return canvas;
267
+ },
268
+
269
+ watermark: function(img, opt) {
270
+ // d
271
+ // d
272
+
273
+ opt = opt || {};
274
+
275
+ var imgWidth = img.width;
276
+ var imgHeight = img.height;
277
+
278
+ var canvas = document.createElement('canvas');
279
+ canvas.width = imgWidth * 3;
280
+ canvas.height = imgHeight * 3;
281
+
282
+ var ctx = canvas.getContext('2d');
283
+ var angle = isNumber(opt.watermarkAngle) ? -opt.watermarkAngle : -20;
284
+ var radians = toRad(angle);
285
+ var stepX = canvas.width / 4;
286
+ var stepY = canvas.height / 4;
287
+
288
+ for (var i = 0; i < 4; i++) {
289
+ for (var j = 0; j < 4; j++) {
290
+ if ((i + j) % 2 > 0) {
291
+ // reset the current transformations
292
+ ctx.setTransform(1, 0, 0, 1, (2 * i - 1) * stepX, (2 * j - 1) * stepY);
293
+ ctx.rotate(radians);
294
+ ctx.drawImage(img, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
295
+ }
296
+ }
297
+ }
298
+
299
+ return canvas;
300
+ }
301
+ };
302
+
303
+ const implicitLayers = [{
304
+ id: paperLayers.GRID,
305
+ type: 'GridLayerView',
306
+ patterns: gridPatterns
89
307
  }, {
90
- name: LayersNames.CELLS,
308
+ id: paperLayers.BACK,
91
309
  }, {
92
- name: LayersNames.LABELS,
310
+ id: paperLayers.LABELS,
93
311
  }, {
94
- name: LayersNames.FRONT
312
+ id: paperLayers.FRONT
95
313
  }, {
96
- name: LayersNames.TOOLS
314
+ id: paperLayers.TOOLS
97
315
  }];
98
316
 
99
- export const Paper = View.extend({
317
+ const CELL_VIEW_PLACEHOLDER_MARKER = Symbol('joint.cellViewPlaceholderMarker');
100
318
 
319
+ export const Paper = View.extend({
101
320
  className: 'paper',
102
321
 
103
322
  options: {
@@ -173,7 +392,7 @@ export const Paper = View.extend({
173
392
  // }
174
393
  defaultLink: function() {
175
394
  // 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');
395
+ const { cellNamespace } = this.model.layerCollection;
177
396
  const ctor = getByPath(cellNamespace, ['standard', 'Link']);
178
397
  if (!ctor) throw new Error('dia.Paper: no default link model found. Use `options.defaultLink` to specify a default link model.');
179
398
  return new ctor();
@@ -267,14 +486,28 @@ export const Paper = View.extend({
267
486
 
268
487
  autoFreeze: false,
269
488
 
489
+ viewManagement: false,
490
+
270
491
  // no docs yet
271
492
  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;
493
+ if (opt.mounting || opt.isolate) {
494
+ // Do not update connected links when:
495
+ // - the view was just mounted (added back to the paper by viewport function)
496
+ // - the change was marked as `isolate`.
497
+ return;
498
+ }
499
+ // Always update connected links when the view model was replaced with another model
500
+ // with the same id.
501
+ // Note: the removal is done in 2 steps: remove the old model, add the new model.
502
+ // We update connected links on the add step.
503
+ if (!(opt.replace && opt.add)) {
504
+ if ((flag & (paper.FLAG_INSERT | paper.FLAG_REMOVE))) {
505
+ // Do not update connected links when:
506
+ // - the view was just inserted (added to the graph and rendered)
507
+ // - the view model was just removed from the graph
508
+ return;
509
+ }
510
+ }
278
511
  paper.requestConnectedLinksUpdate(view, priority, opt);
279
512
  },
280
513
 
@@ -293,6 +526,8 @@ export const Paper = View.extend({
293
526
 
294
527
  cellViewNamespace: null,
295
528
 
529
+ layerViewNamespace: null,
530
+
296
531
  routerNamespace: null,
297
532
 
298
533
  connectorNamespace: null,
@@ -305,7 +540,7 @@ export const Paper = View.extend({
305
540
 
306
541
  connectionPointNamespace: connectionPoints,
307
542
 
308
- overflow: false
543
+ overflow: false,
309
544
  },
310
545
 
311
546
  events: {
@@ -349,11 +584,13 @@ export const Paper = View.extend({
349
584
  `,
350
585
 
351
586
  svg: null,
352
- viewport: null,
353
587
  defs: null,
354
588
  tools: null,
355
589
  layers: null,
356
590
 
591
+ // deprecated, use layers element instead
592
+ viewport: null,
593
+
357
594
  // For storing the current transformation matrix (CTM) of the paper's viewport.
358
595
  _viewportMatrix: null,
359
596
  // For verifying whether the CTM is up-to-date. The viewport transform attribute
@@ -364,7 +601,6 @@ export const Paper = View.extend({
364
601
  // Paper Layers
365
602
  _layers: null,
366
603
 
367
- SORT_DELAYING_BATCHES: ['add', 'to-front', 'to-back'],
368
604
  UPDATE_DELAYING_BATCHES: ['translate'],
369
605
  // If you interact with these elements,
370
606
  // the default interaction such as `element move` is prevented.
@@ -387,8 +623,15 @@ export const Paper = View.extend({
387
623
  // to mitigate the differences between the model and view geometry.
388
624
  DEFAULT_FIND_BUFFER: 200,
389
625
 
390
- // Default layer to insert the cell views into.
391
- DEFAULT_CELL_LAYER: LayersNames.CELLS,
626
+ FLAG_INSERT: 1<<30,
627
+ FLAG_REMOVE: 1<<29,
628
+ FLAG_INIT: 1<<28,
629
+
630
+ // Layers that are always present on the paper (e.g. grid, back, front, tools)
631
+ implicitLayers,
632
+
633
+ // Reference layer for inserting new graph layers.
634
+ graphLayerRefId: paperLayers.LABELS,
392
635
 
393
636
  init: function() {
394
637
 
@@ -399,23 +642,36 @@ export const Paper = View.extend({
399
642
  /* eslint-enable no-undef */
400
643
  }
401
644
 
645
+ const defaultLayerViewNamespace = {
646
+ LayerView,
647
+ GraphLayerView,
648
+ GridLayerView,
649
+ };
650
+
651
+ this.layerViewNamespace = defaultsDeep({}, options.layerViewNamespace || {}, defaultLayerViewNamespace);
652
+
402
653
  const model = this.model = options.model || new Graph;
403
654
 
655
+ // This property tells us if we need to keep the compatibility
656
+ // with the v4 API and behavior.
657
+ this.legacyMode = !options.viewManagement;
658
+
404
659
  // Layers (SVGGroups)
405
660
  this._layers = {
406
661
  viewsMap: {},
407
- namesMap: {},
408
662
  order: [],
409
663
  };
410
664
 
665
+ // Hash of all cell views.
666
+ this._views = {};
667
+ this._viewPlaceholders = {};
668
+ this._idToCid = {};
669
+
411
670
  this.cloneOptions();
412
671
  this.render();
413
672
  this._setDimensions();
414
673
  this.startListening();
415
674
 
416
- // Hash of all cell views.
417
- this._views = {};
418
-
419
675
  // Mouse wheel events buffer
420
676
  this._mw_evt_buffer = {
421
677
  event: null,
@@ -423,9 +679,7 @@ export const Paper = View.extend({
423
679
  };
424
680
 
425
681
  // Render existing cells in the graph
426
- this.resetViews(model.attributes.cells.models);
427
- // Start the Rendering Loop
428
- if (!this.isFrozen() && this.isAsync()) this.updateViewsAsync();
682
+ this.resetViews(model.getCells());
429
683
  },
430
684
 
431
685
  _resetUpdates: function() {
@@ -434,16 +688,15 @@ export const Paper = View.extend({
434
688
  return this._updates = {
435
689
  id: null,
436
690
  priorities: [{}, {}, {}],
437
- unmountedCids: [],
438
- mountedCids: [],
439
- unmounted: {},
440
- mounted: {},
691
+ unmountedList: new Deque(),
692
+ mountedList: new Deque(),
441
693
  count: 0,
442
694
  keyFrozen: false,
443
695
  freezeKey: null,
444
696
  sort: false,
445
697
  disabled: false,
446
- idle: false
698
+ idle: false,
699
+ freshAfterReset: true,
447
700
  };
448
701
  },
449
702
 
@@ -451,10 +704,13 @@ export const Paper = View.extend({
451
704
  var model = this.model;
452
705
  this.listenTo(model, 'add', this.onCellAdded)
453
706
  .listenTo(model, 'remove', this.onCellRemoved)
454
- .listenTo(model, 'change', this.onCellChange)
455
707
  .listenTo(model, 'reset', this.onGraphReset)
456
- .listenTo(model, 'sort', this.onGraphSort)
457
708
  .listenTo(model, 'batch:stop', this.onGraphBatchStop);
709
+
710
+ this.listenTo(model, 'layer:add', this.onGraphLayerAdd)
711
+ .listenTo(model, 'layer:remove', this.onGraphLayerRemove)
712
+ .listenTo(model, 'layers:sort', this.onGraphLayerCollectionSort);
713
+
458
714
  this.on('cell:highlight', this.onCellHighlight)
459
715
  .on('cell:unhighlight', this.onCellUnhighlight)
460
716
  .on('transform', this.update);
@@ -472,33 +728,28 @@ export const Paper = View.extend({
472
728
  },
473
729
 
474
730
  onCellRemoved: function(cell, _, opt) {
475
- const view = this.findViewByModel(cell);
476
- if (view) this.requestViewUpdate(view, view.FLAG_REMOVE, view.UPDATE_PRIORITY, opt);
477
- },
478
-
479
- onCellChange: function(cell, opt) {
480
- if (cell === this.model.attributes.cells) return;
481
- if (
482
- cell.hasChanged('layer') ||
483
- (cell.hasChanged('z') && this.options.sorting === sortingTypes.APPROX)
484
- ) {
485
- const view = this.findViewByModel(cell);
486
- if (view) this.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
731
+ const viewLike = this._getCellViewLike(cell);
732
+ if (!viewLike) return;
733
+ if (viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) {
734
+ this._unregisterCellViewPlaceholder(viewLike);
735
+ } else {
736
+ this.requestViewUpdate(viewLike, this.FLAG_REMOVE, viewLike.UPDATE_PRIORITY, opt);
487
737
  }
488
738
  },
489
739
 
490
- onGraphReset: function(collection, opt) {
491
- this.resetLayers();
492
- this.resetViews(collection.models, opt);
493
- },
494
-
495
- onGraphSort: function() {
496
- if (this.model.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return;
497
- this.sortViews();
740
+ onGraphReset: function(_collection, opt) {
741
+ // Re-render all graph layer views
742
+ // but keep the implicit layer views.
743
+ this.renderGraphLayerViews();
744
+ this.resetLayerViews();
745
+ // Backward compatibility: reassign the `cells` property
746
+ // with the default layer view.
747
+ this.assertLayerViews();
748
+ this.resetViews(this.model.getCells(), opt);
498
749
  },
499
750
 
500
751
  onGraphBatchStop: function(data) {
501
- if (this.isFrozen()) return;
752
+ if (this.isFrozen() || this.isIdle()) return;
502
753
  var name = data && data.batchName;
503
754
  var graph = this.model;
504
755
  if (!this.isAsync()) {
@@ -507,10 +758,95 @@ export const Paper = View.extend({
507
758
  this.updateViews(data);
508
759
  }
509
760
  }
510
- var sortDelayingBatches = this.SORT_DELAYING_BATCHES;
511
- if (sortDelayingBatches.includes(name) && !graph.hasActiveBatch(sortDelayingBatches)) {
512
- this.sortViews();
761
+ },
762
+
763
+ /**
764
+ * @protected
765
+ * @description When a new layer is added to the graph, we create a new layer view
766
+ **/
767
+ onGraphLayerAdd: function(layer, _, opt) {
768
+ if (this.hasLayerView(layer.id)) return;
769
+
770
+ const layerView = this.createLayerView({
771
+ id: layer.id,
772
+ model: layer
773
+ });
774
+
775
+ const layers = this.model.getLayers();
776
+ let before;
777
+ // Note: There is always at least one graph layer.
778
+ if (layers[layers.length - 1] === layer) {
779
+ // This is the last layer, so insert before the labels layer
780
+ before = paperLayers.LABELS;
781
+ } else {
782
+ // There is a layer after the current one, so insert before that one
783
+ const index = layers.indexOf(layer);
784
+ before = layers[index + 1].id;
513
785
  }
786
+
787
+ this.addLayerView(layerView, { before });
788
+ },
789
+
790
+ /**
791
+ * @protected
792
+ * @description When a layer is removed from the graph, we remove the corresponding layer view
793
+ **/
794
+ onGraphLayerRemove: function(layer, _, opt) {
795
+ if (!this.hasLayerView(layer)) return;
796
+
797
+ // Request layer removal. Since the UPDATE_PRIORITY is lower
798
+ // than cells update priority, the cell views will be removed first.
799
+ this.requestLayerViewRemoval(layer);
800
+ },
801
+
802
+ /**
803
+ * @protected
804
+ * @description When the graph layer collection is sorted,
805
+ * we reorder all graph layer views.
806
+ **/
807
+ onGraphLayerCollectionSort: function(layerCollection) {
808
+ layerCollection.each(layer => {
809
+ if (!this.hasLayerView(layer)) return;
810
+
811
+ this.moveLayerView(layer, { before: this.graphLayerRefId });
812
+ });
813
+ },
814
+
815
+ /**
816
+ * @protected
817
+ * @description Resets all graph layer views.
818
+ */
819
+ renderGraphLayerViews: function() {
820
+ // Remove all existing graph layer views
821
+ // Note: we don't use `getGraphLayerViews()` here because
822
+ // rendered graph layer views could be different from the ones
823
+ // in the graph layer collection (`onResetGraphLayerCollectionReset`).
824
+ this.getLayerViews().forEach(layerView => {
825
+ if (!layerView[GRAPH_LAYER_VIEW_MARKER]) return;
826
+ this._removeLayerView(layerView);
827
+ });
828
+ // Create and insert new graph layer views
829
+ this.model.getLayers().forEach(layer => {
830
+ const layerView = this.createLayerView({
831
+ id: layer.id,
832
+ model: layer
833
+ });
834
+ // Insert the layer view into the paper layers, just before the labels layer.
835
+ // All cell layers are positioned between the "back" and "labels" layers,
836
+ // with the default "cells" layer originally occupying this position.
837
+ this.addLayerView(layerView, { before: this.graphLayerRefId });
838
+ });
839
+ },
840
+
841
+ /**
842
+ * @protected
843
+ * @description Renders all implicit layer views.
844
+ */
845
+ renderImplicitLayerViews: function() {
846
+ this.implicitLayers.forEach(layerInit => {
847
+ const layerView = this.createLayerView(layerInit);
848
+ this.addLayerView(layerView);
849
+ });
514
850
  },
515
851
 
516
852
  cloneOptions: function() {
@@ -558,6 +894,15 @@ export const Paper = View.extend({
558
894
  // Return the default highlighting options into the user specified options.
559
895
  options.highlighting = defaultsDeep({}, highlighting, defaultHighlighting);
560
896
  }
897
+ // Copy and set defaults for the view management options.
898
+ options.viewManagement = defaults({}, options.viewManagement, {
899
+ // Whether to lazy initialize the cell views.
900
+ lazyInitialize: !!options.viewManagement, // default `true` if options.viewManagement provided
901
+ // Whether to add initialized cell views into the unmounted queue.
902
+ initializeUnmounted: false,
903
+ // Whether to dispose the cell views that are not visible.
904
+ disposeHidden: false,
905
+ });
561
906
  },
562
907
 
563
908
  children: function() {
@@ -597,121 +942,301 @@ export const Paper = View.extend({
597
942
  }];
598
943
  },
599
944
 
600
- hasLayerView(layerName) {
601
- return (layerName in this._layers.viewsMap);
945
+ /**
946
+ * @public
947
+ * @description Checks whether the layer view exists by the given layer id or layer model.
948
+ * @param {string|dia.GraphLayer} layerRef - Layer id or layer model.
949
+ * @return {boolean} True if the layer view exists, false otherwise.
950
+ */
951
+ hasLayerView(layerRef) {
952
+ let layerId;
953
+ if (isString(layerRef)) {
954
+ layerId = layerRef;
955
+ } else if (layerRef) {
956
+ layerId = layerRef.id;
957
+ } else {
958
+ return false;
959
+ }
960
+ return (layerId in this._layers.viewsMap);
602
961
  },
603
962
 
604
- getLayerView(layerName) {
605
- const { _layers: { viewsMap }} = this;
606
- if (layerName in viewsMap) return viewsMap[layerName];
607
- throw new Error(`dia.Paper: Unknown layer "${layerName}".`);
608
- },
963
+ /**
964
+ * @public
965
+ * @description Returns the layer view by the given layer id or layer model.
966
+ * @param {string|dia.GraphLayer} layerRef - Layer id or layer model.
967
+ * @return {dia.LayerView} The layer view.
968
+ * @throws {Error} if the layer view is not found
969
+ */
970
+ getLayerView(layerRef) {
609
971
 
610
- getLayerNode(layerName) {
611
- return this.getLayerView(layerName).el;
612
- },
972
+ let layerId;
973
+ if (isString(layerRef)) {
974
+ layerId = layerRef;
975
+ } else if (layerRef) {
976
+ layerId = layerRef.id;
977
+ } else {
978
+ throw new Error('dia.Paper: No layer provided.');
979
+ }
613
980
 
614
- _removeLayer(layerView) {
615
- this._unregisterLayer(layerView);
616
- layerView.remove();
617
- },
981
+ const layerView = this._layers.viewsMap[layerId];
982
+ if (!layerView) {
983
+ throw new Error(`dia.Paper: Unknown layer view "${layerId}".`);
984
+ }
618
985
 
619
- _unregisterLayer(layerView) {
620
- const { _layers: { viewsMap, namesMap, order }} = this;
621
- const layerName = this._getLayerName(layerView);
622
- order.splice(order.indexOf(layerName), 1);
623
- delete namesMap[layerView.cid];
624
- delete viewsMap[layerName];
986
+ return layerView;
625
987
  },
626
988
 
627
- _registerLayer(layerName, layerView, beforeLayerView) {
628
- const { _layers: { viewsMap, namesMap, order }} = this;
629
- if (beforeLayerView) {
630
- const beforeLayerName = this._getLayerName(beforeLayerView);
631
- order.splice(order.indexOf(beforeLayerName), 0, layerName);
632
- } else {
633
- order.push(layerName);
634
- }
635
- viewsMap[layerName] = layerView;
636
- namesMap[layerView.cid] = layerName;
989
+ /**
990
+ * @deprecated use `getLayerView(layerId).el` instead
991
+ */
992
+ getLayerNode(layerId) {
993
+ return this.getLayerView(layerId).el;
637
994
  },
638
995
 
639
- _getLayerView(layer) {
640
- const { _layers: { namesMap, viewsMap }} = this;
641
- if (layer instanceof PaperLayer) {
642
- if (layer.cid in namesMap) return layer;
643
- return null;
644
- }
645
- if (layer in viewsMap) return viewsMap[layer];
646
- return null;
996
+ /**
997
+ * @protected
998
+ * @description Removes the given layer view from the paper.
999
+ * It does not check whether the layer view is empty.
1000
+ * @param {dia.LayerView} layerView - The layer view to remove.
1001
+ */
1002
+ _removeLayerView(layerView) {
1003
+ this._unregisterLayerView(layerView);
1004
+ layerView.remove();
647
1005
  },
648
1006
 
649
- _getLayerName(layerView) {
650
- const { _layers: { namesMap }} = this;
651
- return namesMap[layerView.cid];
1007
+
1008
+ /**
1009
+ * @protected
1010
+ * @description Removes all layer views from the paper.
1011
+ * It does not check whether the layer views are empty.
1012
+ */
1013
+ _removeLayerViews: function() {
1014
+ Object.values(this._layers.viewsMap).forEach(layerView => {
1015
+ this._removeLayerView(layerView);
1016
+ });
652
1017
  },
653
1018
 
654
- _requireLayerView(layer) {
655
- const layerView = this._getLayerView(layer);
656
- if (!layerView) {
657
- if (layer instanceof PaperLayer) {
658
- throw new Error('dia.Paper: The layer is not registered.');
1019
+ /**
1020
+ * @protected
1021
+ * @description Unregisters the given layer view from the paper.
1022
+ * @param {dia.LayerView} layerView - The layer view to unregister.
1023
+ */
1024
+ _unregisterLayerView(layerView) {
1025
+ const { _layers: { viewsMap, order }} = this;
1026
+ const layerId = layerView.id;
1027
+ // Remove the layer id from the order list.
1028
+ const layerIndex = order.indexOf(layerId);
1029
+ if (layerIndex !== -1) {
1030
+ order.splice(layerIndex, 1);
1031
+ }
1032
+ // Unlink the layer view from the paper.
1033
+ layerView.unsetPaperReference();
1034
+ // Remove the layer view from the paper's registry.
1035
+ delete viewsMap[layerId];
1036
+ },
1037
+
1038
+ /**
1039
+ * @protected
1040
+ * @description Registers the given layer view in the paper.
1041
+ * @param {dia.LayerView} layerView - The layer view to register.
1042
+ * @throws {Error} if the layer view is not an instance of dia.LayerView
1043
+ * @throws {Error} if the layer view already exists in the paper
1044
+ */
1045
+ _registerLayerView(layerView) {
1046
+ if (!layerView || !layerView[LAYER_VIEW_MARKER]) {
1047
+ throw new Error('dia.Paper: The layer view must be an instance of dia.LayerView.');
1048
+ }
1049
+
1050
+ if (this.hasLayerView(layerView.id)) {
1051
+ throw new Error(`dia.Paper: The layer view "${layerView.id}" already exists.`);
1052
+ }
1053
+ // Link the layer view back to the paper.
1054
+ layerView.setPaperReference(this);
1055
+ // Store the layer view in the paper's registry.
1056
+ this._layers.viewsMap[layerView.id] = layerView;
1057
+ },
1058
+
1059
+ /**
1060
+ * @public
1061
+ * @description Removes the layer view by the given layer id or layer model.
1062
+ * @param {string|dia.GraphLayer} layerRef - Layer id or layer model.
1063
+ * @throws {Error} if the layer view is not empty
1064
+ */
1065
+ removeLayerView(layerRef) {
1066
+ const layerView = this.getLayerView(layerRef);
1067
+ if (!layerView.isEmpty()) {
1068
+ throw new Error('dia.Paper: The layer view is not empty.');
1069
+ }
1070
+
1071
+ this._removeLayerView(layerView);
1072
+ },
1073
+
1074
+ /**
1075
+ * @protected
1076
+ * @description Schedules the layer view removal by the given layer id or layer model.
1077
+ * The actual removal will be performed during the paper update cycle.
1078
+ * @param {string|dia.GraphLayer} layerRef - Layer id or layer model.
1079
+ * @param {Object} [opt] - Update options.
1080
+ */
1081
+ requestLayerViewRemoval(layerRef, opt) {
1082
+ const layerView = this.getLayerView(layerRef);
1083
+ const { FLAG_REMOVE } = this;
1084
+ const { UPDATE_PRIORITY } = layerView;
1085
+
1086
+ this.requestViewUpdate(layerView, FLAG_REMOVE, UPDATE_PRIORITY, opt);
1087
+ },
1088
+
1089
+ /**
1090
+ * @public
1091
+ * @internal not documented
1092
+ * @description Schedules the cell view insertion into the appropriate layer view.
1093
+ * The actual insertion will be performed during the paper update cycle.
1094
+ * @param {dia.Cell} cell - The cell model whose view should be inserted.
1095
+ * @param {Object} [opt] - Update options.
1096
+ */
1097
+ requestCellViewInsertion(cell, opt) {
1098
+ const viewLike = this._getCellViewLike(cell);
1099
+ if (!viewLike) return;
1100
+ this.requestViewUpdate(viewLike, this.FLAG_INSERT, viewLike.UPDATE_PRIORITY, opt);
1101
+ },
1102
+
1103
+ /**
1104
+ * @private
1105
+ * Helper method for addLayerView and moveLayerView methods
1106
+ */
1107
+ _getBeforeLayerViewFromOptions(layerView, options) {
1108
+ let { before = null, index } = options;
1109
+
1110
+ if (before && index !== undefined) {
1111
+ throw new Error('dia.Paper: Options "before" and "index" are mutually exclusive.');
1112
+ }
1113
+
1114
+ let computedBefore;
1115
+ if (index !== undefined) {
1116
+ const { _layers: { order }} = this;
1117
+ if (index >= order.length) {
1118
+ // If index is greater than the number of layers,
1119
+ // return before as null (move to the end).
1120
+ computedBefore = null;
1121
+ } else if (index < 0) {
1122
+ // If index is negative, move to the beginning.
1123
+ computedBefore = order[0];
659
1124
  } else {
660
- throw new Error(`dia.Paper: Unknown layer "${layer}".`);
1125
+ const originalIndex = order.indexOf(layerView.id);
1126
+ if (originalIndex !== -1 && index > originalIndex) {
1127
+ // If moving a layer upwards in the stack, we need to adjust the index
1128
+ // to account for the layer being removed from its original position.
1129
+ index += 1;
1130
+ }
1131
+ // Otherwise, get the layer ID at the specified index.
1132
+ computedBefore = order[index] || null;
661
1133
  }
1134
+ } else {
1135
+ computedBefore = before;
662
1136
  }
663
- return layerView;
1137
+
1138
+ return computedBefore ? this.getLayerView(computedBefore) : null;
664
1139
  },
665
1140
 
666
- hasLayer(layer) {
667
- return this._getLayerView(layer) !== null;
1141
+ /**
1142
+ * @public
1143
+ * @description Adds the layer view to the paper.
1144
+ * @param {dia.LayerView} layerView - The layer view to add.
1145
+ * @param {Object} [options] - Adding options.
1146
+ * @param {string|dia.GraphLayer} [options.before] - Layer id or layer model before
1147
+ */
1148
+ addLayerView(layerView, options = {}) {
1149
+ this._registerLayerView(layerView);
1150
+
1151
+ const beforeLayerView = this._getBeforeLayerViewFromOptions(layerView, options);
1152
+ this.insertLayerView(layerView, beforeLayerView);
668
1153
  },
669
1154
 
670
- removeLayer(layer) {
671
- const layerView = this._requireLayerView(layer);
672
- if (!layerView.isEmpty()) {
673
- throw new Error('dia.Paper: The layer is not empty.');
674
- }
675
- this._removeLayer(layerView);
1155
+ /**
1156
+ * @public
1157
+ * @description Moves the layer view.
1158
+ * @param {Paper.LayerRef} layerRef - The layer view reference to move.
1159
+ * @param {Object} [options] - Moving options.
1160
+ * @param {Paper.LayerRef} [options.before] - Layer id or layer model before
1161
+ * @param {number} [options.index] - Zero-based index to which to move the layer view.
1162
+ */
1163
+ moveLayerView(layerRef, options = {}) {
1164
+ const layerView = this.getLayerView(layerRef);
1165
+
1166
+ const beforeLayerView = this._getBeforeLayerViewFromOptions(layerView, options);
1167
+ this.insertLayerView(layerView, beforeLayerView);
676
1168
  },
677
1169
 
678
- addLayer(layerName, layerView, options = {}) {
679
- if (!layerName || typeof layerName !== 'string') {
680
- throw new Error('dia.Paper: The layer name must be provided.');
681
- }
682
- if (this._getLayerView(layerName)) {
683
- throw new Error(`dia.Paper: The layer "${layerName}" already exists.`);
684
- }
685
- if (!(layerView instanceof PaperLayer)) {
686
- throw new Error('dia.Paper: The layer view is not an instance of dia.PaperLayer.');
687
- }
688
- const { insertBefore } = options;
689
- if (!insertBefore) {
690
- this._registerLayer(layerName, layerView, null);
691
- this.layers.appendChild(layerView.el);
692
- } else {
693
- const beforeLayerView = this._requireLayerView(insertBefore);
694
- this._registerLayer(layerName, layerView, beforeLayerView);
1170
+ /**
1171
+ * @protected
1172
+ * @description Inserts the layer view into the paper.
1173
+ * If the layer view already exists in the paper, it is moved to the new position.
1174
+ * @param {dia.LayerView} layerView - The layer view to insert.
1175
+ * @param {dia.LayerView} [before] - Layer view before
1176
+ * which the layer view should be inserted.
1177
+ */
1178
+ insertLayerView(layerView, beforeLayerView) {
1179
+ const layerId = layerView.id;
1180
+
1181
+ const { _layers: { order }} = this;
1182
+ const currentLayerIndex = order.indexOf(layerId);
1183
+
1184
+ // Should the layer view be inserted before another layer view?
1185
+ if (beforeLayerView) {
1186
+ const beforeLayerViewId = beforeLayerView.id;
1187
+ if (layerId === beforeLayerViewId) {
1188
+ // The layer view is already in the right place.
1189
+ return;
1190
+ }
1191
+
1192
+ let beforeLayerPosition = order.indexOf(beforeLayerViewId);
1193
+ // Remove from the `order` list if the layer view is already in the order.
1194
+ if (currentLayerIndex !== -1) {
1195
+ if (currentLayerIndex < beforeLayerPosition) {
1196
+ beforeLayerPosition -= 1;
1197
+ }
1198
+ order.splice(currentLayerIndex, 1);
1199
+ }
1200
+ order.splice(beforeLayerPosition, 0, layerId);
695
1201
  this.layers.insertBefore(layerView.el, beforeLayerView.el);
1202
+ return;
696
1203
  }
697
- },
698
1204
 
699
- moveLayer(layer, insertBefore) {
700
- const layerView = this._requireLayerView(layer);
701
- if (layerView === this._getLayerView(insertBefore)) return;
702
- const layerName = this._getLayerName(layerView);
703
- this._unregisterLayer(layerView);
704
- this.addLayer(layerName, layerView, { insertBefore });
1205
+ // Remove from the `order` list if the layer view is already in the order.
1206
+ // This is needed for the case when the layer view is inserted in the new position.
1207
+ if (currentLayerIndex !== -1) {
1208
+ order.splice(currentLayerIndex, 1);
1209
+ }
1210
+ order.push(layerId);
1211
+ this.layers.appendChild(layerView.el);
705
1212
  },
706
1213
 
707
- getLayerNames() {
708
- // Returns a sorted array of layer names.
1214
+ /**
1215
+ * @protected
1216
+ * @description Returns an array of layer view ids in the order they are rendered.
1217
+ * @returns {string[]} An array of layer view ids.
1218
+ */
1219
+ getLayerViewOrder() {
709
1220
  return this._layers.order.slice();
710
1221
  },
711
1222
 
712
- getLayers() {
713
- // Returns a sorted array of layer views.
714
- return this.getLayerNames().map(name => this.getLayerView(name));
1223
+ /**
1224
+ * @public
1225
+ * @description Returns an array of layer views in the order they are rendered.
1226
+ * @returns {dia.LayerView[]} An array of layer views.
1227
+ */
1228
+ getLayerViews() {
1229
+ return this.getLayerViewOrder().map(id => this.getLayerView(id));
1230
+ },
1231
+
1232
+ /**
1233
+ * @public
1234
+ * @description Returns an array of graph layer views in the order they are rendered.
1235
+ * @returns {dia.GraphLayerView[]} An array of graph layer views.
1236
+ */
1237
+ getGraphLayerViews() {
1238
+ const { _layers: { viewsMap }} = this;
1239
+ return this.model.getLayers().map(layer => viewsMap[layer.id]);
715
1240
  },
716
1241
 
717
1242
  render: function() {
@@ -727,7 +1252,7 @@ export const Paper = View.extend({
727
1252
  this.defs = defs;
728
1253
  this.layers = layers;
729
1254
 
730
- this.renderLayers();
1255
+ this.renderLayerViews();
731
1256
 
732
1257
  V.ensureId(svg);
733
1258
 
@@ -749,48 +1274,86 @@ export const Paper = View.extend({
749
1274
  V(this.svg).prepend(V.createSVGStyle(css));
750
1275
  },
751
1276
 
752
- createLayer(name) {
753
- switch (name) {
754
- case LayersNames.GRID:
755
- return new GridLayer({ name, paper: this, patterns: this.constructor.gridPatterns });
756
- default:
757
- return new PaperLayer({ name });
1277
+ /**
1278
+ * @protected
1279
+ * @description Creates a layer view instance based on the provided options.
1280
+ * It finds the appropriate layer view constructor from the paper's
1281
+ * `layerViewNamespace` and instantiates it.
1282
+ * @param {*} options See `dia.LayerView` options.
1283
+ * @returns {dia.LayerView}
1284
+ */
1285
+ createLayerView(options) {
1286
+ if (options == null) {
1287
+ throw new Error('dia.Paper: Layer view options are required.');
758
1288
  }
759
- },
760
1289
 
761
- renderLayer: function(name) {
762
- const layerView = this.createLayer(name);
763
- this.addLayer(name, layerView);
764
- return layerView;
765
- },
1290
+ if (options.id == null) {
1291
+ throw new Error('dia.Paper: Layer view id is required.');
1292
+ }
1293
+
1294
+ const viewOptions = clone(options);
1295
+
1296
+ let viewConstructor;
1297
+ if (viewOptions.model) {
1298
+ const modelType = viewOptions.model.get('type') || viewOptions.model.constructor.name;
1299
+ const type = modelType + 'View';
1300
+
1301
+ // For backward compatibility we use the LegacyGraphLayerView for the default `cells` layer.
1302
+ if (this.model.layersController.legacyMode) {
1303
+ viewConstructor = LegacyGraphLayerView;
1304
+ } else {
1305
+ viewConstructor = this.layerViewNamespace[type] || LayerView;
1306
+ }
1307
+ } else {
1308
+ // Paper layers
1309
+ const type = viewOptions.type;
1310
+ viewConstructor = this.layerViewNamespace[type] || LayerView;
1311
+ }
1312
+
1313
+ return new viewConstructor(viewOptions);
1314
+ },
1315
+
1316
+ /**
1317
+ * @protected
1318
+ * @description Renders all paper layer views and graph layer views.
1319
+ */
1320
+ renderLayerViews: function() {
1321
+ this._removeLayerViews();
1322
+ // Render the paper layers.
1323
+ this.renderImplicitLayerViews();
1324
+ // Render the layers.
1325
+ this.renderGraphLayerViews();
1326
+ // Ensure that essential layer views are present.
1327
+ this.assertLayerViews();
1328
+ },
1329
+
1330
+ /**
1331
+ * @protected
1332
+ * @description Ensures that essential layer views are present on the paper.
1333
+ * @throws {Error} if any of the essential layer views is missing
1334
+ */
1335
+ assertLayerViews: function() {
1336
+ // Throws an exception if essential layer views are missing.
1337
+ const cellsLayerView = this.getLayerView(this.model.getDefaultLayer().id);
1338
+ const toolsLayerView = this.getLayerView(paperLayers.TOOLS);
1339
+ const labelsLayerView = this.getLayerView(paperLayers.LABELS);
766
1340
 
767
- renderLayers: function(layers = defaultLayers) {
768
- this.removeLayers();
769
- layers.forEach(({ name }) => this.renderLayer(name));
770
- // Throws an exception if doesn't exist
771
- const cellsLayerView = this.getLayerView(LayersNames.CELLS);
772
- const toolsLayerView = this.getLayerView(LayersNames.TOOLS);
773
- const labelsLayerView = this.getLayerView(LayersNames.LABELS);
774
1341
  // backwards compatibility
775
1342
  this.tools = toolsLayerView.el;
776
1343
  this.cells = this.viewport = cellsLayerView.el;
777
- // user-select: none;
778
- cellsLayerView.vel.addClass(addClassNamePrefix('viewport'));
1344
+ // Backwards compatibility: same as `LegacyGraphLayerView` we keep
1345
+ // the `viewport` class on the labels layer.
779
1346
  labelsLayerView.vel.addClass(addClassNamePrefix('viewport'));
780
- cellsLayerView.el.style.webkitUserSelect = 'none';
781
- cellsLayerView.el.style.userSelect = 'none';
782
1347
  labelsLayerView.el.style.webkitUserSelect = 'none';
783
1348
  labelsLayerView.el.style.userSelect = 'none';
784
1349
  },
785
1350
 
786
- removeLayers: function() {
787
- const { _layers: { viewsMap }} = this;
788
- Object.values(viewsMap).forEach(layerView => this._removeLayer(layerView));
789
- },
790
-
791
- resetLayers: function() {
792
- const { _layers: { viewsMap }} = this;
793
- Object.values(viewsMap).forEach(layerView => layerView.removePivots());
1351
+ /**
1352
+ * @protected
1353
+ * @description Resets all layer views.
1354
+ */
1355
+ resetLayerViews: function() {
1356
+ this.getLayerViews().forEach(layerView => layerView.reset());
794
1357
  },
795
1358
 
796
1359
  update: function() {
@@ -916,56 +1479,54 @@ export const Paper = View.extend({
916
1479
 
917
1480
  clientMatrix: function() {
918
1481
 
919
- return V.createSVGMatrix(this.cells.getScreenCTM());
1482
+ return V.createSVGMatrix(this.layers.getScreenCTM());
920
1483
  },
921
1484
 
922
1485
  requestConnectedLinksUpdate: function(view, priority, opt) {
923
- if (view instanceof CellView) {
924
- var model = view.model;
925
- var links = this.model.getConnectedLinks(model);
926
- for (var j = 0, n = links.length; j < n; j++) {
927
- var link = links[j];
928
- var linkView = this.findViewByModel(link);
929
- if (!linkView) continue;
930
- var flagLabels = ['UPDATE'];
931
- if (link.getTargetCell() === model) flagLabels.push('TARGET');
932
- if (link.getSourceCell() === model) flagLabels.push('SOURCE');
933
- var nextPriority = Math.max(priority + 1, linkView.UPDATE_PRIORITY);
934
- this.scheduleViewUpdate(linkView, linkView.getFlag(flagLabels), nextPriority, opt);
935
- }
1486
+ if (!view || !view[CELL_VIEW_MARKER]) return;
1487
+ var model = view.model;
1488
+ var links = this.model.getConnectedLinks(model);
1489
+ for (var j = 0, n = links.length; j < n; j++) {
1490
+ var link = links[j];
1491
+ var linkView = this._getCellViewLike(link);
1492
+ if (!linkView) continue;
1493
+ // We do not have to update placeholder views.
1494
+ // They will be updated on initial render.
1495
+ if (linkView[CELL_VIEW_PLACEHOLDER_MARKER]) continue;
1496
+ var nextPriority = Math.max(priority + 1, linkView.UPDATE_PRIORITY);
1497
+ this.scheduleViewUpdate(linkView, linkView.getFlag(LinkView.Flags.UPDATE), nextPriority, opt);
936
1498
  }
937
1499
  },
938
1500
 
939
1501
  forcePostponedViewUpdate: function(view, flag) {
940
- if (!view || !(view instanceof CellView)) return false;
941
- var model = view.model;
1502
+ if (!view || !view[CELL_VIEW_MARKER]) return false;
1503
+ const model = view.model;
942
1504
  if (model.isElement()) return false;
943
- if ((flag & view.getFlag(['SOURCE', 'TARGET'])) === 0) {
944
- var dumpOptions = { silent: true };
945
- // LinkView is waiting for the target or the source cellView to be rendered
946
- // This can happen when the cells are not in the viewport.
947
- var sourceFlag = 0;
948
- var sourceView = this.findViewByModel(model.getSourceCell());
949
- if (sourceView && !this.isViewMounted(sourceView)) {
950
- sourceFlag = this.dumpView(sourceView, dumpOptions);
951
- view.updateEndMagnet('source');
952
- }
953
- var targetFlag = 0;
954
- var targetView = this.findViewByModel(model.getTargetCell());
955
- if (targetView && !this.isViewMounted(targetView)) {
956
- targetFlag = this.dumpView(targetView, dumpOptions);
957
- view.updateEndMagnet('target');
958
- }
959
- if (sourceFlag === 0 && targetFlag === 0) {
960
- // If leftover flag is 0, all view updates were done.
961
- return !this.dumpView(view, dumpOptions);
962
- }
1505
+ const dumpOptions = { silent: true };
1506
+ // LinkView is waiting for the target or the source cellView to be rendered
1507
+ // This can happen when the cells are not in the viewport.
1508
+ let sourceFlag = 0;
1509
+ const sourceCell = model.getSourceCell();
1510
+ if (sourceCell && !this.isCellVisible(sourceCell)) {
1511
+ const sourceView = this.findViewByModel(sourceCell);
1512
+ sourceFlag = this.dumpView(sourceView, dumpOptions);
1513
+ }
1514
+ let targetFlag = 0;
1515
+ const targetCell = model.getTargetCell();
1516
+ if (targetCell && !this.isCellVisible(targetCell)) {
1517
+ const targetView = this.findViewByModel(targetCell);
1518
+ targetFlag = this.dumpView(targetView, dumpOptions);
1519
+ }
1520
+ if (sourceFlag === 0 && targetFlag === 0) {
1521
+ // If leftover flag is 0, all view updates were done.
1522
+ return !this.dumpView(view, dumpOptions);
963
1523
  }
964
1524
  return false;
965
1525
  },
966
1526
 
967
1527
  requestViewUpdate: function(view, flag, priority, opt) {
968
1528
  opt || (opt = {});
1529
+ // Note: `scheduleViewUpdate` wakes up the paper if it is idle.
969
1530
  this.scheduleViewUpdate(view, flag, priority, opt);
970
1531
  var isAsync = this.isAsync();
971
1532
  if (this.isFrozen() || (isAsync && opt.async !== false)) return;
@@ -976,13 +1537,14 @@ export const Paper = View.extend({
976
1537
 
977
1538
  scheduleViewUpdate: function(view, type, priority, opt) {
978
1539
  const { _updates: updates, options } = this;
979
- if (updates.idle) {
980
- if (options.autoFreeze) {
981
- updates.idle = false;
982
- this.unfreeze();
983
- }
1540
+ if (updates.idle && options.autoFreeze) {
1541
+ this.legacyMode
1542
+ ? this.unfreeze() // Restart rendering loop without original options
1543
+ : this.wakeUp();
984
1544
  }
985
- const { FLAG_REMOVE, FLAG_INSERT, UPDATE_PRIORITY, cid } = view;
1545
+ const { FLAG_REMOVE, FLAG_INSERT } = this;
1546
+ const { UPDATE_PRIORITY, cid } = view;
1547
+
986
1548
  let priorityUpdates = updates.priorities[priority];
987
1549
  if (!priorityUpdates) priorityUpdates = updates.priorities[priority] = {};
988
1550
  // Move higher priority updates to this priority
@@ -1028,20 +1590,24 @@ export const Paper = View.extend({
1028
1590
  dumpView: function(view, opt = {}) {
1029
1591
  const flag = this.dumpViewUpdate(view);
1030
1592
  if (!flag) return 0;
1031
- const shouldNotify = !opt.silent;
1032
- if (shouldNotify) this.notifyBeforeRender(opt);
1593
+ this.notifyBeforeRender(opt);
1033
1594
  const leftover = this.updateView(view, flag, opt);
1034
- if (shouldNotify) {
1035
- const stats = { updated: 1, priority: view.UPDATE_PRIORITY };
1036
- this.notifyAfterRender(stats, opt);
1037
- }
1595
+ const stats = { updated: 1, priority: view.UPDATE_PRIORITY };
1596
+ this.notifyAfterRender(stats, opt);
1038
1597
  return leftover;
1039
1598
  },
1040
1599
 
1041
1600
  updateView: function(view, flag, opt) {
1042
1601
  if (!view) return 0;
1043
- const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT, model } = view;
1044
- if (view instanceof CellView) {
1602
+ const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT } = this;
1603
+ const { model } = view;
1604
+ if (view[GRAPH_LAYER_VIEW_MARKER]) {
1605
+ if (flag & FLAG_REMOVE) {
1606
+ this.removeLayerView(view);
1607
+ return 0;
1608
+ }
1609
+ }
1610
+ if (view[CELL_VIEW_MARKER]) {
1045
1611
  if (flag & FLAG_REMOVE) {
1046
1612
  this.removeView(model);
1047
1613
  return 0;
@@ -1069,57 +1635,70 @@ export const Paper = View.extend({
1069
1635
  registerUnmountedView: function(view) {
1070
1636
  var cid = view.cid;
1071
1637
  var updates = this._updates;
1072
- if (cid in updates.unmounted) return 0;
1073
- var flag = updates.unmounted[cid] |= view.FLAG_INSERT;
1074
- updates.unmountedCids.push(cid);
1075
- delete updates.mounted[cid];
1638
+ if (updates.unmountedList.has(cid)) return 0;
1639
+ const flag = this.FLAG_INSERT;
1640
+ updates.unmountedList.pushTail(cid, flag);
1641
+ updates.mountedList.delete(cid);
1076
1642
  return flag;
1077
1643
  },
1078
1644
 
1079
1645
  registerMountedView: function(view) {
1080
1646
  var cid = view.cid;
1081
1647
  var updates = this._updates;
1082
- if (cid in updates.mounted) return 0;
1083
- updates.mounted[cid] = true;
1084
- updates.mountedCids.push(cid);
1085
- var flag = updates.unmounted[cid] || 0;
1086
- delete updates.unmounted[cid];
1648
+ if (updates.mountedList.has(cid)) return 0;
1649
+ const unmountedItem = updates.unmountedList.get(cid);
1650
+ const flag = unmountedItem ? unmountedItem.value : 0;
1651
+ updates.unmountedList.delete(cid);
1652
+ updates.mountedList.pushTail(cid);
1087
1653
  return flag;
1088
1654
  },
1089
1655
 
1090
- isViewMounted: function(view) {
1091
- if (!view) return false;
1092
- var cid = view.cid;
1093
- var updates = this._updates;
1094
- return (cid in updates.mounted);
1656
+ isCellVisible: function(cellOrId) {
1657
+ const cid = cellOrId && this._idToCid[cellOrId.id || cellOrId];
1658
+ if (!cid) return false; // The view is not registered.
1659
+ return this.isViewMounted(cid);
1095
1660
  },
1096
1661
 
1662
+ isViewMounted: function(viewOrCid) {
1663
+ if (!viewOrCid) return false;
1664
+ let cid;
1665
+ if (viewOrCid[CELL_VIEW_MARKER] || viewOrCid[CELL_VIEW_PLACEHOLDER_MARKER]) {
1666
+ cid = viewOrCid.cid;
1667
+ } else {
1668
+ cid = viewOrCid;
1669
+ }
1670
+ return this._updates.mountedList.has(cid);
1671
+ },
1672
+
1673
+ /**
1674
+ * @deprecated use `updateCellsVisibility` instead.
1675
+ * `paper.updateCellsVisibility({ cellVisibility: () => true });`
1676
+ */
1097
1677
  dumpViews: function(opt) {
1098
- var passingOpt = defaults({}, opt, { viewport: null });
1099
- this.checkViewport(passingOpt);
1100
- this.updateViews(passingOpt);
1678
+ // Update cell visibility without `cellVisibility` callback i.e. make the cells visible
1679
+ const passingOpt = defaults({}, opt, { cellVisibility: null, viewport: null });
1680
+ this.updateCellsVisibility(passingOpt);
1101
1681
  },
1102
1682
 
1103
- // Synchronous views update
1104
- updateViews: function(opt) {
1683
+ /**
1684
+ * Process all scheduled updates synchronously.
1685
+ */
1686
+ updateViews: function(opt = {}) {
1105
1687
  this.notifyBeforeRender(opt);
1106
- let batchStats;
1107
- let updateCount = 0;
1108
- let batchCount = 0;
1109
- let priority = MIN_PRIORITY;
1110
- do {
1111
- batchCount++;
1112
- batchStats = this.updateViewsBatch(opt);
1113
- updateCount += batchStats.updated;
1114
- priority = Math.min(batchStats.priority, priority);
1115
- } while (!batchStats.empty);
1116
- const stats = { updated: updateCount, batches: batchCount, priority };
1688
+ const batchStats = this.updateViewsBatch({ ...opt, batchSize: Infinity });
1689
+ const stats = {
1690
+ updated: batchStats.updated,
1691
+ priority: batchStats.priority,
1692
+ // For backward compatibility. Will be removed in the future.
1693
+ batches: Number.isFinite(opt.batchSize) ? Math.ceil(batchStats.updated / opt.batchSize) : 1
1694
+ };
1117
1695
  this.notifyAfterRender(stats, opt);
1118
1696
  return stats;
1119
1697
  },
1120
1698
 
1121
1699
  hasScheduledUpdates: function() {
1122
- const priorities = this._updates.priorities;
1700
+ const updates = this._updates;
1701
+ const priorities = updates.priorities;
1123
1702
  const priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
1124
1703
  let i = priorityIndexes.length;
1125
1704
  while (i > 0 && i--) {
@@ -1131,11 +1710,38 @@ export const Paper = View.extend({
1131
1710
 
1132
1711
  updateViewsAsync: function(opt, data) {
1133
1712
  opt || (opt = {});
1134
- data || (data = { processed: 0, priority: MIN_PRIORITY });
1713
+ data || (data = {
1714
+ processed: 0,
1715
+ priority: MIN_PRIORITY,
1716
+ checkedUnmounted: 0,
1717
+ checkedMounted: 0,
1718
+ });
1135
1719
  const { _updates: updates, options } = this;
1136
- const id = updates.id;
1137
- if (id) {
1720
+ const { id, mountedList, unmountedList, freshAfterReset } = updates;
1721
+
1722
+ // Should we run the next batch update this frame?
1723
+ let runBatchUpdate = true;
1724
+ if (!id) {
1725
+ // If there's no scheduled frame, no batch update is needed.
1726
+ runBatchUpdate = false;
1727
+ } else {
1728
+ // Cancel any scheduled frame.
1138
1729
  cancelFrame(id);
1730
+ if (freshAfterReset) {
1731
+ // First update after a reset.
1732
+ updates.freshAfterReset = false;
1733
+ // When `initializeUnmounted` is enabled, there are no scheduled updates.
1734
+ // We check whether the `mountedList` and `unmountedList` are empty.
1735
+ if (!this.legacyMode && mountedList.length === 0 && unmountedList.length === 0) {
1736
+ // No updates to process; We trigger before/after render events via `updateViews`.
1737
+ // Note: If `autoFreeze` is enabled, 'idle' event triggers next frame.
1738
+ this.updateViews();
1739
+ runBatchUpdate = false;
1740
+ }
1741
+ }
1742
+ }
1743
+
1744
+ if (runBatchUpdate) {
1139
1745
  if (data.processed === 0 && this.hasScheduledUpdates()) {
1140
1746
  this.notifyBeforeRender(opt);
1141
1747
  }
@@ -1144,7 +1750,7 @@ export const Paper = View.extend({
1144
1750
  mountBatchSize: MOUNT_BATCH_SIZE - stats.mounted,
1145
1751
  unmountBatchSize: MOUNT_BATCH_SIZE - stats.unmounted
1146
1752
  });
1147
- const checkStats = this.checkViewport(passingOpt);
1753
+ const checkStats = this.scheduleCellsVisibilityUpdate(passingOpt);
1148
1754
  const unmountCount = checkStats.unmounted;
1149
1755
  const mountCount = checkStats.mounted;
1150
1756
  let processed = data.processed;
@@ -1165,11 +1771,22 @@ export const Paper = View.extend({
1165
1771
  } else {
1166
1772
  data.processed = processed;
1167
1773
  }
1774
+ data.checkedUnmounted = 0;
1775
+ data.checkedMounted = 0;
1168
1776
  } else {
1169
- if (!updates.idle) {
1170
- if (options.autoFreeze) {
1777
+ data.checkedUnmounted += Math.max(passingOpt.mountBatchSize, 0);
1778
+ data.checkedMounted += Math.max(passingOpt.unmountBatchSize, 0);
1779
+ // The `scheduleCellsVisibilityUpdate` could have scheduled some insertions
1780
+ // (note that removals are currently done synchronously).
1781
+ if (options.autoFreeze && !this.hasScheduledUpdates()) {
1782
+ // If there are no updates scheduled and we checked all unmounted views,
1783
+ if (
1784
+ data.checkedUnmounted >= unmountedList.length &&
1785
+ data.checkedMounted >= mountedList.length
1786
+ ) {
1787
+ // We freeze the paper and notify the idle state.
1171
1788
  this.freeze();
1172
- updates.idle = true;
1789
+ updates.idle = { wakeUpOptions: opt };
1173
1790
  this.trigger('render:idle', opt);
1174
1791
  }
1175
1792
  }
@@ -1189,6 +1806,7 @@ export const Paper = View.extend({
1189
1806
  },
1190
1807
 
1191
1808
  notifyBeforeRender: function(opt = {}) {
1809
+ if (opt.silent) return;
1192
1810
  let beforeFn = opt.beforeRender;
1193
1811
  if (typeof beforeFn !== 'function') {
1194
1812
  beforeFn = this.options.beforeRender;
@@ -1198,6 +1816,7 @@ export const Paper = View.extend({
1198
1816
  },
1199
1817
 
1200
1818
  notifyAfterRender: function(stats, opt = {}) {
1819
+ if (opt.silent) return;
1201
1820
  let afterFn = opt.afterRender;
1202
1821
  if (typeof afterFn !== 'function') {
1203
1822
  afterFn = this.options.afterRender;
@@ -1208,6 +1827,56 @@ export const Paper = View.extend({
1208
1827
  this.trigger('render:done', stats, opt);
1209
1828
  },
1210
1829
 
1830
+ prioritizeCellViewMount: function(cellOrId) {
1831
+ if (!cellOrId) return false;
1832
+ const cid = this._idToCid[cellOrId.id || cellOrId];
1833
+ if (!cid) return false;
1834
+ const { unmountedList } = this._updates;
1835
+ if (!unmountedList.has(cid)) return false;
1836
+ // Move the view to the head of the mounted list
1837
+ unmountedList.moveToHead(cid);
1838
+ return true;
1839
+ },
1840
+
1841
+ prioritizeCellViewUnmount: function(cellOrId) {
1842
+ if (!cellOrId) return false;
1843
+ const cid = this._idToCid[cellOrId.id || cellOrId];
1844
+ if (!cid) return false;
1845
+ const { mountedList } = this._updates;
1846
+ if (!mountedList.has(cid)) return false;
1847
+ // Move the view to the head of the unmounted list
1848
+ mountedList.moveToHead(cid);
1849
+ return true;
1850
+ },
1851
+
1852
+ _evalCellVisibility: function(viewLike, isMounted, visibilityCallback) {
1853
+ if (!visibilityCallback || !viewLike.DETACHABLE) return true;
1854
+ if (this.legacyMode) {
1855
+ return visibilityCallback.call(this, viewLike, isMounted, this);
1856
+ }
1857
+ // The visibility check runs for CellView only.
1858
+ if (!viewLike[CELL_VIEW_MARKER] && !viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) return true;
1859
+ // The cellView model must be a member of this graph.
1860
+ if (viewLike.model.graph !== this.model) {
1861
+ // It could have been removed from the graph.
1862
+ // If the view was mounted, we keep it mounted.
1863
+ return isMounted;
1864
+ }
1865
+ return visibilityCallback.call(this, viewLike.model, isMounted, this);
1866
+ },
1867
+
1868
+ _getCellVisibilityCallback: function(opt) {
1869
+ const { options } = this;
1870
+ if (this.legacyMode) {
1871
+ const viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
1872
+ if (typeof viewportFn === 'function') return viewportFn;
1873
+ } else {
1874
+ const isVisibleFn = 'cellVisibility' in opt ? opt.cellVisibility : options.cellVisibility;
1875
+ if (typeof isVisibleFn === 'function') return isVisibleFn;
1876
+ }
1877
+ return null;
1878
+ },
1879
+
1211
1880
  updateViewsBatch: function(opt) {
1212
1881
  opt || (opt = {});
1213
1882
  var batchSize = opt.batchSize || UPDATE_BATCH_SIZE;
@@ -1220,8 +1889,7 @@ export const Paper = View.extend({
1220
1889
  var empty = true;
1221
1890
  var options = this.options;
1222
1891
  var priorities = updates.priorities;
1223
- var viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
1224
- if (typeof viewportFn !== 'function') viewportFn = null;
1892
+ const visibilityCb = this._getCellVisibilityCallback(opt);
1225
1893
  var postponeViewFn = options.onViewPostponed;
1226
1894
  if (typeof postponeViewFn !== 'function') postponeViewFn = null;
1227
1895
  var priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
@@ -1233,33 +1901,56 @@ export const Paper = View.extend({
1233
1901
  empty = false;
1234
1902
  break main;
1235
1903
  }
1236
- var view = views[cid];
1904
+ var view = viewsRegistry[cid];
1237
1905
  if (!view) {
1238
- // This should not occur
1239
- delete priorityUpdates[cid];
1240
- continue;
1906
+ view = this._viewPlaceholders[cid];
1907
+ if (!view) {
1908
+ /**
1909
+ * This can occur when:
1910
+ * - the model is removed and a new model with the same id is added
1911
+ * - the view `initialize` method was overridden and the view was not registered
1912
+ * - an mvc.View scheduled an update, was removed and paper was not notified
1913
+ */
1914
+ delete priorityUpdates[cid];
1915
+ continue;
1916
+ }
1241
1917
  }
1242
1918
  var currentFlag = priorityUpdates[cid];
1243
- if ((currentFlag & view.FLAG_REMOVE) === 0) {
1919
+ if ((currentFlag & this.FLAG_REMOVE) === 0) {
1244
1920
  // We should never check a view for viewport if we are about to remove the view
1245
- var isDetached = cid in updates.unmounted;
1246
- if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, !isDetached, this)) {
1921
+ const isMounted = !updates.unmountedList.has(cid);
1922
+ if (!this._evalCellVisibility(view, isMounted, visibilityCb)) {
1247
1923
  // Unmount View
1248
- if (!isDetached) {
1924
+ if (isMounted) {
1925
+ // The view is currently mounted. Hide the view (detach or remove it).
1249
1926
  this.registerUnmountedView(view);
1250
- this.detachView(view);
1927
+ this._hideView(view);
1928
+ } else {
1929
+ // The view is not mounted. We can just update the unmounted list.
1930
+ // We ADD the current flag to the flag that was already scheduled.
1931
+ this._mergeUnmountedViewScheduledUpdates(cid, currentFlag);
1251
1932
  }
1252
- updates.unmounted[cid] |= currentFlag;
1933
+ // Delete the current update as it has been processed.
1253
1934
  delete priorityUpdates[cid];
1254
1935
  unmountCount++;
1255
1936
  continue;
1256
1937
  }
1257
1938
  // Mount View
1258
- if (isDetached) {
1259
- currentFlag |= view.FLAG_INSERT;
1939
+ if (view[CELL_VIEW_PLACEHOLDER_MARKER]) {
1940
+ view = this._resolveCellViewPlaceholder(view);
1941
+ // Newly initialized view needs to be initialized
1942
+ currentFlag |= this.getCellViewInitFlag(view);
1943
+ }
1944
+
1945
+ if (!isMounted) {
1946
+ currentFlag |= this.FLAG_INSERT;
1260
1947
  mountCount++;
1261
1948
  }
1262
1949
  currentFlag |= this.registerMountedView(view);
1950
+ } else if (view[CELL_VIEW_PLACEHOLDER_MARKER]) {
1951
+ // We are trying to remove a placeholder view.
1952
+ // This should not occur as the placeholder should have been unregistered
1953
+ continue;
1263
1954
  }
1264
1955
  var leftoverFlag = this.updateView(view, currentFlag, opt);
1265
1956
  if (leftoverFlag > 0) {
@@ -1286,104 +1977,125 @@ export const Paper = View.extend({
1286
1977
  };
1287
1978
  },
1288
1979
 
1980
+ getCellViewInitFlag: function(cellView) {
1981
+ return this.FLAG_INIT | cellView.getFlag(result(cellView, 'initFlag'));
1982
+ },
1983
+
1984
+ /**
1985
+ * @ignore This method returns an array of cellViewLike objects and therefore
1986
+ * is meant for internal/test use only.
1987
+ * The view placeholders are not exposed via public API.
1988
+ */
1289
1989
  getUnmountedViews: function() {
1290
1990
  const updates = this._updates;
1291
- const unmountedCids = Object.keys(updates.unmounted);
1292
- const n = unmountedCids.length;
1293
- const unmountedViews = new Array(n);
1294
- for (var i = 0; i < n; i++) {
1295
- unmountedViews[i] = views[unmountedCids[i]];
1991
+ const unmountedViews = new Array(updates.unmountedList.length);
1992
+ const unmountedCids = updates.unmountedList.keys();
1993
+ let i = 0;
1994
+ for (const cid of unmountedCids) {
1995
+ // If the view is a placeholder, it won't be in the global views map
1996
+ // If the view is not a cell view, it won't be in the viewPlaceholders map
1997
+ unmountedViews[i++] = viewsRegistry[cid] || this._viewPlaceholders[cid];
1296
1998
  }
1297
1999
  return unmountedViews;
1298
2000
  },
1299
2001
 
2002
+ /**
2003
+ * @ignore This method returns an array of cellViewLike objects and therefore
2004
+ * is meant for internal/test use only.
2005
+ * The view placeholders are not exposed via public API.
2006
+ */
1300
2007
  getMountedViews: function() {
1301
2008
  const updates = this._updates;
1302
- const mountedCids = Object.keys(updates.mounted);
1303
- const n = mountedCids.length;
1304
- const mountedViews = new Array(n);
1305
- for (var i = 0; i < n; i++) {
1306
- mountedViews[i] = views[mountedCids[i]];
2009
+ const mountedViews = new Array(updates.mountedList.length);
2010
+ const mountedCids = updates.mountedList.keys();
2011
+ let i = 0;
2012
+ for (const cid of mountedCids) {
2013
+ mountedViews[i++] = viewsRegistry[cid] || this._viewPlaceholders[cid];
1307
2014
  }
1308
2015
  return mountedViews;
1309
2016
  },
1310
2017
 
1311
- checkUnmountedViews: function(viewportFn, opt) {
2018
+ checkUnmountedViews: function(visibilityCb, opt) {
1312
2019
  opt || (opt = {});
1313
2020
  var mountCount = 0;
1314
- if (typeof viewportFn !== 'function') viewportFn = null;
2021
+ if (typeof visibilityCb !== 'function') visibilityCb = null;
1315
2022
  var batchSize = 'mountBatchSize' in opt ? opt.mountBatchSize : Infinity;
1316
2023
  var updates = this._updates;
1317
- var unmountedCids = updates.unmountedCids;
1318
- var unmounted = updates.unmounted;
1319
- for (var i = 0, n = Math.min(unmountedCids.length, batchSize); i < n; i++) {
1320
- var cid = unmountedCids[i];
1321
- if (!(cid in unmounted)) continue;
1322
- var view = views[cid];
1323
- if (!view) continue;
1324
- if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, false, this)) {
2024
+ var unmountedList = updates.unmountedList;
2025
+ for (var i = 0, n = Math.min(unmountedList.length, batchSize); i < n; i++) {
2026
+ const { key: cid } = unmountedList.peekHead();
2027
+ let view = viewsRegistry[cid] || this._viewPlaceholders[cid];
2028
+ if (!view) {
2029
+ // This should not occur
2030
+ continue;
2031
+ }
2032
+ if (!this._evalCellVisibility(view, false, visibilityCb)) {
1325
2033
  // Push at the end of all unmounted ids, so this can be check later again
1326
- unmountedCids.push(cid);
2034
+ unmountedList.rotate();
1327
2035
  continue;
1328
2036
  }
2037
+ // Remove the view from the unmounted list
2038
+ const { value: prevFlag } = unmountedList.popHead();
1329
2039
  mountCount++;
1330
- var flag = this.registerMountedView(view);
2040
+ const flag = this.registerMountedView(view) | prevFlag;
1331
2041
  if (flag) this.scheduleViewUpdate(view, flag, view.UPDATE_PRIORITY, { mounting: true });
1332
2042
  }
1333
- // Get rid of views, that have been mounted
1334
- unmountedCids.splice(0, i);
1335
2043
  return mountCount;
1336
2044
  },
1337
2045
 
1338
- checkMountedViews: function(viewportFn, opt) {
2046
+ checkMountedViews: function(visibilityCb, opt) {
1339
2047
  opt || (opt = {});
1340
2048
  var unmountCount = 0;
1341
- if (typeof viewportFn !== 'function') return unmountCount;
2049
+ if (typeof visibilityCb !== 'function') return unmountCount;
1342
2050
  var batchSize = 'unmountBatchSize' in opt ? opt.unmountBatchSize : Infinity;
1343
2051
  var updates = this._updates;
1344
- var mountedCids = updates.mountedCids;
1345
- var mounted = updates.mounted;
1346
- for (var i = 0, n = Math.min(mountedCids.length, batchSize); i < n; i++) {
1347
- var cid = mountedCids[i];
1348
- if (!(cid in mounted)) continue;
1349
- var view = views[cid];
1350
- if (!view) continue;
1351
- if (!view.DETACHABLE || viewportFn.call(this, view, true, this)) {
2052
+ const mountedList = updates.mountedList;
2053
+ for (var i = 0, n = Math.min(mountedList.length, batchSize); i < n; i++) {
2054
+ const { key: cid } = mountedList.peekHead();
2055
+ const view = viewsRegistry[cid];
2056
+ if (!view) {
2057
+ // A view (not a cell view) has been removed from the paper.
2058
+ // Remove it from the mounted list and continue.
2059
+ mountedList.popHead();
2060
+ continue;
2061
+ }
2062
+ if (this._evalCellVisibility(view, true, visibilityCb)) {
1352
2063
  // Push at the end of all mounted ids, so this can be check later again
1353
- mountedCids.push(cid);
2064
+ mountedList.rotate();
1354
2065
  continue;
1355
2066
  }
2067
+ // Remove the view from the mounted list
2068
+ mountedList.popHead();
1356
2069
  unmountCount++;
1357
2070
  var flag = this.registerUnmountedView(view);
1358
- if (flag) this.detachView(view);
2071
+ if (flag) {
2072
+ this._hideView(view);
2073
+ }
1359
2074
  }
1360
- // Get rid of views, that have been unmounted
1361
- mountedCids.splice(0, i);
1362
2075
  return unmountCount;
1363
2076
  },
1364
2077
 
1365
2078
  checkViewVisibility: function(cellView, opt = {}) {
1366
- let viewportFn = 'viewport' in opt ? opt.viewport : this.options.viewport;
1367
- if (typeof viewportFn !== 'function') viewportFn = null;
2079
+ const visibilityCb = this._getCellVisibilityCallback(opt);
1368
2080
  const updates = this._updates;
1369
- const { mounted, unmounted } = updates;
1370
- const visible = !cellView.DETACHABLE || !viewportFn || viewportFn.call(this, cellView, false, this);
2081
+ const { mountedList, unmountedList } = updates;
2082
+
2083
+ const visible = this._evalCellVisibility(cellView, false, visibilityCb);
1371
2084
 
1372
2085
  let isUnmounted = false;
1373
2086
  let isMounted = false;
1374
2087
 
1375
- if (cellView.cid in mounted && !visible) {
2088
+ if (mountedList.has(cellView.cid) && !visible) {
1376
2089
  const flag = this.registerUnmountedView(cellView);
1377
- if (flag) this.detachView(cellView);
1378
- const i = updates.mountedCids.indexOf(cellView.cid);
1379
- updates.mountedCids.splice(i, 1);
2090
+ if (flag) this._hideView(cellView);
2091
+ mountedList.delete(cellView.cid);
1380
2092
  isUnmounted = true;
1381
2093
  }
1382
2094
 
1383
- if (!isUnmounted && cellView.cid in unmounted && visible) {
1384
- const i = updates.unmountedCids.indexOf(cellView.cid);
1385
- updates.unmountedCids.splice(i, 1);
1386
- var flag = this.registerMountedView(cellView);
2095
+ if (!isUnmounted && unmountedList.has(cellView.cid) && visible) {
2096
+ const unmountedItem = unmountedList.get(cellView.cid);
2097
+ unmountedList.delete(cellView.cid);
2098
+ const flag = unmountedItem.value | this.registerMountedView(cellView);
1387
2099
  if (flag) this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, { mounting: true });
1388
2100
  isMounted = true;
1389
2101
  }
@@ -1394,25 +2106,65 @@ export const Paper = View.extend({
1394
2106
  };
1395
2107
  },
1396
2108
 
1397
- checkViewport: function(opt) {
1398
- var passingOpt = defaults({}, opt, {
2109
+ /**
2110
+ * @public
2111
+ * Update the visibility of a single cell.
2112
+ */
2113
+ updateCellVisibility: function(cell, opt = {}) {
2114
+ const cellViewLike = this._getCellViewLike(cell);
2115
+ if (!cellViewLike) return;
2116
+ const stats = this.checkViewVisibility(cellViewLike, opt);
2117
+ // Note: `unmounted` views are removed immediately
2118
+ if (stats.mounted > 0) {
2119
+ // Mounting is scheduled. Run the update.
2120
+ // Note: the view might be a placeholder.
2121
+ this.requireView(cell, opt);
2122
+ }
2123
+ },
2124
+
2125
+ /**
2126
+ * @public
2127
+ * Update the visibility of all cells.
2128
+ */
2129
+ updateCellsVisibility: function(opt = {}) {
2130
+ // Check the visibility of all cells and schedule their updates.
2131
+ this.scheduleCellsVisibilityUpdate(opt);
2132
+ // Perform the scheduled updates while avoiding re-evaluating the visibility.
2133
+ const keepCurrentVisibility = (_, isVisible) => isVisible;
2134
+ this.updateViews({ ...opt, cellVisibility: keepCurrentVisibility });
2135
+ },
2136
+
2137
+ /**
2138
+ * @protected
2139
+ * Run visibility checks for all cells and schedule their updates.
2140
+ */
2141
+ scheduleCellsVisibilityUpdate(opt) {
2142
+ const passingOpt = defaults({}, opt, {
1399
2143
  mountBatchSize: Infinity,
1400
2144
  unmountBatchSize: Infinity
1401
2145
  });
1402
- var viewportFn = 'viewport' in passingOpt ? passingOpt.viewport : this.options.viewport;
1403
- var unmountedCount = this.checkMountedViews(viewportFn, passingOpt);
2146
+ const visibilityCb = this._getCellVisibilityCallback(passingOpt);
2147
+ const unmountedCount = this.checkMountedViews(visibilityCb, passingOpt);
1404
2148
  if (unmountedCount > 0) {
1405
2149
  // Do not check views, that have been just unmounted and pushed at the end of the cids array
1406
- var unmountedCids = this._updates.unmountedCids;
1407
- passingOpt.mountBatchSize = Math.min(unmountedCids.length - unmountedCount, passingOpt.mountBatchSize);
2150
+ var unmountedList = this._updates.unmountedList;
2151
+ passingOpt.mountBatchSize = Math.min(unmountedList.length - unmountedCount, passingOpt.mountBatchSize);
1408
2152
  }
1409
- var mountedCount = this.checkUnmountedViews(viewportFn, passingOpt);
2153
+ const mountedCount = this.checkUnmountedViews(visibilityCb, passingOpt);
1410
2154
  return {
1411
2155
  mounted: mountedCount,
1412
2156
  unmounted: unmountedCount
1413
2157
  };
1414
2158
  },
1415
2159
 
2160
+ /**
2161
+ * @deprecated use `updateCellsVisibility` instead
2162
+ * This method will be renamed and made private in the future.
2163
+ */
2164
+ checkViewport: function(opt) {
2165
+ return this.scheduleCellsVisibilityUpdate(opt);
2166
+ },
2167
+
1416
2168
  freeze: function(opt) {
1417
2169
  opt || (opt = {});
1418
2170
  var updates = this._updates;
@@ -1428,6 +2180,10 @@ export const Paper = View.extend({
1428
2180
  this.options.frozen = true;
1429
2181
  var id = updates.id;
1430
2182
  updates.id = null;
2183
+ if (!this.legacyMode) {
2184
+ // Make sure the `freeze()` method ends the idle state.
2185
+ updates.idle = false;
2186
+ }
1431
2187
  if (this.isAsync() && id) cancelFrame(id);
1432
2188
  },
1433
2189
 
@@ -1441,6 +2197,7 @@ export const Paper = View.extend({
1441
2197
  updates.freezeKey = null;
1442
2198
  // key passed, but the paper is already freezed
1443
2199
  if (key && key === freezeKey && updates.keyFrozen) return;
2200
+ updates.idle = false;
1444
2201
  if (this.isAsync()) {
1445
2202
  this.freeze();
1446
2203
  this.updateViewsAsync(opt);
@@ -1449,17 +2206,30 @@ export const Paper = View.extend({
1449
2206
  }
1450
2207
  this.options.frozen = updates.keyFrozen = false;
1451
2208
  if (updates.sort) {
1452
- this.sortViews();
2209
+ this.sortLayerViews();
1453
2210
  updates.sort = false;
1454
2211
  }
1455
2212
  },
1456
2213
 
2214
+ wakeUp: function() {
2215
+ if (!this.isIdle()) return;
2216
+ this.unfreeze(this._updates.idle.wakeUpOptions);
2217
+ },
2218
+
1457
2219
  isAsync: function() {
1458
2220
  return !!this.options.async;
1459
2221
  },
1460
2222
 
1461
2223
  isFrozen: function() {
1462
- return !!this.options.frozen;
2224
+ return !!this.options.frozen && !this.isIdle();
2225
+ },
2226
+
2227
+ isIdle: function() {
2228
+ if (this.legacyMode) {
2229
+ // Not implemented in the legacy mode.
2230
+ return false;
2231
+ }
2232
+ return !!(this._updates && this._updates.idle);
1463
2233
  },
1464
2234
 
1465
2235
  isExactSorting: function() {
@@ -1471,8 +2241,8 @@ export const Paper = View.extend({
1471
2241
  this.freeze();
1472
2242
  this._updates.disabled = true;
1473
2243
  //clean up all DOM elements/views to prevent memory leaks
1474
- this.removeLayers();
1475
2244
  this.removeViews();
2245
+ this._removeLayerViews();
1476
2246
  },
1477
2247
 
1478
2248
  getComputedSize: function() {
@@ -1720,7 +2490,17 @@ export const Paper = View.extend({
1720
2490
  return this.model.getBBox() || new Rect();
1721
2491
  }
1722
2492
 
1723
- return V(this.cells).getBBox();
2493
+ const graphLayerViews = this.getGraphLayerViews();
2494
+ // Return an empty rectangle if there are no layers
2495
+ // should not happen in practice
2496
+ if (graphLayerViews.length === 0) {
2497
+ return new Rect();
2498
+ }
2499
+
2500
+ // Combine content area rectangles from all layers,
2501
+ // considering only graph layer views to exclude non-cell elements (e.g., grid, tools)
2502
+ const bbox = g.Rect.fromRectUnion(...graphLayerViews.map(view => view.vel.getBBox()));
2503
+ return bbox;
1724
2504
  },
1725
2505
 
1726
2506
  // Return the dimensions of the content bbox in the paper units (as it appears on screen).
@@ -1759,21 +2539,57 @@ export const Paper = View.extend({
1759
2539
  return restrictedArea;
1760
2540
  },
1761
2541
 
1762
- createViewForModel: function(cell) {
2542
+ _resolveCellViewPlaceholder: function(placeholder) {
2543
+ const { model, viewClass, cid } = placeholder;
2544
+ const view = this._initializeCellView(viewClass, model, cid);
2545
+ this._registerCellView(view);
2546
+ this._unregisterCellViewPlaceholder(placeholder);
2547
+ return view;
2548
+ },
1763
2549
 
1764
- const { options } = this;
1765
- // A class taken from the paper options.
1766
- var optionalViewClass;
2550
+ _registerCellViewPlaceholder: function(cell, cid = uniqueId('view')) {
2551
+ const ViewClass = this._resolveCellViewClass(cell);
2552
+ const placeholder = {
2553
+ // A tag to identify the placeholder from a CellView.
2554
+ [CELL_VIEW_PLACEHOLDER_MARKER]: true,
2555
+ cid,
2556
+ model: cell,
2557
+ DETACHABLE: true,
2558
+ viewClass: ViewClass,
2559
+ UPDATE_PRIORITY: ViewClass.prototype.UPDATE_PRIORITY,
2560
+ };
2561
+ this._viewPlaceholders[cid] = placeholder;
2562
+ return placeholder;
2563
+ },
1767
2564
 
1768
- // A default basic class (either dia.ElementView or dia.LinkView)
1769
- var defaultViewClass;
2565
+ _registerCellView: function(cellView) {
2566
+ cellView.paper = this;
2567
+ this._views[cellView.model.id] = cellView;
2568
+ },
1770
2569
 
1771
- // A special class defined for this model in the corresponding namespace.
1772
- // e.g. joint.shapes.standard.Rectangle searches for joint.shapes.standard.RectangleView
1773
- var namespace = options.cellViewNamespace;
1774
- var type = cell.get('type') + 'View';
1775
- var namespaceViewClass = getByPath(namespace, type, '.');
2570
+ _unregisterCellViewPlaceholder: function(placeholder) {
2571
+ delete this._viewPlaceholders[placeholder.cid];
2572
+ },
1776
2573
 
2574
+ _initializeCellView: function(ViewClass, cell, cid) {
2575
+ const { options } = this;
2576
+ const { interactive, labelsLayer } = options;
2577
+ return new ViewClass({
2578
+ cid,
2579
+ model: cell,
2580
+ interactive,
2581
+ labelsLayer: labelsLayer === true ? paperLayers.LABELS : labelsLayer
2582
+ });
2583
+ },
2584
+
2585
+ _resolveCellViewClass: function(cell) {
2586
+ const { options } = this;
2587
+ const { cellViewNamespace } = options;
2588
+ const type = cell.get('type') + 'View';
2589
+ const namespaceViewClass = getByPath(cellViewNamespace, type, '.');
2590
+ // A class taken from the paper options.
2591
+ let optionalViewClass;
2592
+ let defaultViewClass;
1777
2593
  if (cell.isLink()) {
1778
2594
  optionalViewClass = options.linkView;
1779
2595
  defaultViewClass = LinkView;
@@ -1781,7 +2597,6 @@ export const Paper = View.extend({
1781
2597
  optionalViewClass = options.elementView;
1782
2598
  defaultViewClass = ElementView;
1783
2599
  }
1784
-
1785
2600
  // a) the paper options view is a class (deprecated)
1786
2601
  // 1. search the namespace for a view
1787
2602
  // 2. if no view was found, use view from the paper options
@@ -1789,29 +2604,54 @@ export const Paper = View.extend({
1789
2604
  // 1. call the function from the paper options
1790
2605
  // 2. if no view was return, search the namespace for a view
1791
2606
  // 3. if no view was found, use the default
1792
- var ViewClass = (optionalViewClass.prototype instanceof ViewBase)
2607
+ return (optionalViewClass.prototype instanceof ViewBase)
1793
2608
  ? namespaceViewClass || optionalViewClass
1794
2609
  : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass;
2610
+ },
1795
2611
 
1796
- return new ViewClass({
1797
- model: cell,
1798
- interactive: options.interactive,
1799
- labelsLayer: options.labelsLayer === true ? LayersNames.LABELS : options.labelsLayer
1800
- });
2612
+ // Returns a CellView instance or its placeholder for the given cell.
2613
+ _getCellViewLike: function(cell) {
2614
+
2615
+ let id;
2616
+ if (isString(cell) || isNumber(cell)) {
2617
+ // If the cell is a string or number, it is an id of the view.
2618
+ id = cell;
2619
+ } else if (cell) {
2620
+ // If the cell is an object, it should have an id property.
2621
+ id = cell.id;
2622
+ } else {
2623
+ // If the cell is falsy, return null.
2624
+ return null;
2625
+ }
2626
+
2627
+ const view = this._views[id];
2628
+ if (view) return view;
2629
+
2630
+ // If the view is not found, it may be a placeholder
2631
+ const cid = this._idToCid[id];
2632
+ if (cid) {
2633
+ return this._viewPlaceholders[cid];
2634
+ }
2635
+
2636
+ return null;
1801
2637
  },
1802
2638
 
1803
- removeView: function(cell) {
2639
+ createViewForModel: function(cell, cid) {
2640
+ return this._initializeCellView(this._resolveCellViewClass(cell), cell, cid);
2641
+ },
1804
2642
 
2643
+ removeView: function(cell) {
1805
2644
  const { id } = cell;
1806
2645
  const { _views, _updates } = this;
1807
2646
  const view = _views[id];
1808
2647
  if (view) {
1809
2648
  var { cid } = view;
1810
- const { mounted, unmounted } = _updates;
2649
+ const { mountedList, unmountedList } = _updates;
1811
2650
  view.remove();
1812
2651
  delete _views[id];
1813
- delete mounted[cid];
1814
- delete unmounted[cid];
2652
+ delete this._idToCid[id];
2653
+ mountedList.delete(cid);
2654
+ unmountedList.delete(cid);
1815
2655
  }
1816
2656
  return view;
1817
2657
  },
@@ -1825,7 +2665,7 @@ export const Paper = View.extend({
1825
2665
  if (id in views) {
1826
2666
  view = views[id];
1827
2667
  if (view.model === cell) {
1828
- flag = view.FLAG_INSERT;
2668
+ flag = this.FLAG_INSERT;
1829
2669
  create = false;
1830
2670
  } else {
1831
2671
  // The view for this `id` already exist.
@@ -1835,14 +2675,42 @@ export const Paper = View.extend({
1835
2675
  }
1836
2676
  }
1837
2677
  if (create) {
1838
- view = views[id] = this.createViewForModel(cell);
1839
- view.paper = this;
1840
- flag = this.registerUnmountedView(view) | this.FLAG_INIT | view.getFlag(result(view, 'initFlag'));
2678
+ const { viewManagement } = this.options;
2679
+ const cid = uniqueId('view');
2680
+ this._idToCid[cell.id] = cid;
2681
+ if (viewManagement.lazyInitialize) {
2682
+ // Register only a placeholder for the view
2683
+ view = this._registerCellViewPlaceholder(cell, cid);
2684
+ flag = this.registerUnmountedView(view);
2685
+ } else {
2686
+ // Create a new view instance
2687
+ view = this.createViewForModel(cell, cid);
2688
+ this._registerCellView(view);
2689
+ flag = this.registerUnmountedView(view);
2690
+ // The newly created view needs to be initialized
2691
+ flag |= this.getCellViewInitFlag(view);
2692
+ }
2693
+ if (viewManagement.initializeUnmounted) {
2694
+ // Save the initialization flags for later and exit early
2695
+ this._mergeUnmountedViewScheduledUpdates(cid, flag);
2696
+ return view;
2697
+ }
1841
2698
  }
2699
+
1842
2700
  this.requestViewUpdate(view, flag, view.UPDATE_PRIORITY, opt);
2701
+
1843
2702
  return view;
1844
2703
  },
1845
2704
 
2705
+ // Update the view flags in the `unmountedList` using the bitwise OR operation
2706
+ _mergeUnmountedViewScheduledUpdates: function(cid, flag) {
2707
+ const { unmountedList } = this._updates;
2708
+ const unmountedItem = unmountedList.get(cid);
2709
+ if (unmountedItem) {
2710
+ unmountedItem.value |= flag;
2711
+ }
2712
+ },
2713
+
1846
2714
  onImageDragStart: function() {
1847
2715
  // This is the only way to prevent image dragging in Firefox that works.
1848
2716
  // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help.
@@ -1853,78 +2721,112 @@ export const Paper = View.extend({
1853
2721
  resetViews: function(cells, opt) {
1854
2722
  opt || (opt = {});
1855
2723
  cells || (cells = []);
2724
+ // Allows to unfreeze normally while in the idle state using autoFreeze option
2725
+ const key = (this.legacyMode ? this.options.autoFreeze : this.isIdle()) ? null : 'reset';
1856
2726
  this._resetUpdates();
1857
2727
  // clearing views removes any event listeners
1858
2728
  this.removeViews();
1859
- // Allows to unfreeze normally while in the idle state using autoFreeze option
1860
- const key = this.options.autoFreeze ? null : 'reset';
1861
2729
  this.freeze({ key });
1862
2730
  for (var i = 0, n = cells.length; i < n; i++) {
1863
2731
  this.renderView(cells[i], opt);
1864
2732
  }
1865
2733
  this.unfreeze({ key });
1866
- this.sortViews();
2734
+ this.sortLayerViews();
1867
2735
  },
1868
2736
 
1869
2737
  removeViews: function() {
1870
-
1871
- invoke(this._views, 'remove');
1872
-
2738
+ // Remove all views and their references from the paper.
2739
+ for (const id in this._views) {
2740
+ const view = this._views[id];
2741
+ if (view) {
2742
+ view.remove();
2743
+ }
2744
+ }
1873
2745
  this._views = {};
2746
+ this._viewPlaceholders = {};
2747
+ this._idToCid = {};
1874
2748
  },
1875
2749
 
1876
- sortViews: function() {
1877
-
2750
+ sortLayerViews: function() {
1878
2751
  if (!this.isExactSorting()) {
1879
2752
  // noop
1880
2753
  return;
1881
2754
  }
1882
- if (this.isFrozen()) {
2755
+ if (this.isFrozen() || this.isIdle()) {
1883
2756
  // sort views once unfrozen
1884
2757
  this._updates.sort = true;
1885
2758
  return;
1886
2759
  }
1887
- this.sortViewsExact();
2760
+ this.sortLayerViewsExact();
1888
2761
  },
1889
2762
 
1890
- sortViewsExact: function() {
2763
+ sortLayerViewsExact: function() {
2764
+ this.getGraphLayerViews().forEach(view => view.sortExact());
2765
+ },
1891
2766
 
1892
- // Run insertion sort algorithm in order to efficiently sort DOM elements according to their
1893
- // associated model `z` attribute.
2767
+ insertView: function(view, isInitialInsert) {
1894
2768
 
1895
- var cellNodes = Array.from(this.cells.childNodes).filter(node => node.getAttribute('model-id'));
1896
- var cells = this.model.get('cells');
2769
+ // layer can be null if it is added to the graph with 'dry' option
2770
+ const layerId = this.model.getCellLayerId(view.model);
2771
+ const layerView = this.getLayerView(layerId);
1897
2772
 
1898
- sortElements(cellNodes, function(a, b) {
1899
- var cellA = cells.get(a.getAttribute('model-id'));
1900
- var cellB = cells.get(b.getAttribute('model-id'));
1901
- var zA = cellA.attributes.z || 0;
1902
- var zB = cellB.attributes.z || 0;
1903
- return (zA === zB) ? 0 : (zA < zB) ? -1 : 1;
1904
- });
2773
+ layerView.insertCellView(view);
2774
+
2775
+ view.onMount(isInitialInsert);
1905
2776
  },
1906
2777
 
1907
- insertView: function(view, isInitialInsert) {
1908
- const { el, model } = view;
2778
+ _hideView: function(viewLike) {
2779
+ if (!viewLike || viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) {
2780
+ // A placeholder view was never mounted
2781
+ return;
2782
+ }
2783
+ if (viewLike[CELL_VIEW_MARKER]) {
2784
+ this._hideCellView(viewLike);
2785
+ } else {
2786
+ // A generic view that is not a cell view.
2787
+ viewLike.unmount();
2788
+ }
2789
+ },
1909
2790
 
1910
- const layerName = model.get('layer') || this.DEFAULT_CELL_LAYER;
1911
- const layerView = this.getLayerView(layerName);
2791
+ // If `cellVisibility` returns `false`, the view will be hidden using this method.
2792
+ _hideCellView: function(cellView) {
2793
+ if (this.options.viewManagement.disposeHidden) {
2794
+ if (this._disposeCellView(cellView)) return;
2795
+ }
2796
+ // Detach the view from the paper, but keep it in memory
2797
+ this._detachCellView(cellView);
2798
+ },
1912
2799
 
1913
- switch (this.options.sorting) {
1914
- case sortingTypes.APPROX:
1915
- layerView.insertSortedNode(el, model.get('z'));
1916
- break;
1917
- case sortingTypes.EXACT:
1918
- default:
1919
- layerView.insertNode(el);
1920
- break;
2800
+ _disposeCellView: function(cellView) {
2801
+ if (HighlighterView.has(cellView) || cellView.hasTools()) {
2802
+ // We currently do not dispose views which has a highlighter or tools attached
2803
+ // Note: Possible improvement would be to serialize highlighters/tools and
2804
+ // restore them on view re-mount.
2805
+ return false;
1921
2806
  }
1922
- view.onMount(isInitialInsert);
2807
+ const cell = cellView.model;
2808
+ // Remove the view from the paper and dispose it
2809
+ cellView.remove();
2810
+ delete this._views[cell.id];
2811
+ this._registerCellViewPlaceholder(cell, cellView.cid);
2812
+ return true;
1923
2813
  },
1924
2814
 
1925
- detachView(view) {
1926
- view.unmount();
1927
- view.onDetach();
2815
+ // Dispose (release resources) all hidden views.
2816
+ disposeHiddenCellViews: function() {
2817
+ // Only cell views can be in the unmounted list (not in the legacy mode).
2818
+ if (this.legacyMode) return;
2819
+ const unmountedCids = this._updates.unmountedList.keys();
2820
+ for (const cid of unmountedCids) {
2821
+ const cellView = viewsRegistry[cid];
2822
+ cellView && this._disposeCellView(cellView);
2823
+ }
2824
+ },
2825
+
2826
+ // Detach a view from the paper, but keep it in memory.
2827
+ _detachCellView(cellView) {
2828
+ cellView.unmount();
2829
+ cellView.onDetach();
1928
2830
  },
1929
2831
 
1930
2832
  // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also
@@ -1932,7 +2834,7 @@ export const Paper = View.extend({
1932
2834
  findView: function($el) {
1933
2835
 
1934
2836
  var el = isString($el)
1935
- ? this.cells.querySelector($el)
2837
+ ? this.layers.querySelector($el)
1936
2838
  : $el instanceof $ ? $el[0] : $el;
1937
2839
 
1938
2840
  var id = this.findAttribute('model-id', el);
@@ -1942,11 +2844,32 @@ export const Paper = View.extend({
1942
2844
  },
1943
2845
 
1944
2846
  // Find a view for a model `cell`. `cell` can also be a string or number representing a model `id`.
1945
- findViewByModel: function(cell) {
1946
-
1947
- var id = (isString(cell) || isNumber(cell)) ? cell : (cell && cell.id);
1948
-
1949
- return this._views[id];
2847
+ findViewByModel: function(cellOrId) {
2848
+
2849
+ const cellViewLike = this._getCellViewLike(cellOrId);
2850
+ if (!cellViewLike) return undefined;
2851
+ if (cellViewLike[CELL_VIEW_MARKER]) {
2852
+ // If the view is not a placeholder, return it directly
2853
+ return cellViewLike;
2854
+ }
2855
+ // We do not expose placeholder views directly. We resolve them before returning.
2856
+ const cellView = this._resolveCellViewPlaceholder(cellViewLike);
2857
+ const flag = this.getCellViewInitFlag(cellView);
2858
+ if (this.isViewMounted(cellView)) {
2859
+ // The view was acting as a placeholder and is already present in the `mounted` list,
2860
+ // indicating that its visibility has been checked, but the update hasn't occurred yet.
2861
+ // Placeholders are resolved during the update routine. Since we're handling it
2862
+ // manually here, we must ensure the view is properly initialized on the next update.
2863
+ this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, {
2864
+ // It's important to run in isolation to avoid triggering the update of
2865
+ // connected links
2866
+ isolate: true
2867
+ });
2868
+ } else {
2869
+ // Update the flags in the `unmounted` list
2870
+ this._mergeUnmountedViewScheduledUpdates(cellView.cid, flag);
2871
+ }
2872
+ return cellView;
1950
2873
  },
1951
2874
 
1952
2875
  // Find all views at given point
@@ -1957,7 +2880,7 @@ export const Paper = View.extend({
1957
2880
  var views = this.model.getElements().map(this.findViewByModel, this);
1958
2881
 
1959
2882
  return views.filter(function(view) {
1960
- return view && view.vel.getBBox({ target: this.cells }).containsPoint(p);
2883
+ return view && view.vel.getBBox({ target: this.layers }).containsPoint(p);
1961
2884
  }, this);
1962
2885
  },
1963
2886
 
@@ -1971,7 +2894,7 @@ export const Paper = View.extend({
1971
2894
  var method = opt.strict ? 'containsRect' : 'intersect';
1972
2895
 
1973
2896
  return views.filter(function(view) {
1974
- return view && rect[method](view.vel.getBBox({ target: this.cells }));
2897
+ return view && rect[method](view.vel.getBBox({ target: this.layers }));
1975
2898
  }, this);
1976
2899
  },
1977
2900
 
@@ -2025,6 +2948,92 @@ export const Paper = View.extend({
2025
2948
  );
2026
2949
  },
2027
2950
 
2951
+ findClosestMagnetToPoint: function(point, options = {}) {
2952
+ let minDistance = Number.MAX_SAFE_INTEGER;
2953
+ let bestPriority = -Infinity;
2954
+ const pointer = new Point(point);
2955
+
2956
+ const radius = options.radius || Number.MAX_SAFE_INTEGER;
2957
+ const viewsInArea = this.findCellViewsInArea(
2958
+ { x: pointer.x - radius, y: pointer.y - radius, width: 2 * radius, height: 2 * radius },
2959
+ options.findInAreaOptions
2960
+ );
2961
+ // Enable all connections by default
2962
+ const filterFn = typeof options.filter === 'function' ? options.filter : null;
2963
+
2964
+ let closestView = null;
2965
+ let closestMagnet = null;
2966
+
2967
+ // Note: If snapRadius is smaller than magnet size, views will not be found.
2968
+ viewsInArea.forEach((view) => {
2969
+
2970
+ const candidates = [];
2971
+ const { model } = view;
2972
+ // skip connecting to the element in case '.': { magnet: false } attribute present
2973
+ if (view.el.getAttribute('magnet') !== 'false') {
2974
+
2975
+ if (model.isLink()) {
2976
+ const connection = view.getConnection();
2977
+ candidates.push({
2978
+ // find distance from the closest point of a link to pointer coordinates
2979
+ priority: 0,
2980
+ distance: connection.closestPoint(pointer).squaredDistance(pointer),
2981
+ magnet: view.el
2982
+ });
2983
+ } else {
2984
+ candidates.push({
2985
+ // Set the priority to the level of nested elements of the model
2986
+ // To ensure that the embedded cells get priority over the parent cells
2987
+ priority: model.getAncestors().length,
2988
+ // find distance from the center of the model to pointer coordinates
2989
+ distance: model.getBBox().center().squaredDistance(pointer),
2990
+ magnet: view.el
2991
+ });
2992
+ }
2993
+ }
2994
+
2995
+ view.$('[magnet]').toArray().forEach(magnet => {
2996
+
2997
+ const magnetBBox = view.getNodeBBox(magnet);
2998
+ let magnetDistance = magnetBBox.pointNearestToPoint(pointer).squaredDistance(pointer);
2999
+ if (magnetBBox.containsPoint(pointer)) {
3000
+ // Pointer sits inside this magnet.
3001
+ // Push its distance far into the negative range so any
3002
+ // "under-pointer" magnet outranks magnets that are only nearby
3003
+ // (positive distance) and every non-magnet candidate.
3004
+ // We add the original distance back to keep ordering among
3005
+ // overlapping magnets: the one whose border is closest to the
3006
+ // pointer (smaller original distance) still wins.
3007
+ magnetDistance = -Number.MAX_SAFE_INTEGER + magnetDistance;
3008
+ }
3009
+
3010
+ // Check if magnet is inside the snap radius.
3011
+ if (magnetDistance <= radius * radius) {
3012
+ candidates.push({
3013
+ // Give magnets priority over other candidates.
3014
+ priority: Number.MAX_SAFE_INTEGER,
3015
+ distance: magnetDistance,
3016
+ magnet
3017
+ });
3018
+ }
3019
+ });
3020
+
3021
+ candidates.forEach(candidate => {
3022
+ const { magnet, distance, priority } = candidate;
3023
+ const isBetterCandidate = (priority > bestPriority) || (priority === bestPriority && distance < minDistance);
3024
+ if (isBetterCandidate && (!filterFn || filterFn(view, magnet))) {
3025
+ bestPriority = priority;
3026
+ minDistance = distance;
3027
+ closestView = view;
3028
+ closestMagnet = magnet;
3029
+ }
3030
+ });
3031
+
3032
+ });
3033
+
3034
+ return closestView ? { view: closestView, magnet: closestMagnet } : null;
3035
+ },
3036
+
2028
3037
  _findInExtendedArea: function(area, findCellsFn, opt = {}) {
2029
3038
  const {
2030
3039
  buffer = this.DEFAULT_FIND_BUFFER,
@@ -2477,8 +3486,11 @@ export const Paper = View.extend({
2477
3486
 
2478
3487
  var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2479
3488
 
2480
- var view = data.sourceView;
3489
+ let view = data.sourceView;
2481
3490
  if (view) {
3491
+ // The view could have been disposed during dragging
3492
+ // e.g. dragged outside of the viewport and hidden
3493
+ view = this.findViewByModel(view.model);
2482
3494
  view.pointermove(evt, localPoint.x, localPoint.y);
2483
3495
  } else {
2484
3496
  this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y);
@@ -2495,8 +3507,11 @@ export const Paper = View.extend({
2495
3507
 
2496
3508
  var localPoint = this.snapToGrid(normalizedEvt.clientX, normalizedEvt.clientY);
2497
3509
 
2498
- var view = this.eventData(evt).sourceView;
3510
+ let view = this.eventData(evt).sourceView;
2499
3511
  if (view) {
3512
+ // The view could have been disposed during dragging
3513
+ // e.g. dragged outside of the viewport and hidden
3514
+ view = this.findViewByModel(view.model);
2500
3515
  view.pointerup(normalizedEvt, localPoint.x, localPoint.y);
2501
3516
  } else {
2502
3517
  this.trigger('blank:pointerup', normalizedEvt, localPoint.x, localPoint.y);
@@ -2797,7 +3812,7 @@ export const Paper = View.extend({
2797
3812
  return true;
2798
3813
  }
2799
3814
 
2800
- if (view && view.model && (view.model instanceof Cell)) {
3815
+ if (view && view.model && (view.model[CELL_MARKER])) {
2801
3816
  return false;
2802
3817
  }
2803
3818
 
@@ -2813,13 +3828,13 @@ export const Paper = View.extend({
2813
3828
  options.gridSize = gridSize;
2814
3829
  if (options.drawGrid && !options.drawGridSize) {
2815
3830
  // Do not redraw the grid if the `drawGridSize` is set.
2816
- this.getLayerView(LayersNames.GRID).renderGrid();
3831
+ this.getLayerView(paperLayers.GRID).renderGrid();
2817
3832
  }
2818
3833
  return this;
2819
3834
  },
2820
3835
 
2821
3836
  setGrid: function(drawGrid) {
2822
- this.getLayerView(LayersNames.GRID).setGrid(drawGrid);
3837
+ this.getLayerView(paperLayers.GRID).setGrid(drawGrid);
2823
3838
  return this;
2824
3839
  },
2825
3840
 
@@ -3168,202 +4183,8 @@ export const Paper = View.extend({
3168
4183
 
3169
4184
  sorting: sortingTypes,
3170
4185
 
3171
- Layers: LayersNames,
4186
+ Layers: paperLayers,
3172
4187
 
3173
- backgroundPatterns: {
3174
-
3175
- flipXy: function(img) {
3176
- // d b
3177
- // q p
3178
-
3179
- var canvas = document.createElement('canvas');
3180
- var imgWidth = img.width;
3181
- var imgHeight = img.height;
3182
-
3183
- canvas.width = 2 * imgWidth;
3184
- canvas.height = 2 * imgHeight;
3185
-
3186
- var ctx = canvas.getContext('2d');
3187
- // top-left image
3188
- ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3189
- // xy-flipped bottom-right image
3190
- ctx.setTransform(-1, 0, 0, -1, canvas.width, canvas.height);
3191
- ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3192
- // x-flipped top-right image
3193
- ctx.setTransform(-1, 0, 0, 1, canvas.width, 0);
3194
- ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3195
- // y-flipped bottom-left image
3196
- ctx.setTransform(1, 0, 0, -1, 0, canvas.height);
3197
- ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3198
-
3199
- return canvas;
3200
- },
3201
-
3202
- flipX: function(img) {
3203
- // d b
3204
- // d b
3205
-
3206
- var canvas = document.createElement('canvas');
3207
- var imgWidth = img.width;
3208
- var imgHeight = img.height;
3209
-
3210
- canvas.width = imgWidth * 2;
3211
- canvas.height = imgHeight;
3212
-
3213
- var ctx = canvas.getContext('2d');
3214
- // left image
3215
- ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3216
- // flipped right image
3217
- ctx.translate(2 * imgWidth, 0);
3218
- ctx.scale(-1, 1);
3219
- ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3220
-
3221
- return canvas;
3222
- },
3223
-
3224
- flipY: function(img) {
3225
- // d d
3226
- // q q
3227
-
3228
- var canvas = document.createElement('canvas');
3229
- var imgWidth = img.width;
3230
- var imgHeight = img.height;
3231
-
3232
- canvas.width = imgWidth;
3233
- canvas.height = imgHeight * 2;
3234
-
3235
- var ctx = canvas.getContext('2d');
3236
- // top image
3237
- ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3238
- // flipped bottom image
3239
- ctx.translate(0, 2 * imgHeight);
3240
- ctx.scale(1, -1);
3241
- ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
3242
-
3243
- return canvas;
3244
- },
3245
-
3246
- watermark: function(img, opt) {
3247
- // d
3248
- // d
3249
-
3250
- opt = opt || {};
3251
-
3252
- var imgWidth = img.width;
3253
- var imgHeight = img.height;
3254
-
3255
- var canvas = document.createElement('canvas');
3256
- canvas.width = imgWidth * 3;
3257
- canvas.height = imgHeight * 3;
3258
-
3259
- var ctx = canvas.getContext('2d');
3260
- var angle = isNumber(opt.watermarkAngle) ? -opt.watermarkAngle : -20;
3261
- var radians = toRad(angle);
3262
- var stepX = canvas.width / 4;
3263
- var stepY = canvas.height / 4;
3264
-
3265
- for (var i = 0; i < 4; i++) {
3266
- for (var j = 0; j < 4; j++) {
3267
- if ((i + j) % 2 > 0) {
3268
- // reset the current transformations
3269
- ctx.setTransform(1, 0, 0, 1, (2 * i - 1) * stepX, (2 * j - 1) * stepY);
3270
- ctx.rotate(radians);
3271
- ctx.drawImage(img, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
3272
- }
3273
- }
3274
- }
3275
-
3276
- return canvas;
3277
- }
3278
- },
3279
-
3280
- gridPatterns: {
3281
- dot: [{
3282
- color: '#AAAAAA',
3283
- thickness: 1,
3284
- markup: 'rect',
3285
- render: function(el, opt) {
3286
- V(el).attr({
3287
- width: opt.thickness,
3288
- height: opt.thickness,
3289
- fill: opt.color
3290
- });
3291
- }
3292
- }],
3293
- fixedDot: [{
3294
- color: '#AAAAAA',
3295
- thickness: 1,
3296
- markup: 'rect',
3297
- render: function(el, opt) {
3298
- V(el).attr({ fill: opt.color });
3299
- },
3300
- update: function(el, opt, paper) {
3301
- const { sx, sy } = paper.scale();
3302
- const width = sx <= 1 ? opt.thickness : opt.thickness / sx;
3303
- const height = sy <= 1 ? opt.thickness : opt.thickness / sy;
3304
- V(el).attr({ width, height });
3305
- }
3306
- }],
3307
- mesh: [{
3308
- color: '#AAAAAA',
3309
- thickness: 1,
3310
- markup: 'path',
3311
- render: function(el, opt) {
3312
-
3313
- var d;
3314
- var width = opt.width;
3315
- var height = opt.height;
3316
- var thickness = opt.thickness;
3317
-
3318
- if (width - thickness >= 0 && height - thickness >= 0) {
3319
- d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
3320
- } else {
3321
- d = 'M 0 0 0 0';
3322
- }
3323
-
3324
- V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
3325
- }
3326
- }],
3327
- doubleMesh: [{
3328
- color: '#AAAAAA',
3329
- thickness: 1,
3330
- markup: 'path',
3331
- render: function(el, opt) {
3332
-
3333
- var d;
3334
- var width = opt.width;
3335
- var height = opt.height;
3336
- var thickness = opt.thickness;
3337
-
3338
- if (width - thickness >= 0 && height - thickness >= 0) {
3339
- d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
3340
- } else {
3341
- d = 'M 0 0 0 0';
3342
- }
3343
-
3344
- V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
3345
- }
3346
- }, {
3347
- color: '#000000',
3348
- thickness: 3,
3349
- scaleFactor: 4,
3350
- markup: 'path',
3351
- render: function(el, opt) {
3352
-
3353
- var d;
3354
- var width = opt.width;
3355
- var height = opt.height;
3356
- var thickness = opt.thickness;
3357
-
3358
- if (width - thickness >= 0 && height - thickness >= 0) {
3359
- d = ['M', width, 0, 'H0 M0 0 V0', height].join(' ');
3360
- } else {
3361
- d = 'M 0 0 0 0';
3362
- }
3363
-
3364
- V(el).attr({ 'd': d, stroke: opt.color, 'stroke-width': opt.thickness });
3365
- }
3366
- }]
3367
- }
4188
+ backgroundPatterns,
4189
+ gridPatterns,
3368
4190
  });
3369
-