@joint/core 4.1.3 → 4.2.0-alpha.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 (58) hide show
  1. package/README.md +4 -2
  2. package/dist/geometry.js +129 -124
  3. package/dist/geometry.min.js +4 -3
  4. package/dist/joint.d.ts +352 -160
  5. package/dist/joint.js +3654 -2191
  6. package/dist/joint.min.js +4 -3
  7. package/dist/joint.nowrap.js +3653 -2188
  8. package/dist/joint.nowrap.min.js +4 -3
  9. package/dist/vectorizer.js +489 -279
  10. package/dist/vectorizer.min.js +4 -3
  11. package/dist/version.mjs +1 -1
  12. package/package.json +33 -27
  13. package/src/V/create.mjs +51 -0
  14. package/src/V/index.mjs +89 -159
  15. package/src/V/namespace.mjs +9 -0
  16. package/src/V/transform.mjs +183 -0
  17. package/src/V/traverse.mjs +16 -0
  18. package/src/alg/Deque.mjs +126 -0
  19. package/src/anchors/index.mjs +140 -33
  20. package/src/cellTools/Boundary.mjs +15 -13
  21. package/src/cellTools/Button.mjs +7 -5
  22. package/src/cellTools/Control.mjs +38 -15
  23. package/src/cellTools/HoverConnect.mjs +5 -1
  24. package/src/cellTools/helpers.mjs +44 -3
  25. package/src/config/index.mjs +8 -0
  26. package/src/connectionPoints/index.mjs +24 -9
  27. package/src/connectionStrategies/index.mjs +1 -1
  28. package/src/connectors/jumpover.mjs +1 -1
  29. package/src/dia/Cell.mjs +32 -12
  30. package/src/dia/CellView.mjs +53 -38
  31. package/src/dia/Element.mjs +81 -35
  32. package/src/dia/ElementView.mjs +2 -1
  33. package/src/dia/HighlighterView.mjs +54 -11
  34. package/src/dia/LinkView.mjs +118 -98
  35. package/src/dia/Paper.mjs +831 -231
  36. package/src/dia/PaperLayer.mjs +9 -2
  37. package/src/dia/ToolView.mjs +4 -0
  38. package/src/dia/ToolsView.mjs +12 -3
  39. package/src/dia/attributes/text.mjs +16 -5
  40. package/src/dia/layers/GridLayer.mjs +5 -0
  41. package/src/dia/ports.mjs +344 -111
  42. package/src/elementTools/HoverConnect.mjs +14 -8
  43. package/src/env/index.mjs +7 -4
  44. package/src/g/rect.mjs +7 -0
  45. package/src/highlighters/stroke.mjs +1 -1
  46. package/src/layout/ports/port.mjs +30 -15
  47. package/src/layout/ports/portLabel.mjs +1 -1
  48. package/src/linkAnchors/index.mjs +2 -2
  49. package/src/linkTools/Anchor.mjs +2 -2
  50. package/src/linkTools/Vertices.mjs +4 -6
  51. package/src/mvc/View.mjs +4 -0
  52. package/src/mvc/ViewBase.mjs +1 -1
  53. package/src/util/util.mjs +1 -1
  54. package/src/util/utilHelpers.mjs +2 -0
  55. package/types/geometry.d.ts +65 -59
  56. package/types/joint.d.ts +278 -102
  57. package/types/vectorizer.d.ts +11 -1
  58. package/src/V/annotation.mjs +0 -0
package/src/dia/Paper.mjs CHANGED
@@ -28,17 +28,20 @@ import {
28
28
  filter as _filter,
29
29
  parseDOMJSON,
30
30
  toArray,
31
- has
31
+ has,
32
+ uniqueId,
32
33
  } from '../util/index.mjs';
33
34
  import { ViewBase } from '../mvc/ViewBase.mjs';
34
35
  import { Rect, Point, toRad } from '../g/index.mjs';
35
- import { View, views } from '../mvc/index.mjs';
36
- import { CellView } from './CellView.mjs';
36
+ import { View, views as viewsRegistry } from '../mvc/index.mjs';
37
+ import { CellView, CELL_VIEW_MARKER } from './CellView.mjs';
37
38
  import { ElementView } from './ElementView.mjs';
38
39
  import { LinkView } from './LinkView.mjs';
39
40
  import { Cell } from './Cell.mjs';
40
41
  import { Graph } from './Graph.mjs';
41
42
  import { LayersNames, PaperLayer } from './PaperLayer.mjs';
43
+ import { HighlighterView } from './HighlighterView.mjs';
44
+ import { Deque } from '../alg/Deque.mjs';
42
45
  import * as highlighters from '../highlighters/index.mjs';
43
46
  import * as linkAnchors from '../linkAnchors/index.mjs';
44
47
  import * as connectionPoints from '../connectionPoints/index.mjs';
@@ -96,6 +99,8 @@ const defaultLayers = [{
96
99
  name: LayersNames.TOOLS
97
100
  }];
98
101
 
