@joint/core 4.2.0-alpha.0 → 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.
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
 
@@ -390,6 +397,11 @@ export const Paper = View.extend({
390
397
  // Default layer to insert the cell views into.
391
398
  DEFAULT_CELL_LAYER: LayersNames.CELLS,
392
399
 
400
+ // Update flags
401
+ FLAG_INSERT: 1<<30,
402
+ FLAG_REMOVE: 1<<29,
403
+ FLAG_INIT: 1<<28,
404
+
393
405
  init: function() {
394
406
 
395
407
  const { options } = this;
@@ -401,6 +413,10 @@ export const Paper = View.extend({
401
413
 
402
414
  const model = this.model = options.model || new Graph;
403
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
+
404
420
  // Layers (SVGGroups)
405
421
  this._layers = {
406
422
  viewsMap: {},
@@ -415,6 +431,8 @@ export const Paper = View.extend({
415
431
 
416
432
  // Hash of all cell views.
417
433
  this._views = {};
434
+ this._viewPlaceholders = {};
435
+ this._idToCid = {};
418
436
 
419
437
  // Mouse wheel events buffer
420
438
  this._mw_evt_buffer = {
@@ -424,8 +442,6 @@ export const Paper = View.extend({
424
442
 
425
443
  // Render existing cells in the graph
426
444
  this.resetViews(model.attributes.cells.models);
427
- // Start the Rendering Loop
428
- if (!this.isFrozen() && this.isAsync()) this.updateViewsAsync();
429
445
  },
430
446
 
431
447
  _resetUpdates: function() {
@@ -434,16 +450,15 @@ export const Paper = View.extend({
434
450
  return this._updates = {
435
451
  id: null,
436
452
  priorities: [{}, {}, {}],
437
- unmountedCids: [],
438
- mountedCids: [],
439
- unmounted: {},
440
- mounted: {},
453
+ unmountedList: new Deque(),
454
+ mountedList: new Deque(),
441
455
  count: 0,
442
456
  keyFrozen: false,
443
457
  freezeKey: null,
444
458
  sort: false,
445
459
  disabled: false,
446
- idle: false
460
+ idle: false,
461
+ freshAfterReset: true,
447
462
  };
448
463
  },
449
464
 
@@ -472,8 +487,13 @@ export const Paper = View.extend({
472
487
  },
473
488
 
474
489
  onCellRemoved: function(cell, _, opt) {
475
- const view = this.findViewByModel(cell);
476
- 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
+ }
477
497
  },
478
498
 
479
499
  onCellChange: function(cell, opt) {
@@ -482,8 +502,10 @@ export const Paper = View.extend({
482
502
  cell.hasChanged('layer') ||
483
503
  (cell.hasChanged('z') && this.options.sorting === sortingTypes.APPROX)
484
504
  ) {
485
- const view = this.findViewByModel(cell);
486
- if (view) this.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
505
+ const viewLike = this._getCellViewLike(cell);
506
+ if (viewLike) {
507
+ this.requestViewUpdate(viewLike, this.FLAG_INSERT, viewLike.UPDATE_PRIORITY, opt);
508
+ }
487
509
  }
488
510
  },
489
511
 
@@ -498,7 +520,7 @@ export const Paper = View.extend({
498
520
  },
499
521
 
500
522
  onGraphBatchStop: function(data) {
501
- if (this.isFrozen()) return;
523
+ if (this.isFrozen() || this.isIdle()) return;
502
524
  var name = data && data.batchName;
503
525
  var graph = this.model;
504
526
  if (!this.isAsync()) {
@@ -558,6 +580,15 @@ export const Paper = View.extend({
558
580
  // Return the default highlighting options into the user specified options.
559
581
  options.highlighting = defaultsDeep({}, highlighting, defaultHighlighting);
560
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
+ });
561
592
  },
562
593
 
563
594
  children: function() {
@@ -925,47 +956,46 @@ export const Paper = View.extend({
925
956
  var links = this.model.getConnectedLinks(model);
926
957
  for (var j = 0, n = links.length; j < n; j++) {
927
958
  var link = links[j];
928
- var linkView = this.findViewByModel(link);
959
+ var linkView = this._getCellViewLike(link);
929
960
  if (!linkView) continue;
930
- var flagLabels = ['UPDATE'];
931
- if (link.getTargetCell() === model) flagLabels.push('TARGET');
932
- 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;
933
964
  var nextPriority = Math.max(priority + 1, linkView.UPDATE_PRIORITY);
934
- this.scheduleViewUpdate(linkView, linkView.getFlag(flagLabels), nextPriority, opt);
965
+ this.scheduleViewUpdate(linkView, linkView.getFlag(LinkView.Flags.UPDATE), nextPriority, opt);
935
966
  }
936
967
  }
937
968
  },
938
969
 
939
970
  forcePostponedViewUpdate: function(view, flag) {
940
971
  if (!view || !(view instanceof CellView)) return false;
941
- var model = view.model;
972
+ const model = view.model;
942
973
  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
- }
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);
963
992
  }
964
993
  return false;
965
994
  },
966
995
 
967
996
  requestViewUpdate: function(view, flag, priority, opt) {
968
997
  opt || (opt = {});
998
+ // Note: `scheduleViewUpdate` wakes up the paper if it is idle.
969
999
  this.scheduleViewUpdate(view, flag, priority, opt);
970
1000
  var isAsync = this.isAsync();
971
1001
  if (this.isFrozen() || (isAsync && opt.async !== false)) return;
@@ -976,13 +1006,14 @@ export const Paper = View.extend({
976
1006
 
977
1007
  scheduleViewUpdate: function(view, type, priority, opt) {
978
1008
  const { _updates: updates, options } = this;
979
- if (updates.idle) {
980
- if (options.autoFreeze) {
981
- updates.idle = false;
982
- this.unfreeze();
983
- }
1009
+ if (updates.idle && options.autoFreeze) {
1010
+ this.legacyMode
1011
+ ? this.unfreeze() // Restart rendering loop without original options
1012
+ : this.wakeUp();
984
1013
  }
985
- const { FLAG_REMOVE, FLAG_INSERT, UPDATE_PRIORITY, cid } = view;
1014
+ const { FLAG_REMOVE, FLAG_INSERT } = this;
1015
+ const { UPDATE_PRIORITY, cid } = view;
1016
+
986
1017
  let priorityUpdates = updates.priorities[priority];
987
1018
  if (!priorityUpdates) priorityUpdates = updates.priorities[priority] = {};
988
1019
  // Move higher priority updates to this priority
@@ -1028,20 +1059,18 @@ export const Paper = View.extend({
1028
1059
  dumpView: function(view, opt = {}) {
1029
1060
  const flag = this.dumpViewUpdate(view);
1030
1061
  if (!flag) return 0;
1031
- const shouldNotify = !opt.silent;
1032
- if (shouldNotify) this.notifyBeforeRender(opt);
1062
+ this.notifyBeforeRender(opt);
1033
1063
  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
- }
1064
+ const stats = { updated: 1, priority: view.UPDATE_PRIORITY };
1065
+ this.notifyAfterRender(stats, opt);
1038
1066
  return leftover;
1039
1067
  },
1040
1068
 
1041
1069
  updateView: function(view, flag, opt) {
1042
1070
  if (!view) return 0;
1043
- const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT, model } = view;
1044
- if (view instanceof CellView) {
1071
+ const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT } = this;
1072
+ const { model } = view;
1073
+ if (view[CELL_VIEW_MARKER]) {
1045
1074
  if (flag & FLAG_REMOVE) {
1046
1075
  this.removeView(model);
1047
1076
  return 0;
@@ -1069,57 +1098,70 @@ export const Paper = View.extend({
1069
1098
  registerUnmountedView: function(view) {
1070
1099
  var cid = view.cid;
1071
1100
  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];
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);
1076
1105
  return flag;
1077
1106
  },
1078
1107
 
1079
1108
  registerMountedView: function(view) {
1080
1109
  var cid = view.cid;
1081
1110
  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];
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);
1087
1116
  return flag;
1088
1117
  },
1089
1118
 
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);
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);
1123
+ },
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);
1095
1134
  },
1096
1135
 
1136
+ /**
1137
+ * @deprecated use `updateCellsVisibility` instead.
1138
+ * `paper.updateCellsVisibility({ cellVisibility: () => true });`
1139
+ */
1097
1140
  dumpViews: function(opt) {
1098
- var passingOpt = defaults({}, opt, { viewport: null });
1099
- this.checkViewport(passingOpt);
1100
- 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);
1101
1144
  },
1102
1145
 
1103
- // Synchronous views update
1104
- updateViews: function(opt) {
1146
+ /**
1147
+ * Process all scheduled updates synchronously.
1148
+ */
1149
+ updateViews: function(opt = {}) {
1105
1150
  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 };
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
+ };
1117
1158
  this.notifyAfterRender(stats, opt);
1118
1159
  return stats;
1119
1160
  },
1120
1161
 
1121
1162
  hasScheduledUpdates: function() {
1122
- const priorities = this._updates.priorities;
1163
+ const updates = this._updates;
1164
+ const priorities = updates.priorities;
1123
1165
  const priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
1124
1166
  let i = priorityIndexes.length;
1125
1167
  while (i > 0 && i--) {
@@ -1131,11 +1173,38 @@ export const Paper = View.extend({
1131
1173
 
1132
1174
  updateViewsAsync: function(opt, data) {
1133
1175
  opt || (opt = {});
1134
- data || (data = { processed: 0, priority: MIN_PRIORITY });
1176
+ data || (data = {
1177
+ processed: 0,
1178
+ priority: MIN_PRIORITY,
1179
+ checkedUnmounted: 0,
1180
+ checkedMounted: 0,
1181
+ });
1135
1182
  const { _updates: updates, options } = this;
1136
- const id = updates.id;
1137
- 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.
1138
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) {
1139
1208
  if (data.processed === 0 && this.hasScheduledUpdates()) {
1140
1209
  this.notifyBeforeRender(opt);
1141
1210
  }
@@ -1144,7 +1213,7 @@ export const Paper = View.extend({
1144
1213
  mountBatchSize: MOUNT_BATCH_SIZE - stats.mounted,
1145
1214
  unmountBatchSize: MOUNT_BATCH_SIZE - stats.unmounted
1146
1215
  });
1147
- const checkStats = this.checkViewport(passingOpt);
1216
+ const checkStats = this.scheduleCellsVisibilityUpdate(passingOpt);
1148
1217
  const unmountCount = checkStats.unmounted;
1149
1218
  const mountCount = checkStats.mounted;
1150
1219
  let processed = data.processed;
@@ -1165,11 +1234,22 @@ export const Paper = View.extend({
1165
1234
  } else {
1166
1235
  data.processed = processed;
1167
1236
  }
1237
+ data.checkedUnmounted = 0;
1238
+ data.checkedMounted = 0;
1168
1239
  } else {
1169
- if (!updates.idle) {
1170
- 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.
1171
1251
  this.freeze();
1172
- updates.idle = true;
1252
+ updates.idle = { wakeUpOptions: opt };
1173
1253
  this.trigger('render:idle', opt);
1174
1254
  }
1175
1255
  }
@@ -1189,6 +1269,7 @@ export const Paper = View.extend({
1189
1269
  },
1190
1270
 
1191
1271
  notifyBeforeRender: function(opt = {}) {
1272
+ if (opt.silent) return;
1192
1273
  let beforeFn = opt.beforeRender;
1193
1274
  if (typeof beforeFn !== 'function') {
1194
1275
  beforeFn = this.options.beforeRender;
@@ -1198,6 +1279,7 @@ export const Paper = View.extend({
1198
1279
  },
1199
1280
 
1200
1281
  notifyAfterRender: function(stats, opt = {}) {
1282
+ if (opt.silent) return;
1201
1283
  let afterFn = opt.afterRender;
1202
1284
  if (typeof afterFn !== 'function') {
1203
1285
  afterFn = this.options.afterRender;
@@ -1208,6 +1290,56 @@ export const Paper = View.extend({
1208
1290
  this.trigger('render:done', stats, opt);
1209
1291
  },
1210
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
+
1211
1343
  updateViewsBatch: function(opt) {
1212
1344
  opt || (opt = {});
1213
1345
  var batchSize = opt.batchSize || UPDATE_BATCH_SIZE;
@@ -1220,8 +1352,7 @@ export const Paper = View.extend({
1220
1352
  var empty = true;
1221
1353
  var options = this.options;
1222
1354
  var priorities = updates.priorities;
1223
- var viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
1224
- if (typeof viewportFn !== 'function') viewportFn = null;
1355
+ const visibilityCb = this._getCellVisibilityCallback(opt);
1225
1356
  var postponeViewFn = options.onViewPostponed;
1226
1357
  if (typeof postponeViewFn !== 'function') postponeViewFn = null;
1227
1358
  var priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
@@ -1233,33 +1364,56 @@ export const Paper = View.extend({
1233
1364
  empty = false;
1234
1365
  break main;
1235
1366
  }
1236
- var view = views[cid];
1367
+ var view = viewsRegistry[cid];
1237
1368
  if (!view) {
1238
- // This should not occur
1239
- delete priorityUpdates[cid];
1240
- 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
+ }
1241
1380
  }
1242
1381
  var currentFlag = priorityUpdates[cid];
1243
- if ((currentFlag & view.FLAG_REMOVE) === 0) {
1382
+ if ((currentFlag & this.FLAG_REMOVE) === 0) {
1244
1383
  // 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)) {
1384
+ const isMounted = !updates.unmountedList.has(cid);
1385
+ if (!this._evalCellVisibility(view, isMounted, visibilityCb)) {
1247
1386
  // Unmount View
1248
- if (!isDetached) {
1387
+ if (isMounted) {
1388
+ // The view is currently mounted. Hide the view (detach or remove it).
1249
1389
  this.registerUnmountedView(view);
1250
- 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);
1251
1395
  }
1252
- updates.unmounted[cid] |= currentFlag;
1396
+ // Delete the current update as it has been processed.
1253
1397
  delete priorityUpdates[cid];
1254
1398
  unmountCount++;
1255
1399
  continue;
1256
1400
  }
1257
1401
  // Mount View
1258
- if (isDetached) {
1259
- 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;
1260
1410
  mountCount++;
1261
1411
  }
1262
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;
1263
1417
  }
1264
1418
  var leftoverFlag = this.updateView(view, currentFlag, opt);
1265
1419
  if (leftoverFlag > 0) {
@@ -1286,104 +1440,125 @@ export const Paper = View.extend({
1286
1440
  };
1287
1441
  },
1288
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
+ */
1289
1452
  getUnmountedViews: function() {
1290
1453
  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]];
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];
1296
1461
  }
1297
1462
  return unmountedViews;
1298
1463
  },
1299
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
+ */
1300
1470
  getMountedViews: function() {
1301
1471
  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]];
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];
1307
1477
  }
1308
1478
  return mountedViews;
1309
1479
  },
1310
1480
 
1311
- checkUnmountedViews: function(viewportFn, opt) {
1481
+ checkUnmountedViews: function(visibilityCb, opt) {
1312
1482
  opt || (opt = {});
1313
1483
  var mountCount = 0;
1314
- if (typeof viewportFn !== 'function') viewportFn = null;
1484
+ if (typeof visibilityCb !== 'function') visibilityCb = null;
1315
1485
  var batchSize = 'mountBatchSize' in opt ? opt.mountBatchSize : Infinity;
1316
1486
  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)) {
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)) {
1325
1496
  // Push at the end of all unmounted ids, so this can be check later again
1326
- unmountedCids.push(cid);
1497
+ unmountedList.rotate();
1327
1498
  continue;
1328
1499
  }
1500
+ // Remove the view from the unmounted list
1501
+ const { value: prevFlag } = unmountedList.popHead();
1329
1502
  mountCount++;
1330
- var flag = this.registerMountedView(view);
1503
+ const flag = this.registerMountedView(view) | prevFlag;
1331
1504
  if (flag) this.scheduleViewUpdate(view, flag, view.UPDATE_PRIORITY, { mounting: true });
1332
1505
  }
1333
- // Get rid of views, that have been mounted
1334
- unmountedCids.splice(0, i);
1335
1506
  return mountCount;
1336
1507
  },
1337
1508
 
1338
- checkMountedViews: function(viewportFn, opt) {
1509
+ checkMountedViews: function(visibilityCb, opt) {
1339
1510
  opt || (opt = {});
1340
1511
  var unmountCount = 0;
1341
- if (typeof viewportFn !== 'function') return unmountCount;
1512
+ if (typeof visibilityCb !== 'function') return unmountCount;
1342
1513
  var batchSize = 'unmountBatchSize' in opt ? opt.unmountBatchSize : Infinity;
1343
1514
  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)) {
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)) {
1352
1526
  // Push at the end of all mounted ids, so this can be check later again
1353
- mountedCids.push(cid);
1527
+ mountedList.rotate();
1354
1528
  continue;
1355
1529
  }
1530
+ // Remove the view from the mounted list
1531
+ mountedList.popHead();
1356
1532
  unmountCount++;
1357
1533
  var flag = this.registerUnmountedView(view);
1358
- if (flag) this.detachView(view);
1534
+ if (flag) {
1535
+ this._hideView(view);
1536
+ }
1359
1537
  }
1360
- // Get rid of views, that have been unmounted
1361
- mountedCids.splice(0, i);
1362
1538
  return unmountCount;
1363
1539
  },
1364
1540
 
1365
1541
  checkViewVisibility: function(cellView, opt = {}) {
1366
- let viewportFn = 'viewport' in opt ? opt.viewport : this.options.viewport;
1367
- if (typeof viewportFn !== 'function') viewportFn = null;
1542
+ const visibilityCb = this._getCellVisibilityCallback(opt);
1368
1543
  const updates = this._updates;
1369
- const { mounted, unmounted } = updates;
1370
- 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);
1371
1547
 
1372
1548
  let isUnmounted = false;
1373
1549
  let isMounted = false;
1374
1550
 
1375
- if (cellView.cid in mounted && !visible) {
1551
+ if (mountedList.has(cellView.cid) && !visible) {
1376
1552
  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);
1553
+ if (flag) this._hideView(cellView);
1554
+ mountedList.delete(cellView.cid);
1380
1555
  isUnmounted = true;
1381
1556
  }
1382
1557
 
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);
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);
1387
1562
  if (flag) this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, { mounting: true });
1388
1563
  isMounted = true;
1389
1564
  }
@@ -1394,25 +1569,65 @@ export const Paper = View.extend({
1394
1569
  };
1395
1570
  },
1396
1571
 
1397
- checkViewport: function(opt) {
1398
- 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, {
1399
1606
  mountBatchSize: Infinity,
1400
1607
  unmountBatchSize: Infinity
1401
1608
  });
1402
- var viewportFn = 'viewport' in passingOpt ? passingOpt.viewport : this.options.viewport;
1403
- var unmountedCount = this.checkMountedViews(viewportFn, passingOpt);
1609
+ const visibilityCb = this._getCellVisibilityCallback(passingOpt);
1610
+ const unmountedCount = this.checkMountedViews(visibilityCb, passingOpt);
1404
1611
  if (unmountedCount > 0) {
1405
1612
  // 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);
1613
+ var unmountedList = this._updates.unmountedList;
1614
+ passingOpt.mountBatchSize = Math.min(unmountedList.length - unmountedCount, passingOpt.mountBatchSize);
1408
1615
  }
1409
- var mountedCount = this.checkUnmountedViews(viewportFn, passingOpt);
1616
+ const mountedCount = this.checkUnmountedViews(visibilityCb, passingOpt);
1410
1617
  return {
1411
1618
  mounted: mountedCount,
1412
1619
  unmounted: unmountedCount
1413
1620
  };
1414
1621
  },
1415
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
+
1416
1631
  freeze: function(opt) {
1417
1632
  opt || (opt = {});
1418
1633
  var updates = this._updates;
@@ -1428,6 +1643,10 @@ export const Paper = View.extend({
1428
1643
  this.options.frozen = true;
1429
1644
  var id = updates.id;
1430
1645
  updates.id = null;
1646
+ if (!this.legacyMode) {
1647
+ // Make sure the `freeze()` method ends the idle state.
1648
+ updates.idle = false;
1649
+ }
1431
1650
  if (this.isAsync() && id) cancelFrame(id);
1432
1651
  },
1433
1652
 
@@ -1441,6 +1660,7 @@ export const Paper = View.extend({
1441
1660
  updates.freezeKey = null;
1442
1661
  // key passed, but the paper is already freezed
1443
1662
  if (key && key === freezeKey && updates.keyFrozen) return;
1663
+ updates.idle = false;
1444
1664
  if (this.isAsync()) {
1445
1665
  this.freeze();
1446
1666
  this.updateViewsAsync(opt);
@@ -1454,12 +1674,25 @@ export const Paper = View.extend({
1454
1674
  }
1455
1675
  },
1456
1676
 
1677
+ wakeUp: function() {
1678
+ if (!this.isIdle()) return;
1679
+ this.unfreeze(this._updates.idle.wakeUpOptions);
1680
+ },
1681
+
1457
1682
  isAsync: function() {
1458
1683
  return !!this.options.async;
1459
1684
  },
1460
1685
 
1461
1686
  isFrozen: function() {
1462
- 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);
1463
1696
  },
1464
1697
 
1465
1698
  isExactSorting: function() {
@@ -1759,21 +1992,57 @@ export const Paper = View.extend({
1759
1992
  return restrictedArea;
1760
1993
  },
1761
1994
 
1762
- 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
+ },
1763
2002
 
1764
- const { options } = this;
1765
- // A class taken from the paper options.
1766
- 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
+ },
1767
2017
 
1768
- // A default basic class (either dia.ElementView or dia.LinkView)
1769
- var defaultViewClass;
2018
+ _registerCellView: function(cellView) {
2019
+ cellView.paper = this;
2020
+ this._views[cellView.model.id] = cellView;
2021
+ },
1770
2022
 
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, '.');
2023
+ _unregisterCellViewPlaceholder: function(placeholder) {
2024
+ delete this._viewPlaceholders[placeholder.cid];
2025
+ },
1776
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;
1777
2046
  if (cell.isLink()) {
1778
2047
  optionalViewClass = options.linkView;
1779
2048
  defaultViewClass = LinkView;
@@ -1781,7 +2050,6 @@ export const Paper = View.extend({
1781
2050
  optionalViewClass = options.elementView;
1782
2051
  defaultViewClass = ElementView;
1783
2052
  }
1784
-
1785
2053
  // a) the paper options view is a class (deprecated)
1786
2054
  // 1. search the namespace for a view
1787
2055
  // 2. if no view was found, use view from the paper options
@@ -1789,29 +2057,54 @@ export const Paper = View.extend({
1789
2057
  // 1. call the function from the paper options
1790
2058
  // 2. if no view was return, search the namespace for a view
1791
2059
  // 3. if no view was found, use the default
1792
- var ViewClass = (optionalViewClass.prototype instanceof ViewBase)
2060
+ return (optionalViewClass.prototype instanceof ViewBase)
1793
2061
  ? namespaceViewClass || optionalViewClass
1794
2062
  : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass;
2063
+ },
1795
2064
 
1796
- return new ViewClass({
1797
- model: cell,
1798
- interactive: options.interactive,
1799
- labelsLayer: options.labelsLayer === true ? LayersNames.LABELS : options.labelsLayer
1800
- });
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;
1801
2090
  },
1802
2091
 
1803
- removeView: function(cell) {
2092
+ createViewForModel: function(cell, cid) {
2093
+ return this._initializeCellView(this._resolveCellViewClass(cell), cell, cid);
2094
+ },
1804
2095
 
2096
+ removeView: function(cell) {
1805
2097
  const { id } = cell;
1806
2098
  const { _views, _updates } = this;
1807
2099
  const view = _views[id];
1808
2100
  if (view) {
1809
2101
  var { cid } = view;
1810
- const { mounted, unmounted } = _updates;
2102
+ const { mountedList, unmountedList } = _updates;
1811
2103
  view.remove();
1812
2104
  delete _views[id];
1813
- delete mounted[cid];
1814
- delete unmounted[cid];
2105
+ delete this._idToCid[id];
2106
+ mountedList.delete(cid);
2107
+ unmountedList.delete(cid);
1815
2108
  }
1816
2109
  return view;
1817
2110
  },
@@ -1825,7 +2118,7 @@ export const Paper = View.extend({
1825
2118
  if (id in views) {
1826
2119
  view = views[id];
1827
2120
  if (view.model === cell) {
1828
- flag = view.FLAG_INSERT;
2121
+ flag = this.FLAG_INSERT;
1829
2122
  create = false;
1830
2123
  } else {
1831
2124
  // The view for this `id` already exist.
@@ -1835,14 +2128,42 @@ export const Paper = View.extend({
1835
2128
  }
1836
2129
  }
1837
2130
  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'));
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
+ }
1841
2151
  }
2152
+
1842
2153
  this.requestViewUpdate(view, flag, view.UPDATE_PRIORITY, opt);
2154
+
1843
2155
  return view;
1844
2156
  },
1845
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
+
1846
2167
  onImageDragStart: function() {
1847
2168
  // This is the only way to prevent image dragging in Firefox that works.
1848
2169
  // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help.
@@ -1853,11 +2174,11 @@ export const Paper = View.extend({
1853
2174
  resetViews: function(cells, opt) {
1854
2175
  opt || (opt = {});
1855
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';
1856
2179
  this._resetUpdates();
1857
2180
  // clearing views removes any event listeners
1858
2181
  this.removeViews();
1859
- // Allows to unfreeze normally while in the idle state using autoFreeze option
1860
- const key = this.options.autoFreeze ? null : 'reset';
1861
2182
  this.freeze({ key });
1862
2183
  for (var i = 0, n = cells.length; i < n; i++) {
1863
2184
  this.renderView(cells[i], opt);
@@ -1867,10 +2188,16 @@ export const Paper = View.extend({
1867
2188
  },
1868
2189
 
1869
2190
  removeViews: function() {
1870
-
1871
- invoke(this._views, 'remove');
1872
-
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
+ }
1873
2198
  this._views = {};
2199
+ this._viewPlaceholders = {};
2200
+ this._idToCid = {};
1874
2201
  },
1875
2202
 
1876
2203
  sortViews: function() {
@@ -1879,7 +2206,7 @@ export const Paper = View.extend({
1879
2206
  // noop
1880
2207
  return;
1881
2208
  }
1882
- if (this.isFrozen()) {
2209
+ if (this.isFrozen() || this.isIdle()) {
1883
2210
  // sort views once unfrozen
1884
2211
  this._updates.sort = true;
1885
2212
  return;
@@ -1922,9 +2249,58 @@ export const Paper = View.extend({
1922
2249
  view.onMount(isInitialInsert);
1923
2250
  },
1924
2251
 
1925
- detachView(view) {
1926
- view.unmount();
1927
- 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();
1928
2304
  },
1929
2305
 
1930
2306
  // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also
@@ -1942,11 +2318,32 @@ export const Paper = View.extend({
1942
2318
  },
1943
2319
 
1944
2320
  // 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];
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;
1950
2347
  },
1951
2348
 
1952
2349
  // Find all views at given point
@@ -2025,6 +2422,92 @@ export const Paper = View.extend({
2025
2422
  );
2026
2423
  },
2027
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
+
2028
2511
  _findInExtendedArea: function(area, findCellsFn, opt = {}) {
2029
2512
  const {
2030
2513
  buffer = this.DEFAULT_FIND_BUFFER,
@@ -2477,8 +2960,11 @@ export const Paper = View.extend({
2477
2960
 
2478
2961
  var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
2479
2962
 
2480
- var view = data.sourceView;
2963
+ let view = data.sourceView;
2481
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);
2482
2968
  view.pointermove(evt, localPoint.x, localPoint.y);
2483
2969
  } else {
2484
2970
  this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y);
@@ -2495,8 +2981,11 @@ export const Paper = View.extend({
2495
2981
 
2496
2982
  var localPoint = this.snapToGrid(normalizedEvt.clientX, normalizedEvt.clientY);
2497
2983
 
2498
- var view = this.eventData(evt).sourceView;
2984
+ let view = this.eventData(evt).sourceView;
2499
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);
2500
2989
  view.pointerup(normalizedEvt, localPoint.x, localPoint.y);
2501
2990
  } else {
2502
2991
  this.trigger('blank:pointerup', normalizedEvt, localPoint.x, localPoint.y);
@@ -3366,4 +3855,3 @@ export const Paper = View.extend({
3366
3855
  }]
3367
3856
  }
3368
3857
  });
3369
-