102
+ const CELL_VIEW_PLACEHOLDER_MARKER = Symbol('joint.cellViewPlaceholderMarker');
103
+
99
104
  export const Paper = View.extend({
100
105
 
101
106
  className: 'paper',
@@ -267,6 +272,8 @@ export const Paper = View.extend({
267
272
 
268
273
  autoFreeze: false,
269
274
 
275
+ viewManagement: false,
276
+
270
277
  // no docs yet
271
278
  onViewUpdate: function(view, flag, priority, opt, paper) {
272
279
  // Do not update connected links when:
@@ -274,7 +281,7 @@ export const Paper = View.extend({
274
281
  // 2. the view was just mounted (added back to the paper by viewport function)
275
282
  // 3. the change was marked as `isolate`.
276
283
  // 4. the view model was just removed from the graph
277
- if ((flag & (view.FLAG_INSERT | view.FLAG_REMOVE)) || opt.mounting || opt.isolate) return;
284
+ if ((flag & (paper.FLAG_INSERT | paper.FLAG_REMOVE)) || opt.mounting || opt.isolate) return;
278
285
  paper.requestConnectedLinksUpdate(view, priority, opt);
279
286
  },
280
287
 
@@ -387,6 +394,14 @@ export const Paper = View.extend({
387
394
  // to mitigate the differences between the model and view geometry.
388
395
  DEFAULT_FIND_BUFFER: 200,
389
396
 
397
+ // Default layer to insert the cell views into.
398
+ DEFAULT_CELL_LAYER: LayersNames.CELLS,
399
+
400
+ // Update flags
401
+ FLAG_INSERT: 1<<30,
402
+ FLAG_REMOVE: 1<<29,
403
+ FLAG_INIT: 1<<28,
404
+
390
405
  init: function() {
391
406
 
392
407
  const { options } = this;
@@ -398,8 +413,16 @@ export const Paper = View.extend({
398
413
 
399
414
  const model = this.model = options.model || new Graph;
400
415
 
416
+ // This property tells us if we need to keep the compatibility
417
+ // with the v4 API and behavior.
418
+ this.legacyMode = !options.viewManagement;
419
+
401
420
  // Layers (SVGGroups)
402
- this._layers = {};
421
+ this._layers = {
422
+ viewsMap: {},
423
+ namesMap: {},
424
+ order: [],
425
+ };
403
426
 
404
427
  this.cloneOptions();
405
428
  this.render();
@@ -408,6 +431,8 @@ export const Paper = View.extend({
408
431
 
409
432
  // Hash of all cell views.
410
433
  this._views = {};
434
+ this._viewPlaceholders = {};
435
+ this._idToCid = {};
411
436
 
412
437
  // Mouse wheel events buffer
413
438
  this._mw_evt_buffer = {
@@ -417,8 +442,6 @@ export const Paper = View.extend({
417
442
 
418
443
  // Render existing cells in the graph
419
444
  this.resetViews(model.attributes.cells.models);
420
- // Start the Rendering Loop
421
- if (!this.isFrozen() && this.isAsync()) this.updateViewsAsync();
422
445
  },
423
446
 
424
447
  _resetUpdates: function() {
@@ -427,16 +450,15 @@ export const Paper = View.extend({
427
450
  return this._updates = {
428
451
  id: null,
429
452
  priorities: [{}, {}, {}],
430
- unmountedCids: [],
431
- mountedCids: [],
432
- unmounted: {},
433
- mounted: {},
453
+ unmountedList: new Deque(),
454
+ mountedList: new Deque(),
434
455
  count: 0,
435
456
  keyFrozen: false,
436
457
  freezeKey: null,
437
458
  sort: false,
438
459
  disabled: false,
439
- idle: false
460
+ idle: false,
461
+ freshAfterReset: true,
440
462
  };
441
463
  },
442
464
 
@@ -465,15 +487,25 @@ export const Paper = View.extend({
465
487
  },
466
488
 
467
489
  onCellRemoved: function(cell, _, opt) {
468
- const view = this.findViewByModel(cell);
469
- if (view) this.requestViewUpdate(view, view.FLAG_REMOVE, view.UPDATE_PRIORITY, opt);
490
+ const viewLike = this._getCellViewLike(cell);
491
+ if (!viewLike) return;
492
+ if (viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) {
493
+ this._unregisterCellViewPlaceholder(viewLike);
494
+ } else {
495
+ this.requestViewUpdate(viewLike, this.FLAG_REMOVE, viewLike.UPDATE_PRIORITY, opt);
496
+ }
470
497
  },
471
498
 
472
499
  onCellChange: function(cell, opt) {
473
500
  if (cell === this.model.attributes.cells) return;
474
- if (cell.hasChanged('z') && this.options.sorting === sortingTypes.APPROX) {
475
- const view = this.findViewByModel(cell);
476
- if (view) this.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
501
+ if (
502
+ cell.hasChanged('layer') ||
503
+ (cell.hasChanged('z') && this.options.sorting === sortingTypes.APPROX)
504
+ ) {
505
+ const viewLike = this._getCellViewLike(cell);
506
+ if (viewLike) {
507
+ this.requestViewUpdate(viewLike, this.FLAG_INSERT, viewLike.UPDATE_PRIORITY, opt);
508
+ }
477
509
  }
478
510
  },
479
511
 
@@ -488,7 +520,7 @@ export const Paper = View.extend({
488
520
  },
489
521
 
490
522
  onGraphBatchStop: function(data) {
491
- if (this.isFrozen()) return;
523
+ if (this.isFrozen() || this.isIdle()) return;
492
524
  var name = data && data.batchName;
493
525
  var graph = this.model;
494
526
  if (!this.isAsync()) {
@@ -548,6 +580,15 @@ export const Paper = View.extend({
548
580
  // Return the default highlighting options into the user specified options.
549
581
  options.highlighting = defaultsDeep({}, highlighting, defaultHighlighting);
550
582
  }
583
+ // Copy and set defaults for the view management options.
584
+ options.viewManagement = defaults({}, options.viewManagement, {
585
+ // Whether to lazy initialize the cell views.
586
+ lazyInitialize: !!options.viewManagement, // default `true` if options.viewManagement provided
587
+ // Whether to add initialized cell views into the unmounted queue.
588
+ initializeUnmounted: false,
589
+ // Whether to dispose the cell views that are not visible.
590
+ disposeHidden: false,
591
+ });
551
592
  },
552
593
 
553
594
  children: function() {
@@ -588,19 +629,122 @@ export const Paper = View.extend({
588
629
  },
589
630
 
590
631
  hasLayerView(layerName) {
591
- return (layerName in this._layers);
632
+ return (layerName in this._layers.viewsMap);
592
633
  },
593
634
 
594
635
  getLayerView(layerName) {
595
- const { _layers } = this;
596
- if (layerName in _layers) return _layers[layerName];
597
- throw new Error(`dia.Paper: Unknown layer "${layerName}"`);
636
+ const { _layers: { viewsMap }} = this;
637
+ if (layerName in viewsMap) return viewsMap[layerName];
638
+ throw new Error(`dia.Paper: Unknown layer "${layerName}".`);
598
639
  },
599
640
 
600
641
  getLayerNode(layerName) {
601
642
  return this.getLayerView(layerName).el;
602
643
  },
603
644
 
645
+ _removeLayer(layerView) {
646
+ this._unregisterLayer(layerView);
647
+ layerView.remove();
648
+ },
649
+
650
+ _unregisterLayer(layerView) {
651
+ const { _layers: { viewsMap, namesMap, order }} = this;
652
+ const layerName = this._getLayerName(layerView);
653
+ order.splice(order.indexOf(layerName), 1);
654
+ delete namesMap[layerView.cid];
655
+ delete viewsMap[layerName];
656
+ },
657
+
658
+ _registerLayer(layerName, layerView, beforeLayerView) {
659
+ const { _layers: { viewsMap, namesMap, order }} = this;
660
+ if (beforeLayerView) {
661
+ const beforeLayerName = this._getLayerName(beforeLayerView);
662
+ order.splice(order.indexOf(beforeLayerName), 0, layerName);
663
+ } else {
664
+ order.push(layerName);
665
+ }
666
+ viewsMap[layerName] = layerView;
667
+ namesMap[layerView.cid] = layerName;
668
+ },
669
+
670
+ _getLayerView(layer) {
671
+ const { _layers: { namesMap, viewsMap }} = this;
672
+ if (layer instanceof PaperLayer) {
673
+ if (layer.cid in namesMap) return layer;
674
+ return null;
675
+ }
676
+ if (layer in viewsMap) return viewsMap[layer];
677
+ return null;
678
+ },
679
+
680
+ _getLayerName(layerView) {
681
+ const { _layers: { namesMap }} = this;
682
+ return namesMap[layerView.cid];
683
+ },
684
+
685
+ _requireLayerView(layer) {
686
+ const layerView = this._getLayerView(layer);
687
+ if (!layerView) {
688
+ if (layer instanceof PaperLayer) {
689
+ throw new Error('dia.Paper: The layer is not registered.');
690
+ } else {
691
+ throw new Error(`dia.Paper: Unknown layer "${layer}".`);
692
+ }
693
+ }
694
+ return layerView;
695
+ },
696
+
697
+ hasLayer(layer) {
698
+ return this._getLayerView(layer) !== null;
699
+ },
700
+
701
+ removeLayer(layer) {
702
+ const layerView = this._requireLayerView(layer);
703
+ if (!layerView.isEmpty()) {
704
+ throw new Error('dia.Paper: The layer is not empty.');
705
+ }
706
+ this._removeLayer(layerView);
707
+ },
708
+
709
+ addLayer(layerName, layerView, options = {}) {
710
+ if (!layerName || typeof layerName !== 'string') {
711
+ throw new Error('dia.Paper: The layer name must be provided.');
712
+ }
713
+ if (this._getLayerView(layerName)) {
714
+ throw new Error(`dia.Paper: The layer "${layerName}" already exists.`);
715
+ }
716
+ if (!(layerView instanceof PaperLayer)) {
717
+ throw new Error('dia.Paper: The layer view is not an instance of dia.PaperLayer.');
718
+ }
719
+ const { insertBefore } = options;
720
+ if (!insertBefore) {
721
+ this._registerLayer(layerName, layerView, null);
722
+ this.layers.appendChild(layerView.el);
723
+ } else {
724
+ const beforeLayerView = this._requireLayerView(insertBefore);
725
+ this._registerLayer(layerName, layerView, beforeLayerView);
726
+ this.layers.insertBefore(layerView.el, beforeLayerView.el);
727
+ }
728
+ },
729
+
730
+ moveLayer(layer, insertBefore) {
731
+ const layerView = this._requireLayerView(layer);
732
+ if (layerView === this._getLayerView(insertBefore)) return;
733
+ const layerName = this._getLayerName(layerView);
734
+ this._unregisterLayer(layerView);
735
+ this.addLayer(layerName, layerView, { insertBefore });
736
+ },
737
+
738
+ getLayerNames() {
739
+ // Returns a sorted array of layer names.
740
+ return this._layers.order.slice();
741
+ },
742
+
743
+ getLayers() {
744
+ // Returns a sorted array of layer views.
745
+ return this.getLayerNames().map(name => this.getLayerView(name));
746
+ },
747
+
604
748
  render: function() {
605
749
 
606
750
  this.renderChildren();
@@ -645,14 +789,15 @@ export const Paper = View.extend({
645
789
  }
646
790
  },
647
791
 
792
+ renderLayer: function(name) {
793
+ const layerView = this.createLayer(name);
794
+ this.addLayer(name, layerView);
795
+ return layerView;
796
+ },
797
+
648
798
  renderLayers: function(layers = defaultLayers) {
649
799
  this.removeLayers();
650
- // TODO: Layers to be read from the graph `layers` attribute
651
- layers.forEach(({ name, sorted }) => {
652
- const layerView = this.createLayer(name);
653
- this.layers.appendChild(layerView.el);
654
- this._layers[name] = layerView;
655
- });
800
+ layers.forEach(({ name }) => this.renderLayer(name));
656
801
  // Throws an exception if doesn't exist
657
802
  const cellsLayerView = this.getLayerView(LayersNames.CELLS);
658
803
  const toolsLayerView = this.getLayerView(LayersNames.TOOLS);
@@ -670,18 +815,13 @@ export const Paper = View.extend({
670
815
  },
671
816
 
672
817
  removeLayers: function() {
673
- const { _layers } = this;
674
- Object.keys(_layers).forEach(name => {
675
- _layers[name].remove();
676
- delete _layers[name];
677
- });
818
+ const { _layers: { viewsMap }} = this;
819
+ Object.values(viewsMap).forEach(layerView => this._removeLayer(layerView));
678
820
  },
679
821
 
680
822
  resetLayers: function() {
681
- const { _layers } = this;
682
- Object.keys(_layers).forEach(name => {
683
- _layers[name].removePivots();
684
- });
823
+ const { _layers: { viewsMap }} = this;
824
+ Object.values(viewsMap).forEach(layerView => layerView.removePivots());
685
825
  },
686
826
 
687
827
  update: function() {
@@ -816,47 +956,46 @@ export const Paper = View.extend({
816
956
  var links = this.model.getConnectedLinks(model);
817
957
  for (var j = 0, n = links.length; j < n; j++) {
818
958
  var link = links[j];
819
- var linkView = this.findViewByModel(link);
959
+ var linkView = this._getCellViewLike(link);
820
960
  if (!linkView) continue;
821
- var flagLabels = ['UPDATE'];
822
- if (link.getTargetCell() === model) flagLabels.push('TARGET');
823
- if (link.getSourceCell() === model) flagLabels.push('SOURCE');
961
+ // We do not have to update placeholder views.
962
+ // They will be updated on initial render.
963
+ if (linkView[CELL_VIEW_PLACEHOLDER_MARKER]) continue;
824
964
  var nextPriority = Math.max(priority + 1, linkView.UPDATE_PRIORITY);
825
- this.scheduleViewUpdate(linkView, linkView.getFlag(flagLabels), nextPriority, opt);
965
+ this.scheduleViewUpdate(linkView, linkView.getFlag(LinkView.Flags.UPDATE), nextPriority, opt);
826
966
  }
827
967
  }
828
968
  },
829
969
 
830
970
  forcePostponedViewUpdate: function(view, flag) {
831
971
  if (!view || !(view instanceof CellView)) return false;
832
- var model = view.model;
972
+ const model = view.model;
833
973
  if (model.isElement()) return false;
834
- if ((flag & view.getFlag(['SOURCE', 'TARGET'])) === 0) {
835
- var dumpOptions = { silent: true };
836
- // LinkView is waiting for the target or the source cellView to be rendered
837
- // This can happen when the cells are not in the viewport.
838
- var sourceFlag = 0;
839
- var sourceView = this.findViewByModel(model.getSourceCell());
840
- if (sourceView && !this.isViewMounted(sourceView)) {
841
- sourceFlag = this.dumpView(sourceView, dumpOptions);
842
- view.updateEndMagnet('source');
843
- }
844
- var targetFlag = 0;
845
- var targetView = this.findViewByModel(model.getTargetCell());
846
- if (targetView && !this.isViewMounted(targetView)) {
847
- targetFlag = this.dumpView(targetView, dumpOptions);
848
- view.updateEndMagnet('target');
849
- }
850
- if (sourceFlag === 0 && targetFlag === 0) {
851
- // If leftover flag is 0, all view updates were done.
852
- return !this.dumpView(view, dumpOptions);
853
- }
974
+ const dumpOptions = { silent: true };
975
+ // LinkView is waiting for the target or the source cellView to be rendered
976
+ // This can happen when the cells are not in the viewport.
977
+ let sourceFlag = 0;
978
+ const sourceCell = model.getSourceCell();
979
+ if (sourceCell && !this.isCellVisible(sourceCell)) {
980
+ const sourceView = this.findViewByModel(sourceCell);
981
+ sourceFlag = this.dumpView(sourceView, dumpOptions);
982
+ }
983
+ let targetFlag = 0;
984
+ const targetCell = model.getTargetCell();
985
+ if (targetCell && !this.isCellVisible(targetCell)) {
986
+ const targetView = this.findViewByModel(targetCell);
987
+ targetFlag = this.dumpView(targetView, dumpOptions);
988
+ }
989
+ if (sourceFlag === 0 && targetFlag === 0) {
990
+ // If leftover flag is 0, all view updates were done.
991
+ return !this.dumpView(view, dumpOptions);
854
992
  }
855
993
  return false;
856
994
  },
857
995
 
858
996
  requestViewUpdate: function(view, flag, priority, opt) {
859
997
  opt || (opt = {});
998
+ // Note: `scheduleViewUpdate` wakes up the paper if it is idle.
860
999
  this.scheduleViewUpdate(view, flag, priority, opt);
861
1000
  var isAsync = this.isAsync();
862
1001
  if (this.isFrozen() || (isAsync && opt.async !== false)) return;
@@ -867,13 +1006,14 @@ export const Paper = View.extend({
867
1006
 
868
1007
  scheduleViewUpdate: function(view, type, priority, opt) {
869
1008
  const { _updates: updates, options } = this;
870
- if (updates.idle) {
871
- if (options.autoFreeze) {
872
- updates.idle = false;
873
- this.unfreeze();
874
- }
1009
+ if (updates.idle && options.autoFreeze) {
1010
+ this.legacyMode
1011
+ ? this.unfreeze() // Restart rendering loop without original options
1012
+ : this.wakeUp();
875
1013
  }
876
- const { FLAG_REMOVE, FLAG_INSERT, UPDATE_PRIORITY, cid } = view;
1014
+ const { FLAG_REMOVE, FLAG_INSERT } = this;
1015
+ const { UPDATE_PRIORITY, cid } = view;
1016
+
877
1017
  let priorityUpdates = updates.priorities[priority];
878
1018
  if (!priorityUpdates) priorityUpdates = updates.priorities[priority] = {};
879
1019
  // Move higher priority updates to this priority
@@ -919,20 +1059,18 @@ export const Paper = View.extend({
919
1059
  dumpView: function(view, opt = {}) {
920
1060
  const flag = this.dumpViewUpdate(view);
921
1061
  if (!flag) return 0;
922
- const shouldNotify = !opt.silent;
923
- if (shouldNotify) this.notifyBeforeRender(opt);
1062
+ this.notifyBeforeRender(opt);
924
1063
  const leftover = this.updateView(view, flag, opt);
925
- if (shouldNotify) {
926
- const stats = { updated: 1, priority: view.UPDATE_PRIORITY };
927
- this.notifyAfterRender(stats, opt);
928
- }
1064
+ const stats = { updated: 1, priority: view.UPDATE_PRIORITY };
1065
+ this.notifyAfterRender(stats, opt);
929
1066
  return leftover;
930
1067
  },
931
1068
 
932
1069
  updateView: function(view, flag, opt) {
933
1070
  if (!view) return 0;
934
- const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT, model } = view;
935
- if (view instanceof CellView) {
1071
+ const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT } = this;
1072
+ const { model } = view;
1073
+ if (view[CELL_VIEW_MARKER]) {
936
1074
  if (flag & FLAG_REMOVE) {
937
1075
  this.removeView(model);
938
1076
  return 0;
@@ -960,57 +1098,70 @@ export const Paper = View.extend({
960
1098
  registerUnmountedView: function(view) {
961
1099
  var cid = view.cid;
962
1100
  var updates = this._updates;
963
- if (cid in updates.unmounted) return 0;
964
- var flag = updates.unmounted[cid] |= view.FLAG_INSERT;
965
- updates.unmountedCids.push(cid);
966
- delete updates.mounted[cid];
1101
+ if (updates.unmountedList.has(cid)) return 0;
1102
+ const flag = this.FLAG_INSERT;
1103
+ updates.unmountedList.pushTail(cid, flag);
1104
+ updates.mountedList.delete(cid);
967
1105
  return flag;
968
1106
  },
969
1107
 
970
1108
  registerMountedView: function(view) {
971
1109
  var cid = view.cid;
972
1110
  var updates = this._updates;
973
- if (cid in updates.mounted) return 0;
974
- updates.mounted[cid] = true;
975
- updates.mountedCids.push(cid);
976
- var flag = updates.unmounted[cid] || 0;
977
- delete updates.unmounted[cid];
1111
+ if (updates.mountedList.has(cid)) return 0;
1112
+ const unmountedItem = updates.unmountedList.get(cid);
1113
+ const flag = unmountedItem ? unmountedItem.value : 0;
1114
+ updates.unmountedList.delete(cid);
1115
+ updates.mountedList.pushTail(cid);
978
1116
  return flag;
979
1117
  },
980
1118
 
981
- isViewMounted: function(view) {
982
- if (!view) return false;
983
- var cid = view.cid;
984
- var updates = this._updates;
985
- return (cid in updates.mounted);
1119
+ isCellVisible: function(cellOrId) {
1120
+ const cid = cellOrId && this._idToCid[cellOrId.id || cellOrId];
1121
+ if (!cid) return false; // The view is not registered.
1122
+ return this.isViewMounted(cid);
986
1123
  },
987
1124
 
1125
+ isViewMounted: function(viewOrCid) {
1126
+ if (!viewOrCid) return false;
1127
+ let cid;
1128
+ if (viewOrCid[CELL_VIEW_MARKER] || viewOrCid[CELL_VIEW_PLACEHOLDER_MARKER]) {
1129
+ cid = viewOrCid.cid;
1130
+ } else {
1131
+ cid = viewOrCid;
1132
+ }
1133
+ return this._updates.mountedList.has(cid);
1134
+ },
1135
+
1136
+ /**
1137
+ * @deprecated use `updateCellsVisibility` instead.
1138
+ * `paper.updateCellsVisibility({ cellVisibility: () => true });`
1139
+ */
988
1140
  dumpViews: function(opt) {
989
- var passingOpt = defaults({}, opt, { viewport: null });
990
- this.checkViewport(passingOpt);
991
- this.updateViews(passingOpt);
1141
+ // Update cell visibility without `cellVisibility` callback i.e. make the cells visible
1142
+ const passingOpt = defaults({}, opt, { cellVisibility: null, viewport: null });
1143
+ this.updateCellsVisibility(passingOpt);
992
1144
  },
993
1145
 
994
- // Synchronous views update
995
- updateViews: function(opt) {
1146
+ /**
1147
+ * Process all scheduled updates synchronously.
1148
+ */
1149
+ updateViews: function(opt = {}) {
996
1150
  this.notifyBeforeRender(opt);
997
- let batchStats;
998
- let updateCount = 0;
999
- let batchCount = 0;
1000
- let priority = MIN_PRIORITY;
1001
- do {
1002
- batchCount++;
1003
- batchStats = this.updateViewsBatch(opt);
1004
- updateCount += batchStats.updated;
1005
- priority = Math.min(batchStats.priority, priority);
1006
- } while (!batchStats.empty);
1007
- const stats = { updated: updateCount, batches: batchCount, priority };
1151
+ const batchStats = this.updateViewsBatch({ ...opt, batchSize: Infinity });
1152
+ const stats = {
1153
+ updated: batchStats.updated,
1154
+ priority: batchStats.priority,
1155
+ // For backward compatibility. Will be removed in the future.
1156
+ batches: Number.isFinite(opt.batchSize) ? Math.ceil(batchStats.updated / opt.batchSize) : 1
1157
+ };
1008
1158
  this.notifyAfterRender(stats, opt);
1009
1159
  return stats;
1010
1160
  },
1011
1161
 
1012
1162
  hasScheduledUpdates: function() {
1013
- const priorities = this._updates.priorities;
1163
+ const updates = this._updates;
1164
+ const priorities = updates.priorities;
1014
1165
  const priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
1015
1166
  let i = priorityIndexes.length;
1016
1167
  while (i > 0 && i--) {
@@ -1022,11 +1173,38 @@ export const Paper = View.extend({
1022
1173
 
1023
1174
  updateViewsAsync: function(opt, data) {
1024
1175
  opt || (opt = {});
1025
- data || (data = { processed: 0, priority: MIN_PRIORITY });
1176
+ data || (data = {
1177
+ processed: 0,
1178
+ priority: MIN_PRIORITY,
1179
+ checkedUnmounted: 0,
1180
+ checkedMounted: 0,
1181
+ });
1026
1182
  const { _updates: updates, options } = this;
1027
- const id = updates.id;
1028
- if (id) {
1183
+ const { id, mountedList, unmountedList, freshAfterReset } = updates;
1184
+
1185
+ // Should we run the next batch update this frame?
1186
+ let runBatchUpdate = true;
1187
+ if (!id) {
1188
+ // If there's no scheduled frame, no batch update is needed.
1189
+ runBatchUpdate = false;
1190
+ } else {
1191
+ // Cancel any scheduled frame.
1029
1192
  cancelFrame(id);
1193
+ if (freshAfterReset) {
1194
+ // First update after a reset.
1195
+ updates.freshAfterReset = false;
1196
+ // When `initializeUnmounted` is enabled, there are no scheduled updates.
1197
+ // We check whether the `mountedList` and `unmountedList` are empty.
1198
+ if (!this.legacyMode && mountedList.length === 0 && unmountedList.length === 0) {
1199
+ // No updates to process; We trigger before/after render events via `updateViews`.
1200
+ // Note: If `autoFreeze` is enabled, 'idle' event triggers next frame.
1201
+ this.updateViews();
1202
+ runBatchUpdate = false;
1203
+ }
1204
+ }
1205
+ }
1206
+
1207
+ if (runBatchUpdate) {
1030
1208
  if (data.processed === 0 && this.hasScheduledUpdates()) {
1031
1209
  this.notifyBeforeRender(opt);
1032
1210
  }
@@ -1035,7 +1213,7 @@ export const Paper = View.extend({
1035
1213
  mountBatchSize: MOUNT_BATCH_SIZE - stats.mounted,
1036
1214
  unmountBatchSize: MOUNT_BATCH_SIZE - stats.unmounted
1037
1215
  });
1038
- const checkStats = this.checkViewport(passingOpt);
1216
+ const checkStats = this.scheduleCellsVisibilityUpdate(passingOpt);
1039
1217
  const unmountCount = checkStats.unmounted;
1040
1218
  const mountCount = checkStats.mounted;
1041
1219
  let processed = data.processed;
@@ -1056,11 +1234,22 @@ export const Paper = View.extend({
1056
1234
  } else {
1057
1235
  data.processed = processed;
1058
1236
  }
1237
+ data.checkedUnmounted = 0;
1238
+ data.checkedMounted = 0;
1059
1239
  } else {
1060
- if (!updates.idle) {
1061
- if (options.autoFreeze) {
1240
+ data.checkedUnmounted += Math.max(passingOpt.mountBatchSize, 0);
1241
+ data.checkedMounted += Math.max(passingOpt.unmountBatchSize, 0);
1242
+ // The `scheduleCellsVisibilityUpdate` could have scheduled some insertions
1243
+ // (note that removals are currently done synchronously).
1244
+ if (options.autoFreeze && !this.hasScheduledUpdates()) {
1245
+ // If there are no updates scheduled and we checked all unmounted views,
1246
+ if (
1247
+ data.checkedUnmounted >= unmountedList.length &&
1248
+ data.checkedMounted >= mountedList.length
1249
+ ) {
1250
+ // We freeze the paper and notify the idle state.
1062
1251
  this.freeze();
1063
- updates.idle = true;
1252
+ updates.idle = { wakeUpOptions: opt };
1064
1253
  this.trigger('render:idle', opt);
1065
1254
  }
1066
1255
  }
@@ -1080,6 +1269,7 @@ export const Paper = View.extend({
1080
1269
  },
1081
1270
 
1082
1271
  notifyBeforeRender: function(opt = {}) {
1272
+ if (opt.silent) return;
1083
1273
  let beforeFn = opt.beforeRender;
1084
1274
  if (typeof beforeFn !== 'function') {
1085
1275
  beforeFn = this.options.beforeRender;
@@ -1089,6 +1279,7 @@ export const Paper = View.extend({
1089
1279
  },
1090
1280
 
1091
1281
  notifyAfterRender: function(stats, opt = {}) {
1282
+ if (opt.silent) return;
1092
1283
  let afterFn = opt.afterRender;
1093
1284
  if (typeof afterFn !== 'function') {
1094
1285
  afterFn = this.options.afterRender;
@@ -1099,6 +1290,56 @@ export const Paper = View.extend({
1099
1290
  this.trigger('render:done', stats, opt);
1100
1291
  },
1101
1292
 
1293
+ prioritizeCellViewMount: function(cellOrId) {
1294
+ if (!cellOrId) return false;
1295
+ const cid = this._idToCid[cellOrId.id || cellOrId];
1296
+ if (!cid) return false;
1297
+ const { unmountedList } = this._updates;
1298
+ if (!unmountedList.has(cid)) return false;
1299
+ // Move the view to the head of the mounted list
1300
+ unmountedList.moveToHead(cid);
1301
+ return true;
1302
+ },
1303
+
1304
+ prioritizeCellViewUnmount: function(cellOrId) {
1305
+ if (!cellOrId) return false;
1306
+ const cid = this._idToCid[cellOrId.id || cellOrId];
1307
+ if (!cid) return false;
1308
+ const { mountedList } = this._updates;
1309
+ if (!mountedList.has(cid)) return false;
1310
+ // Move the view to the head of the unmounted list
1311
+ mountedList.moveToHead(cid);
1312
+ return true;
1313
+ },
1314
+
1315
+ _evalCellVisibility: function(viewLike, isMounted, visibilityCallback) {
1316
+ if (!visibilityCallback || !viewLike.DETACHABLE) return true;
1317
+ if (this.legacyMode) {
1318
+ return visibilityCallback.call(this, viewLike, isMounted, this);
1319
+ }
1320
+ // The visibility check runs for CellView only.
1321
+ if (!viewLike[CELL_VIEW_MARKER] && !viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) return true;
1322
+ // The cellView model must be a member of this graph.
1323
+ if (viewLike.model.graph !== this.model) {
1324
+ // It could have been removed from the graph.
1325
+ // If the view was mounted, we keep it mounted.
1326
+ return isMounted;
1327
+ }
1328
+ return visibilityCallback.call(this, viewLike.model, isMounted, this);
1329
+ },
1330
+
1331
+ _getCellVisibilityCallback: function(opt) {
1332
+ const { options } = this;
1333
+ if (this.legacyMode) {
1334
+ const viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
1335
+ if (typeof viewportFn === 'function') return viewportFn;
1336
+ } else {
1337
+ const isVisibleFn = 'cellVisibility' in opt ? opt.cellVisibility : options.cellVisibility;
1338
+ if (typeof isVisibleFn === 'function') return isVisibleFn;
1339
+ }
1340
+ return null;
1341
+ },
1342
+
1102
1343
  updateViewsBatch: function(opt) {
1103
1344
  opt || (opt = {});
1104
1345
  var batchSize = opt.batchSize || UPDATE_BATCH_SIZE;
@@ -1111,8 +1352,7 @@ export const Paper = View.extend({
1111
1352
  var empty = true;
1112
1353
  var options = this.options;
1113
1354
  var priorities = updates.priorities;
1114
- var viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
1115
- if (typeof viewportFn !== 'function') viewportFn = null;
1355
+ const visibilityCb = this._getCellVisibilityCallback(opt);
1116
1356
  var postponeViewFn = options.onViewPostponed;
1117
1357
  if (typeof postponeViewFn !== 'function') postponeViewFn = null;
1118
1358
  var priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
@@ -1124,33 +1364,56 @@ export const Paper = View.extend({
1124
1364
  empty = false;
1125
1365
  break main;
1126
1366
  }
1127
- var view = views[cid];
1367
+ var view = viewsRegistry[cid];
1128
1368
  if (!view) {
1129
- // This should not occur
1130
- delete priorityUpdates[cid];
1131
- continue;
1369
+ view = this._viewPlaceholders[cid];
1370
+ if (!view) {
1371
+ /**
1372
+ * This can occur when:
1373
+ * - the model is removed and a new model with the same id is added
1374
+ * - the view `initialize` method was overridden and the view was not registered
1375
+ * - an mvc.View scheduled an update, was removed and paper was not notified
1376
+ */
1377
+ delete priorityUpdates[cid];
1378
+ continue;
1379
+ }
1132
1380
  }
1133
1381
  var currentFlag = priorityUpdates[cid];
1134
- if ((currentFlag & view.FLAG_REMOVE) === 0) {
1382
+ if ((currentFlag & this.FLAG_REMOVE) === 0) {
1135
1383
  // We should never check a view for viewport if we are about to remove the view
1136
- var isDetached = cid in updates.unmounted;
1137
- if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, !isDetached, this)) {
1384
+ const isMounted = !updates.unmountedList.has(cid);
1385
+ if (!this._evalCellVisibility(view, isMounted, visibilityCb)) {
1138
1386
  // Unmount View
1139
- if (!isDetached) {
1387
+ if (isMounted) {
1388
+ // The view is currently mounted. Hide the view (detach or remove it).
1140
1389
  this.registerUnmountedView(view);
1141
- this.detachView(view);
1390
+ this._hideView(view);
1391
+ } else {
1392
+ // The view is not mounted. We can just update the unmounted list.
1393
+ // We ADD the current flag to the flag that was already scheduled.
1394
+ this._mergeUnmountedViewScheduledUpdates(cid, currentFlag);
1142
1395
  }
1143
- updates.unmounted[cid] |= currentFlag;
1396
+ // Delete the current update as it has been processed.
1144
1397
  delete priorityUpdates[cid];
1145
1398
  unmountCount++;
1146
1399
  continue;
1147
1400
  }
1148
1401
  // Mount View
1149
- if (isDetached) {
1150
- currentFlag |= view.FLAG_INSERT;
1402
+ if (view[CELL_VIEW_PLACEHOLDER_MARKER]) {
1403
+ view = this._resolveCellViewPlaceholder(view);
1404
+ // Newly initialized view needs to be initialized
1405
+ currentFlag |= this.getCellViewInitFlag(view);
1406
+ }
1407
+
1408
+ if (!isMounted) {
1409
+ currentFlag |= this.FLAG_INSERT;
1151
1410
  mountCount++;
1152
1411
  }
1153
1412
  currentFlag |= this.registerMountedView(view);
1413
+ } else if (view[CELL_VIEW_PLACEHOLDER_MARKER]) {
1414
+ // We are trying to remove a placeholder view.
1415
+ // This should not occur as the placeholder should have been unregistered
1416
+ continue;
1154
1417
  }
1155
1418
  var leftoverFlag = this.updateView(view, currentFlag, opt);
1156
1419
  if (leftoverFlag > 0) {
@@ -1177,104 +1440,125 @@ export const Paper = View.extend({
1177
1440
  };
1178
1441
  },
1179
1442
 
1443
+ getCellViewInitFlag: function(cellView) {
1444
+ return this.FLAG_INIT | cellView.getFlag(result(cellView, 'initFlag'));
1445
+ },
1446
+
1447
+ /**
1448
+ * @ignore This method returns an array of cellViewLike objects and therefore
1449
+ * is meant for internal/test use only.
1450
+ * The view placeholders are not exposed via public API.
1451
+ */
1180
1452
  getUnmountedViews: function() {
1181
1453
  const updates = this._updates;
1182
- const unmountedCids = Object.keys(updates.unmounted);
1183
- const n = unmountedCids.length;
1184
- const unmountedViews = new Array(n);
1185
- for (var i = 0; i < n; i++) {
1186
- unmountedViews[i] = views[unmountedCids[i]];
1454
+ const unmountedViews = new Array(updates.unmountedList.length);
1455
+ const unmountedCids = updates.unmountedList.keys();
1456
+ let i = 0;
1457
+ for (const cid of unmountedCids) {
1458
+ // If the view is a placeholder, it won't be in the global views map
1459
+ // If the view is not a cell view, it won't be in the viewPlaceholders map
1460
+ unmountedViews[i++] = viewsRegistry[cid] || this._viewPlaceholders[cid];
1187
1461
  }
1188
1462
  return unmountedViews;
1189
1463
  },
1190
1464
 
1465
+ /**
1466
+ * @ignore This method returns an array of cellViewLike objects and therefore
1467
+ * is meant for internal/test use only.
1468
+ * The view placeholders are not exposed via public API.
1469
+ */
1191
1470
  getMountedViews: function() {
1192
1471
  const updates = this._updates;
1193
- const mountedCids = Object.keys(updates.mounted);
1194
- const n = mountedCids.length;
1195
- const mountedViews = new Array(n);
1196
- for (var i = 0; i < n; i++) {
1197
- mountedViews[i] = views[mountedCids[i]];
1472
+ const mountedViews = new Array(updates.mountedList.length);
1473
+ const mountedCids = updates.mountedList.keys();
1474
+ let i = 0;
1475
+ for (const cid of mountedCids) {
1476
+ mountedViews[i++] = viewsRegistry[cid] || this._viewPlaceholders[cid];
1198
1477
  }
1199
1478
  return mountedViews;
1200
1479
  },
1201
1480
 
1202
- checkUnmountedViews: function(viewportFn, opt) {
1481
+ checkUnmountedViews: function(visibilityCb, opt) {
1203
1482
  opt || (opt = {});
1204
1483
  var mountCount = 0;
1205
- if (typeof viewportFn !== 'function') viewportFn = null;
1484
+ if (typeof visibilityCb !== 'function') visibilityCb = null;
1206
1485
  var batchSize = 'mountBatchSize' in opt ? opt.mountBatchSize : Infinity;
1207
1486
  var updates = this._updates;
1208
- var unmountedCids = updates.unmountedCids;
1209
- var unmounted = updates.unmounted;
1210
- for (var i = 0, n = Math.min(unmountedCids.length, batchSize); i < n; i++) {
1211
- var cid = unmountedCids[i];
1212
- if (!(cid in unmounted)) continue;
1213
- var view = views[cid];
1214
- if (!view) continue;
1215
- if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, false, this)) {
1487
+ var unmountedList = updates.unmountedList;
1488
+ for (var i = 0, n = Math.min(unmountedList.length, batchSize); i < n; i++) {
1489
+ const { key: cid } = unmountedList.peekHead();
1490
+ let view = viewsRegistry[cid] || this._viewPlaceholders[cid];
1491
+ if (!view) {
1492
+ // This should not occur
1493
+ continue;
1494
+ }
1495
+ if (!this._evalCellVisibility(view, false, visibilityCb)) {
1216
1496
  // Push at the end of all unmounted ids, so this can be check later again
1217
- unmountedCids.push(cid);
1497
+ unmountedList.rotate();
1218
1498
  continue;
1219
1499
  }
1500
+ // Remove the view from the unmounted list
1501
+ const { value: prevFlag } = unmountedList.popHead();
1220
1502
  mountCount++;
1221
- var flag = this.registerMountedView(view);
1503
+ const flag = this.registerMountedView(view) | prevFlag;
1222
1504
  if (flag) this.scheduleViewUpdate(view, flag, view.UPDATE_PRIORITY, { mounting: true });
1223
1505
  }
1224
- // Get rid of views, that have been mounted
1225
- unmountedCids.splice(0, i);
1226
1506
  return mountCount;
1227
1507
  },
1228
1508
 
1229
- checkMountedViews: function(viewportFn, opt) {
1509
+ checkMountedViews: function(visibilityCb, opt) {
1230
1510
  opt || (opt = {});
1231
1511
  var unmountCount = 0;
1232
- if (typeof viewportFn !== 'function') return unmountCount;
1512
+ if (typeof visibilityCb !== 'function') return unmountCount;
1233
1513
  var batchSize = 'unmountBatchSize' in opt ? opt.unmountBatchSize : Infinity;
1234
1514
  var updates = this._updates;
1235
- var mountedCids = updates.mountedCids;
1236
- var mounted = updates.mounted;
1237
- for (var i = 0, n = Math.min(mountedCids.length, batchSize); i < n; i++) {
1238
- var cid = mountedCids[i];
1239
- if (!(cid in mounted)) continue;
1240
- var view = views[cid];
1241
- if (!view) continue;
1242
- if (!view.DETACHABLE || viewportFn.call(this, view, true, this)) {
1515
+ const mountedList = updates.mountedList;
1516
+ for (var i = 0, n = Math.min(mountedList.length, batchSize); i < n; i++) {
1517
+ const { key: cid } = mountedList.peekHead();
1518
+ const view = viewsRegistry[cid];
1519
+ if (!view) {
1520
+ // A view (not a cell view) has been removed from the paper.
1521
+ // Remove it from the mounted list and continue.
1522
+ mountedList.popHead();
1523
+ continue;
1524
+ }
1525
+ if (this._evalCellVisibility(view, true, visibilityCb)) {
1243
1526
  // Push at the end of all mounted ids, so this can be check later again
1244
- mountedCids.push(cid);
1527
+ mountedList.rotate();
1245
1528
  continue;
1246
1529
  }
1530
+ // Remove the view from the mounted list
1531
+ mountedList.popHead();
1247
1532
  unmountCount++;
1248
1533
  var flag = this.registerUnmountedView(view);
1249
- if (flag) this.detachView(view);
1534
+ if (flag) {
1535
+ this._hideView(view);
1536
+ }
1250
1537
  }
1251
- // Get rid of views, that have been unmounted
1252
- mountedCids.splice(0, i);
1253
1538
  return unmountCount;
1254
1539
  },
1255
1540
 
1256
1541
  checkViewVisibility: function(cellView, opt = {}) {
1257
- let viewportFn = 'viewport' in opt ? opt.viewport : this.options.viewport;
1258
- if (typeof viewportFn !== 'function') viewportFn = null;
1542
+ const visibilityCb = this._getCellVisibilityCallback(opt);
1259
1543
  const updates = this._updates;
1260
- const { mounted, unmounted } = updates;
1261
- const visible = !cellView.DETACHABLE || !viewportFn || viewportFn.call(this, cellView, false, this);
1544
+ const { mountedList, unmountedList } = updates;
1545
+
1546
+ const visible = this._evalCellVisibility(cellView, false, visibilityCb);
1262
1547
 
1263
1548
  let isUnmounted = false;
1264
1549
  let isMounted = false;
1265
1550
 
1266
- if (cellView.cid in mounted && !visible) {
1551
+ if (mountedList.has(cellView.cid) && !visible) {
1267
1552
  const flag = this.registerUnmountedView(cellView);
1268
- if (flag) this.detachView(cellView);
1269
- const i = updates.mountedCids.indexOf(cellView.cid);
1270
- updates.mountedCids.splice(i, 1);
1553
+ if (flag) this._hideView(cellView);
1554
+ mountedList.delete(cellView.cid);
1271
1555
  isUnmounted = true;
1272
1556
  }
1273
1557
 
1274
- if (!isUnmounted && cellView.cid in unmounted && visible) {
1275
- const i = updates.unmountedCids.indexOf(cellView.cid);
1276
- updates.unmountedCids.splice(i, 1);
1277
- var flag = this.registerMountedView(cellView);
1558
+ if (!isUnmounted && unmountedList.has(cellView.cid) && visible) {
1559
+ const unmountedItem = unmountedList.get(cellView.cid);
1560
+ unmountedList.delete(cellView.cid);
1561
+ const flag = unmountedItem.value | this.registerMountedView(cellView);
1278
1562
  if (flag) this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, { mounting: true });
1279
1563
  isMounted = true;
1280
1564
  }
@@ -1285,25 +1569,65 @@ export const Paper = View.extend({
1285
1569
  };
1286
1570
  },
1287
1571
 
1288
- checkViewport: function(opt) {
1289
- var passingOpt = defaults({}, opt, {
1572
+ /**
1573
+ * @public
1574
+ * Update the visibility of a single cell.
1575
+ */
1576
+ updateCellVisibility: function(cell, opt = {}) {
1577
+ const cellViewLike = this._getCellViewLike(cell);
1578
+ if (!cellViewLike) return;
1579
+ const stats = this.checkViewVisibility(cellViewLike, opt);
1580
+ // Note: `unmounted` views are removed immediately
1581
+ if (stats.mounted > 0) {
1582
+ // Mounting is scheduled. Run the update.
1583
+ // Note: the view might be a placeholder.
1584
+ this.requireView(cell, opt);
1585
+ }
1586
+ },
1587
+
1588
+ /**
1589
+ * @public
1590
+ * Update the visibility of all cells.
1591
+ */
1592
+ updateCellsVisibility: function(opt = {}) {
1593
+ // Check the visibility of all cells and schedule their updates.
1594
+ this.scheduleCellsVisibilityUpdate(opt);
1595
+ // Perform the scheduled updates while avoiding re-evaluating the visibility.
1596
+ const keepCurrentVisibility = (_, isVisible) => isVisible;
1597
+ this.updateViews({ ...opt, cellVisibility: keepCurrentVisibility });
1598
+ },
1599
+
1600
+ /**
1601
+ * @protected
1602
+ * Run visibility checks for all cells and schedule their updates.
1603
+ */
1604
+ scheduleCellsVisibilityUpdate(opt) {
1605
+ const passingOpt = defaults({}, opt, {
1290
1606
  mountBatchSize: Infinity,
1291
1607
  unmountBatchSize: Infinity
1292
1608
  });
1293
- var viewportFn = 'viewport' in passingOpt ? passingOpt.viewport : this.options.viewport;
1294
- var unmountedCount = this.checkMountedViews(viewportFn, passingOpt);
1609
+ const visibilityCb = this._getCellVisibilityCallback(passingOpt);
1610
+ const unmountedCount = this.checkMountedViews(visibilityCb, passingOpt);
1295
1611
  if (unmountedCount > 0) {
1296
1612
  // Do not check views, that have been just unmounted and pushed at the end of the cids array
1297
- var unmountedCids = this._updates.unmountedCids;
1298
- passingOpt.mountBatchSize = Math.min(unmountedCids.length - unmountedCount, passingOpt.mountBatchSize);
1613
+ var unmountedList = this._updates.unmountedList;
1614
+ passingOpt.mountBatchSize = Math.min(unmountedList.length - unmountedCount, passingOpt.mountBatchSize);
1299
1615
  }
1300
- var mountedCount = this.checkUnmountedViews(viewportFn, passingOpt);
1616
+ const mountedCount = this.checkUnmountedViews(visibilityCb, passingOpt);
1301
1617
  return {
1302
1618
  mounted: mountedCount,
1303
1619
  unmounted: unmountedCount
1304
1620
  };
1305
1621
  },
1306
1622
 
1623
+ /**
1624
+ * @deprecated use `updateCellsVisibility` instead
1625
+ * This method will be renamed and made private in the future.
1626
+ */
1627
+ checkViewport: function(opt) {
1628
+ return this.scheduleCellsVisibilityUpdate(opt);
1629
+ },
1630
+
1307
1631
  freeze: function(opt) {
1308
1632
  opt || (opt = {});
1309
1633
  var updates = this._updates;
@@ -1319,6 +1643,10 @@ export const Paper = View.extend({
1319
1643
  this.options.frozen = true;
1320
1644
  var id = updates.id;
1321
1645
  updates.id = null;
1646
+ if (!this.legacyMode) {
1647
+ // Make sure the `freeze()` method ends the idle state.
1648
+ updates.idle = false;
1649
+ }
1322
1650
  if (this.isAsync() && id) cancelFrame(id);
1323
1651
  },
1324
1652
 
@@ -1332,6 +1660,7 @@ export const Paper = View.extend({
1332
1660
  updates.freezeKey = null;
1333
1661
  // key passed, but the paper is already freezed
1334
1662
  if (key && key === freezeKey && updates.keyFrozen) return;
1663
+ updates.idle = false;
1335
1664
  if (this.isAsync()) {
1336
1665
  this.freeze();
1337
1666
  this.updateViewsAsync(opt);
@@ -1345,12 +1674,25 @@ export const Paper = View.extend({
1345
1674
  }
1346
1675
  },
1347
1676
 
1677
+ wakeUp: function() {
1678
+ if (!this.isIdle()) return;
1679
+ this.unfreeze(this._updates.idle.wakeUpOptions);
1680
+ },
1681
+
1348
1682
  isAsync: function() {
1349
1683
  return !!this.options.async;
1350
1684
  },
1351
1685
 
1352
1686
  isFrozen: function() {
1353
- return !!this.options.frozen;
1687
+ return !!this.options.frozen && !this.isIdle();
1688
+ },
1689
+
1690
+ isIdle: function() {
1691
+ if (this.legacyMode) {
1692
+ // Not implemented in the legacy mode.
1693
+ return false;
1694
+ }
1695
+ return !!(this._updates && this._updates.idle);
1354
1696
  },
1355
1697
 
1356
1698
  isExactSorting: function() {
@@ -1650,21 +1992,57 @@ export const Paper = View.extend({
1650
1992
  return restrictedArea;
1651
1993
  },
1652
1994
 
1653
- createViewForModel: function(cell) {
1995
+ _resolveCellViewPlaceholder: function(placeholder) {
1996
+ const { model, viewClass, cid } = placeholder;
1997
+ const view = this._initializeCellView(viewClass, model, cid);
1998
+ this._registerCellView(view);
1999
+ this._unregisterCellViewPlaceholder(placeholder);
2000
+ return view;
2001
+ },
1654
2002
 
1655
- const { options } = this;
1656
- // A class taken from the paper options.
1657
- var optionalViewClass;
2003
+ _registerCellViewPlaceholder: function(cell, cid = uniqueId('view')) {
2004
+ const ViewClass = this._resolveCellViewClass(cell);
2005
+ const placeholder = {
2006
+ // A tag to identify the placeholder from a CellView.
2007
+ [CELL_VIEW_PLACEHOLDER_MARKER]: true,
2008
+ cid,
2009
+ model: cell,
2010
+ DETACHABLE: true,
2011
+ viewClass: ViewClass,
2012
+ UPDATE_PRIORITY: ViewClass.prototype.UPDATE_PRIORITY,
2013
+ };
2014
+ this._viewPlaceholders[cid] = placeholder;
2015
+ return placeholder;
2016
+ },
1658
2017
 
1659
- // A default basic class (either dia.ElementView or dia.LinkView)
1660
- var defaultViewClass;
2018
+ _registerCellView: function(cellView) {
2019
+ cellView.paper = this;
2020
+ this._views[cellView.model.id] = cellView;
2021
+ },
1661
2022
 
1662
- // A special class defined for this model in the corresponding namespace.
1663
- // e.g. joint.shapes.standard.Rectangle searches for joint.shapes.standard.RectangleView
1664
- var namespace = options.cellViewNamespace;
1665
- var type = cell.get('type') + 'View';
1666
- var namespaceViewClass = getByPath(namespace, type, '.');
2023
+ _unregisterCellViewPlaceholder: function(placeholder) {
2024
+ delete this._viewPlaceholders[placeholder.cid];
2025
+ },
1667
2026
 
2027
+ _initializeCellView: function(ViewClass, cell, cid) {
2028
+ const { options } = this;
2029
+ const { interactive, labelsLayer } = options;
2030
+ return new ViewClass({
2031
+ cid,
2032
+ model: cell,
2033
+ interactive,
2034
+ labelsLayer: labelsLayer === true ? LayersNames.LABELS : labelsLayer
2035
+ });
2036
+ },
2037
+
2038
+ _resolveCellViewClass: function(cell) {
2039
+ const { options } = this;
2040
+ const { cellViewNamespace } = options;
2041
+ const type = cell.get('type') + 'View';
2042
+ const namespaceViewClass = getByPath(cellViewNamespace, type, '.');
2043
+ // A class taken from the paper options.
2044
+ let optionalViewClass;
2045
+ let defaultViewClass;
1668
2046
  if (cell.isLink()) {
1669
2047
  optionalViewClass = options.linkView;
1670
2048
  defaultViewClass = LinkView;
@@ -1672,7 +2050,6 @@ export const Paper = View.extend({
1672
2050
  optionalViewClass = options.elementView;
1673
2051
  defaultViewClass = ElementView;
1674
2052
  }
1675
-
1676
2053
  // a) the paper options view is a class (deprecated)
1677
2054
  // 1. search the namespace for a view
1678
2055
  // 2. if no view was found, use view from the paper options
@@ -1680,29 +2057,54 @@ export const Paper = View.extend({
1680
2057
  // 1. call the function from the paper options
1681
2058
  // 2. if no view was return, search the namespace for a view
1682
2059
  // 3. if no view was found, use the default
1683
- var ViewClass = (optionalViewClass.prototype instanceof ViewBase)
2060
+ return (optionalViewClass.prototype instanceof ViewBase)
1684
2061
  ? namespaceViewClass || optionalViewClass
1685
2062
  : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass;
2063
+ },
1686
2064
 
1687
- return new ViewClass({
1688
- model: cell,
1689
- interactive: options.interactive,
1690
- labelsLayer: options.labelsLayer === true ? LayersNames.LABELS : options.labelsLayer
1691
- });
2065
+ // Returns a CellView instance or its placeholder for the given cell.
2066
+ _getCellViewLike: function(cell) {
2067
+
2068
+ let id;
2069
+ if (isString(cell) || isNumber(cell)) {
2070
+ // If the cell is a string or number, it is an id of the view.
2071
+ id = cell;
2072
+ } else if (cell) {
2073
+ // If the cell is an object, it should have an id property.
2074
+ id = cell.id;
2075
+ } else {
2076
+ // If the cell is falsy, return null.
2077
+ return null;
2078
+ }
2079
+
2080
+ const view = this._views[id];
2081
+ if (view) return view;
2082
+
2083
+ // If the view is not found, it may be a placeholder
2084
+ const cid = this._idToCid[id];
2085
+ if (cid) {
2086
+ return this._viewPlaceholders[cid];
2087
+ }
2088
+
2089
+ return null;
1692
2090
  },
1693
2091
 
1694
- removeView: function(cell) {
2092
+ createViewForModel: function(cell, cid) {
2093
+ return this._initializeCellView(this._resolveCellViewClass(cell), cell, cid);
2094
+ },
1695
2095
 
2096
+ removeView: function(cell) {
1696
2097
  const { id } = cell;
1697
2098
  const { _views, _updates } = this;
1698
2099
  const view = _views[id];
1699
2100
  if (view) {
1700
2101
  var { cid } = view;
1701
- const { mounted, unmounted } = _updates;
2102
+ const { mountedList, unmountedList } = _updates;
1702
2103
  view.remove();
1703
2104
  delete _views[id];
1704
- delete mounted[cid];
1705
- delete unmounted[cid];
2105
+ delete this._idToCid[id];
2106
+ mountedList.delete(cid);
2107
+ unmountedList.delete(cid);
1706
2108
  }
1707
2109
  return view;
1708
2110
  },
@@ -1716,7 +2118,7 @@ export const Paper = View.extend({
1716
2118
  if (id in views) {
1717
2119
  view = views[id];
1718
2120
  if (view.model === cell) {
1719
- flag = view.FLAG_INSERT;
2121
+ flag = this.FLAG_INSERT;
1720
2122
  create = false;
1721
2123
  } else {
1722
2124
  // The view for this `id` already exist.
@@ -1726,14 +2128,42 @@ export const Paper = View.extend({
1726
2128
  }
1727
2129
  }
1728
2130
  if (create) {
1729
- view = views[id] = this.createViewForModel(cell);
1730
- view.paper = this;
1731
- flag = this.registerUnmountedView(view) | this.FLAG_INIT | view.getFlag(result(view, 'initFlag'));
2131
+ const { viewManagement } = this.options;
2132
+ const cid = uniqueId('view');
2133
+ this._idToCid[cell.id] = cid;
2134
+ if (viewManagement.lazyInitialize) {
2135
+ // Register only a placeholder for the view
2136
+ view = this._registerCellViewPlaceholder(cell, cid);
2137
+ flag = this.registerUnmountedView(view);
2138
+ } else {
2139
+ // Create a new view instance
2140
+ view = this.createViewForModel(cell, cid);
2141
+ this._registerCellView(view);
2142
+ flag = this.registerUnmountedView(view);
2143
+ // The newly created view needs to be initialized
2144
+ flag |= this.getCellViewInitFlag(view);
2145
+ }
2146
+ if (viewManagement.initializeUnmounted) {
2147
+ // Save the initialization flags for later and exit early
2148
+ this._mergeUnmountedViewScheduledUpdates(cid, flag);
2149
+ return view;
2150
+ }
1732
2151
  }
2152
+
1733
2153
  this.requestViewUpdate(view, flag, view.UPDATE_PRIORITY, opt);
2154
+
1734
2155
  return view;
1735
2156
  },
1736
2157
 
2158
+ // Update the view flags in the `unmountedList` using the bitwise OR operation
2159
+ _mergeUnmountedViewScheduledUpdates: function(cid, flag) {
2160
+ const { unmountedList } = this._updates;
2161
+ const unmountedItem = unmountedList.get(cid);
2162
+ if (unmountedItem) {
2163
+ unmountedItem.value |= flag;
2164
+ }
2165
+ },
2166
+
1737
2167
  onImageDragStart: function() {
1738
2168
  // This is the only way to prevent image dragging in Firefox that works.
1739
2169
  // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help.
@@ -1744,11 +2174,11 @@ export const Paper = View.extend({
1744
2174
  resetViews: function(cells, opt) {
1745
2175
  opt || (opt = {});
1746
2176
  cells || (cells = []);
2177
+ // Allows to unfreeze normally while in the idle state using autoFreeze option
2178
+ const key = (this.legacyMode ? this.options.autoFreeze : this.isIdle()) ? null : 'reset';
1747
2179
  this._resetUpdates();
1748
2180
  // clearing views removes any event listeners
1749
2181
  this.removeViews();
1750
- // Allows to unfreeze normally while in the idle state using autoFreeze option
1751
- const key = this.options.autoFreeze ? null : 'reset';
1752
2182
  this.freeze({ key });
1753
2183
  for (var i = 0, n = cells.length; i < n; i++) {
1754
2184
  this.renderView(cells[i], opt);
@@ -1758,10 +2188,16 @@ export const Paper = View.extend({
1758
2188
  },
1759
2189
 
1760
2190
  removeViews: function() {
1761
-
1762
- invoke(this._views, 'remove');
1763
-
2191
+ // Remove all views and their references from the paper.
2192
+ for (const id in this._views) {
2193
+ const view = this._views[id];
2194
+ if (view) {
2195
+ view.remove();
2196
+ }
2197
+ }
1764
2198
  this._views = {};
2199
+ this._viewPlaceholders = {};
2200
+ this._idToCid = {};
1765
2201
  },
1766
2202
 
1767
2203
  sortViews: function() {
@@ -1770,7 +2206,7 @@ export const Paper = View.extend({
1770
2206
  // noop
1771
2207
  return;
1772
2208
  }
1773
- if (this.isFrozen()) {
2209
+ if (this.isFrozen() || this.isIdle()) {
1774
2210
  // sort views once unfrozen
1775
2211
  this._updates.sort = true;
1776
2212
  return;
@@ -1796,8 +2232,11 @@ export const Paper = View.extend({
1796
2232
  },
1797
2233
 
1798
2234
  insertView: function(view, isInitialInsert) {
1799
- const layerView = this.getLayerView(LayersNames.CELLS);
1800
2235
  const { el, model } = view;
2236
+
2237
+ const layerName = model.get('layer') || this.DEFAULT_CELL_LAYER;
2238
+ const layerView = this.getLayerView(layerName);
2239
+
1801
2240
  switch (this.options.sorting) {
1802
2241
  case sortingTypes.APPROX:
1803
2242
  layerView.insertSortedNode(el, model.get('z'));
@@ -1810,9 +2249,58 @@ export const Paper = View.extend({
1810
2249
  view.onMount(isInitialInsert);
1811
2250
  },
1812
2251
 
1813
- detachView(view) {
1814
- view.unmount();
1815
- view.onDetach();
2252
+ _hideView: function(viewLike) {
2253
+ if (!viewLike || viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) {
2254
+ // A placeholder view was never mounted
2255
+ return;
2256
+ }
2257
+ if (viewLike[CELL_VIEW_MARKER]) {
2258
+ this._hideCellView(viewLike);
2259
+ } else {
2260
+ // A generic view that is not a cell view.
2261
+ viewLike.unmount();
2262
+ }
2263
+ },
2264
+
2265
+ // If `cellVisibility` returns `false`, the view will be hidden using this method.
2266
+ _hideCellView: function(cellView) {
2267
+ if (this.options.viewManagement.disposeHidden) {
2268
+ if (this._disposeCellView(cellView)) return;
2269
+ }
2270
+ // Detach the view from the paper, but keep it in memory
2271
+ this._detachCellView(cellView);
2272
+ },
2273
+
2274
+ _disposeCellView: function(cellView) {
2275
+ if (HighlighterView.has(cellView) || cellView.hasTools()) {
2276
+ // We currently do not dispose views which has a highlighter or tools attached
2277
+ // Note: Possible improvement would be to serialize highlighters/tools and
2278
+ // restore them on view re-mount.
2279
+ return false;
2280
+ }
2281
+ const cell = cellView.model;
2282
+ // Remove the view from the paper and dispose it
2283
+ cellView.remove();
2284
+ delete this._views[cell.id];
2285
+ this._registerCellViewPlaceholder(cell, cellView.cid);
2286
+ return true;
2287
+ },
2288
+
2289
+ // Dispose (release resources) all hidden views.
2290
+ disposeHiddenCellViews: function() {
2291
+ // Only cell views can be in the unmounted list (not in the legacy mode).
2292
+ if (this.legacyMode) return;
2293
+ const unmountedCids = this._updates.unmountedList.keys();
2294
+ for (const cid of unmountedCids) {
2295
+ const cellView = viewsRegistry[cid];
2296
+ cellView && this._disposeCellView(cellView);
2297
+ }
2298
+ },
2299
+
2300
+ // Detach a view from the paper, but keep it in memory.
2301
+ _detachCellView(cellView) {
2302
+ cellView.unmount();
2303
+ cellView.onDetach();
1816
2304
  },
1817
2305
 
1818
2306
  // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also
@@ -1830,11 +2318,32 @@ export const Paper = View.extend({
1830
2318
  },
1831
2319
 
1832
2320
  // Find a view for a model `cell`. `cell` can also be a string or number representing a model `id`.
1833
- findViewByModel: function(cell) {
1834
-
1835
- var id = (isString(cell) || isNumber(cell)) ? cell : (cell && cell.id);
1836
-
1837
- return this._views[id];
2321
+ findViewByModel: function(cellOrId) {
2322
+
2323
+ const cellViewLike = this._getCellViewLike(cellOrId);
2324
+ if (!cellViewLike) return undefined;
2325
+ if (cellViewLike[CELL_VIEW_MARKER]) {
2326
+ // If the view is not a placeholder, return it directly
2327
+ return cellViewLike;
2328
+ }
2329
+ // We do not expose placeholder views directly. We resolve them before returning.
2330
+ const cellView = this._resolveCellViewPlaceholder(cellViewLike);
2331
+ const flag = this.getCellViewInitFlag(cellView);
2332
+ if (this.isViewMounted(cellView)) {
2333
+ // The view was acting as a placeholder and is already present in the `mounted` list,
2334
+ // indicating that its visibility has been checked, but the update hasn't occurred yet.
2335
+ // Placeholders are resolved during the update routine. Since we're handling it
2336
+ // manually here, we must ensure the view is properly initialized on the next update.
2337
+ this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, {
2338
+ // It's important to run in isolation to avoid triggering the update of
2339
+ // connected links
2340
+ isolate: true
2341
+ });
2342
+ } else {
2343
+ // Update the flags in the `unmounted` list
2344
+ this._mergeUnmountedViewScheduledUpdates(cellView.cid, flag);
2345
+ }
2346
+ return cellView;
1838
2347
  },
1839
2348
 
1840
2349
  // Find all views at given point
@@ -1913,6 +2422,92 @@ export const Paper = View.extend({
1913
2422
  );
1914
2423
  },
1915
2424
 
2425
+ findClosestMagnetToPoint: function(point, options = {}) {
2426
+ let minDistance = Number.MAX_SAFE_INTEGER;
2427
+ let bestPriority = -Infinity;
2428
+ const pointer = new Point(point);
2429
+
2430
+ const radius = options.radius || Number.MAX_SAFE_INTEGER;
2431
+ const viewsInArea = this.findCellViewsInArea(
2432
+ { x: pointer.x - radius, y: pointer.y - radius, width: 2 * radius, height: 2 * radius },
2433
+ options.findInAreaOptions
2434
+ );
2435
+ // Enable all connections by default
2436
+ const filterFn = typeof options.filter === 'function' ? options.filter : null;
2437
+
2438
+ let closestView = null;
2439
+ let closestMagnet = null;
2440
+
2441
+ // Note: If snapRadius is smaller than magnet size, views will not be found.
2442
+ viewsInArea.forEach((view) => {
2443
+
2444
+ const candidates = [];
2445
+ const { model } = view;
2446
+ // skip connecting to the element in case '.': { magnet: false } attribute present
2447
+ if (view.el.getAttribute('magnet') !== 'false') {
2448
+
2449
+ if (model.isLink()) {
2450
+ const connection = view.getConnection();
2451
+ candidates.push({
2452
+ // find distance from the closest point of a link to pointer coordinates
2453
+ priority: 0,
2454
+ distance: connection.closestPoint(pointer).squaredDistance(pointer),
2455
+ magnet: view.el
2456
+ });
2457
+ } else {
2458
+ candidates.push({
2459
+ // Set the priority to the level of nested elements of the model
2460
+ // To ensure that the embedded cells get priority over the parent cells
2461
+ priority: model.getAncestors().length,
2462
+ // find distance from the center of the model to pointer coordinates
2463
+ distance: model.getBBox().center().squaredDistance(pointer),
2464
+ magnet: view.el
2465
+ });
2466
+ }
2467
+ }
2468
+
2469
+ view.$('[magnet]').toArray().forEach(magnet => {
2470
+
2471
+ const magnetBBox = view.getNodeBBox(magnet);
2472
+ let magnetDistance = magnetBBox.pointNearestToPoint(pointer).squaredDistance(pointer);
2473
+ if (magnetBBox.containsPoint(pointer)) {
2474
+ // Pointer sits inside this magnet.
2475
+ // Push its distance far into the negative range so any
2476
+ // "under-pointer" magnet outranks magnets that are only nearby
2477
+ // (positive distance) and every non-magnet candidate.
2478
+ // We add the original distance back to keep ordering among
2479
+ // overlapping magnets: the one whose border is closest to the
2480
+ // pointer (smaller original distance) still wins.
2481
+ magnetDistance = -Number.MAX_SAFE_INTEGER + magnetDistance;
2482
+ }
2483
+
2484
+ // Check if magnet is inside the snap radius.
2485
+ if (magnetDistance <= radius * radius) {
2486
+ candidates.push({
2487
+ // Give magnets priority over other candidates.
2488
+ priority: Number.MAX_SAFE_INTEGER,
2489
+ distance: magnetDistance,
2490
+ magnet
2491
+ });
2492
+ }
2493
+ });
2494
+
2495
+ candidates.forEach(candidate => {
2496
+ const { magnet, distance, priority } = candidate;
2497
+ const isBetterCandidate = (priority > bestPriority) || (priority === bestPriority && distance < minDistance);
2498
+ if (isBetterCandidate && (!filterFn || filterFn(view, magnet))) {
2499
+ bestPriority = priority;
2500
+ minDistance = distance;
2501
+ closestView = view;
2502
+ closestMagnet = magnet;
2503
+ }
2504
+ });
2505
+
2506
+ });
2507
+
2508
+ return closestView ? { view: closestView, magnet: closestMagnet } : null;
2509
+ },
2510
+
1916
2511
  _findInExtendedArea: function(area, findCellsFn, opt = {}) {
1917
2512
  const {
1918
2513
  buffer = this.DEFAULT_FIND_BUFFER,
@@ -2365,8 +2960,11 @@ export const Paper = View.extend({
2365
2960
 
2366
2961
  var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2367
2962
 
2368
- var view = data.sourceView;
2963
+ let view = data.sourceView;
2369
2964
  if (view) {
2965
+ // The view could have been disposed during dragging
2966
+ // e.g. dragged outside of the viewport and hidden
2967
+ view = this.findViewByModel(view.model);
2370
2968
  view.pointermove(evt, localPoint.x, localPoint.y);
2371
2969
  } else {
2372
2970
  this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y);
@@ -2383,8 +2981,11 @@ export const Paper = View.extend({
2383
2981
 
2384
2982
  var localPoint = this.snapToGrid(normalizedEvt.clientX, normalizedEvt.clientY);
2385
2983
 
2386
- var view = this.eventData(evt).sourceView;
2984
+ let view = this.eventData(evt).sourceView;
2387
2985
  if (view) {
2986
+ // The view could have been disposed during dragging
2987
+ // e.g. dragged outside of the viewport and hidden
2988
+ view = this.findViewByModel(view.model);
2388
2989
  view.pointerup(normalizedEvt, localPoint.x, localPoint.y);
2389
2990
  } else {
2390
2991
  this.trigger('blank:pointerup', normalizedEvt, localPoint.x, localPoint.y);
@@ -3254,4 +3855,3 @@ export const Paper = View.extend({
3254
3855
  }]
3255
3856
  }
3256
3857
  });
3257
-