@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/dist/joint.js CHANGED
@@ -1,10 +1,10 @@
1
- /*! JointJS v4.2.0-alpha.0 (2025-06-16) - JavaScript diagramming library
2
-
1
+ /*! JointJS v4.2.0-alpha.1 (2025-09-25) - JavaScript diagramming library
3
2
 
4
3
  This Source Code Form is subject to the terms of the Mozilla Public
5
4
  License, v. 2.0. If a copy of the MPL was not distributed with this
6
5
  file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
6
  */
7
+
8
8
  (function (global, factory) {
9
9
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
10
10
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
@@ -9862,8 +9862,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
9862
9862
  });
9863
9863
 
9864
9864
  /**
9865
- * @param {SVGGElement} toElem
9866
- * @returns {SVGMatrix}
9865
+ * Calculates the transformation matrix from this element to the target element.
9866
+ * @param {SVGElement|V} target - The target element.
9867
+ * @param {Object} [opt] - Options object for transformation calculation.
9868
+ * @param {boolean} [opt.safe] - Use a safe traversal method to compute the matrix.
9869
+ * @returns {DOMMatrix} The transformation matrix from this element to the target element.
9867
9870
  */
9868
9871
  VPrototype.getTransformToElement = function (target, opt) {
9869
9872
  const node = this.node;
@@ -10196,11 +10199,18 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
10196
10199
  }
10197
10200
  VPrototype.text = function (content, opt) {
10198
10201
  if (content && typeof content !== 'string') throw new Error('Vectorizer: text() expects the first argument to be a string.');
10199
-
10200
- // Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm).
10201
- // IE would otherwise collapse all spaces into one.
10202
- content = V.sanitizeText(content);
10203
10202
  opt || (opt = {});
10203
+
10204
+ // Backwards-compatibility: if no content was provided, treat it as an
10205
+ // empty string so that subsequent string operations (e.g. split) do
10206
+ // not throw and behaviour matches the previous implementation that
10207
+ // always sanitised the input.
10208
+ if (content == null) content = '';
10209
+ if (opt.useNoBreakSpace) {
10210
+ // Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm).
10211
+ // IE would otherwise collapse all spaces into one.
10212
+ content = V.sanitizeText(content);
10213
+ }
10204
10214
  // Should we allow the text to be selected?
10205
10215
  var displayEmpty = opt.displayEmpty;
10206
10216
  // End of Line character
@@ -10943,6 +10953,9 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
10943
10953
  // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests
10944
10954
  // when you want to compare the actual DOM text content without having to add the unicode character in
10945
10955
  // the place of all spaces.
10956
+ /**
10957
+ * @deprecated Use regular spaces and rely on xml:space="preserve" instead.
10958
+ */
10946
10959
  V.sanitizeText = function (text) {
10947
10960
  return (text || '').replace(/ /g, '\u00A0');
10948
10961
  };
@@ -12490,6 +12503,14 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
12490
12503
  });
12491
12504
 
12492
12505
  const config$3 = {
12506
+ // How the cell attributes are merged when `cell.prop()` is called.
12507
+ // DEFAULT: the arrays are merged into the source array.
12508
+ cellMergeStrategy: null,
12509
+ // How the cell default attributes are merged with the attributes provided
12510
+ // in the cell constructor.
12511
+ // DEFAULT: the arrays are merged by replacing the source array
12512
+ // with the destination array.
12513
+ cellDefaultsMergeStrategy: null,
12493
12514
  // When set to `true` the cell selectors could be defined as CSS selectors.
12494
12515
  // If not, only JSON Markup selectors are taken into account.
12495
12516
  useCSSSelectors: false,
@@ -15034,9 +15055,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
15034
15055
  const eol = attrs.eol;
15035
15056
  const x = attrs.x;
15036
15057
  let textPath = attrs['text-path'];
15058
+ const useNoBreakSpace = attrs['use-no-break-space'] === true;
15037
15059
  // Update the text only if there was a change in the string
15038
15060
  // or any of its attributes.
15039
- const textHash = JSON.stringify([text, lineHeight, annotations, textVerticalAnchor, eol, displayEmpty, textPath, x, fontSize]);
15061
+ const textHash = JSON.stringify([text, lineHeight, annotations, textVerticalAnchor, eol, displayEmpty, textPath, x, fontSize, useNoBreakSpace]);
15040
15062
  if (cache === undefined || cache !== textHash) {
15041
15063
  // Chrome bug:
15042
15064
  // <tspan> positions defined as `em` are not updated
@@ -15061,7 +15083,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
15061
15083
  x,
15062
15084
  textVerticalAnchor,
15063
15085
  eol,
15064
- displayEmpty
15086
+ displayEmpty,
15087
+ useNoBreakSpace
15065
15088
  });
15066
15089
  $.data.set(node, cacheName, textHash);
15067
15090
  }
@@ -15577,12 +15600,18 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
15577
15600
  if (defaults = result(this, 'defaults')) {
15578
15601
  //<custom code>
15579
15602
  // Replaced the call to _.defaults with util.merge.
15580
- const customizer = options && options.mergeArrays === true ? false : attributesMerger;
15603
+ const customizer = options && options.mergeArrays === true ? false : config$3.cellDefaultsMergeStrategy || attributesMerger;
15581
15604
  attrs = merge({}, defaults, attrs, customizer);
15582
15605
  //</custom code>
15583
15606
  }
15584
15607
  this.set(attrs, options);
15585
15608
  this.changed = {};
15609
+ if (options && options.portLayoutNamespace) {
15610
+ this.portLayoutNamespace = options.portLayoutNamespace;
15611
+ }
15612
+ if (options && options.portLabelLayoutNamespace) {
15613
+ this.portLabelLayoutNamespace = options.portLabelLayoutNamespace;
15614
+ }
15586
15615
  this.initialize.apply(this, arguments);
15587
15616
  },
15588
15617
  translate: function (dx, dy, opt) {
@@ -15976,7 +16005,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
15976
16005
  if (!opt.deep) {
15977
16006
  // Shallow cloning.
15978
16007
 
15979
- var clone = Model.prototype.clone.apply(this, arguments);
16008
+ // Preserve the original's `portLayoutNamespace` and `portLabelLayoutNamespace`.
16009
+ const clone = new this.constructor(this.attributes, {
16010
+ portLayoutNamespace: this.portLayoutNamespace,
16011
+ portLabelLayoutNamespace: this.portLabelLayoutNamespace
16012
+ });
15980
16013
  // We don't want the clone to have the same ID as the original.
15981
16014
  clone.set(this.getIdAttribute(), this.generateId());
15982
16015
  // A shallow cloned element does not carry over the original embeds.
@@ -15988,7 +16021,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
15988
16021
  } else {
15989
16022
  // Deep cloning.
15990
16023
 
15991
- // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells.
16024
+ // For a deep clone, simply call `util.cloneCells()` with the cell and all its embedded cells.
15992
16025
  return toArray$1(cloneCells([this].concat(this.getEmbeddedCells({
15993
16026
  deep: true
15994
16027
  }))));
@@ -16053,7 +16086,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
16053
16086
  options.rewrite && unsetByPath(baseAttributes, path, '/');
16054
16087
 
16055
16088
  // Merge update with the model attributes.
16056
- var attributes = merge(baseAttributes, update);
16089
+ var attributes = merge(baseAttributes, update, config$3.cellMergeStrategy);
16057
16090
  // Finally, set the property to the updated attributes.
16058
16091
  return this.set(property, attributes[property], options);
16059
16092
  } else {
@@ -16075,11 +16108,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
16075
16108
  // Merging the values of changed attributes with the current ones.
16076
16109
  const {
16077
16110
  changedValue
16078
- } = merge({}, {
16111
+ } = merge(merge({}, {
16079
16112
  changedValue: this.attributes[key]
16080
- }, {
16113
+ }), {
16081
16114
  changedValue: props[key]
16082
- });
16115
+ }, config$3.cellMergeStrategy);
16083
16116
  changedAttributes[key] = changedValue;
16084
16117
  }
16085
16118
  return this.set(changedAttributes, options);
@@ -16313,9 +16346,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
16313
16346
  }
16314
16347
  }, {
16315
16348
  getAttributeDefinition: function (attrName) {
16316
- var defNS = this.attributes;
16317
- var globalDefNS = attributes;
16318
- return defNS && defNS[attrName] || globalDefNS[attrName];
16349
+ const defNS = this.attributes;
16350
+ const globalDefNS = attributes;
16351
+ const definition = defNS && defNS[attrName] || globalDefNS[attrName];
16352
+ return definition !== undefined ? definition : null;
16319
16353
  },
16320
16354
  define: function (type, defaults, protoProps, staticProps) {
16321
16355
  protoProps = assign({
@@ -16621,6 +16655,22 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
16621
16655
  wrappers: wrappers
16622
16656
  };
16623
16657
 
16658
+ function parseCoordinate(coordinate, dimension, bbox, value) {
16659
+ if (isPercentage(value)) {
16660
+ return parseFloat(value) / 100 * bbox[dimension];
16661
+ }
16662
+ if (isCalcExpression(value)) {
16663
+ return Number(evalCalcExpression(value, bbox));
16664
+ }
16665
+ if (typeof value === 'string') {
16666
+ const num = Number(value);
16667
+ if (isNaN(num)) {
16668
+ throw new TypeError(`Cannot convert port coordinate ${coordinate}: "${value}" to a number`);
16669
+ }
16670
+ return num;
16671
+ }
16672
+ return value;
16673
+ }
16624
16674
  function portTransformAttrs(point, angle, opt) {
16625
16675
  var trans = point.toJSON();
16626
16676
  trans.angle = angle || 0;
@@ -16664,19 +16714,9 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
16664
16714
  y,
16665
16715
  angle
16666
16716
  } = args;
16667
- if (isPercentage(x)) {
16668
- x = parseFloat(x) / 100 * bbox.width;
16669
- } else if (isCalcExpression(x)) {
16670
- x = Number(evalCalcExpression(x, bbox));
16671
- }
16672
- if (isPercentage(y)) {
16673
- y = parseFloat(y) / 100 * bbox.height;
16674
- } else if (isCalcExpression(y)) {
16675
- y = Number(evalCalcExpression(y, bbox));
16676
- }
16677
16717
  return {
16678
- x,
16679
- y,
16718
+ x: parseCoordinate('x', 'width', bbox, x),
16719
+ y: parseCoordinate('y', 'height', bbox, y),
16680
16720
  angle
16681
16721
  };
16682
16722
  }
@@ -16696,7 +16736,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
16696
16736
  * @param {Object=} opt opt Group options
16697
16737
  * @returns {Array<g.Point>}
16698
16738
  */
16699
- const absolute = function (ports, elBBox) {
16739
+ const absolute = function (ports, elBBox, opt) {
16700
16740
  return ports.map(port => {
16701
16741
  const transformation = argPoint(elBBox, port).round().toJSON();
16702
16742
  transformation.angle = port.angle || 0;
@@ -16705,6 +16745,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
16705
16745
  };
16706
16746
 
16707
16747
  /**
16748
+ * @deprecated
16708
16749
  * @param {Array<Object>} ports
16709
16750
  * @param {g.Rect} elBBox
16710
16751
  * @param {Object=} opt opt Group options
@@ -16969,7 +17010,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
16969
17010
  }
16970
17011
  });
16971
17012
  }
16972
- const manual = function (_portPosition, _elBBox, opt) {
17013
+ const manual = function (portPosition, elBBox, opt) {
16973
17014
  return labelAttributes(opt);
16974
17015
  };
16975
17016
  const left$1 = function (portPosition, elBBox, opt) {
@@ -17050,13 +17091,20 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17050
17091
  top: top$1
17051
17092
  };
17052
17093
 
17053
- var PortData = function (data) {
17054
- var clonedData = cloneDeep(data) || {};
17094
+ const DEFAULT_PORT_POSITION_NAME = 'left';
17095
+ const DEFAULT_ABSOLUTE_PORT_POSITION_NAME = 'absolute';
17096
+ const DEFAULT_PORT_LABEL_POSITION_NAME = 'left';
17097
+ const PortData = function (model) {
17098
+ const {
17099
+ portLayoutNamespace = Port,
17100
+ portLabelLayoutNamespace = PortLabel
17101
+ } = model;
17102
+ const clonedData = cloneDeep(model.get('ports')) || {};
17055
17103
  this.ports = [];
17056
17104
  this.portsMap = {};
17057
17105
  this.groups = {};
17058
- this.portLayoutNamespace = Port;
17059
- this.portLabelLayoutNamespace = PortLabel;
17106
+ this.portLayoutNamespace = portLayoutNamespace;
17107
+ this.portLabelLayoutNamespace = portLabelLayoutNamespace;
17060
17108
  this.metrics = {};
17061
17109
  this.metricsKey = null;
17062
17110
  this._init(clonedData);
@@ -17081,6 +17129,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17081
17129
  return port.group === groupName;
17082
17130
  });
17083
17131
  },
17132
+ // Calculate SVG transformations based on evaluated group + port data
17133
+ // NOTE: This function is also called for ports without a group (groupName = undefined)
17084
17134
  getGroupPortsMetrics: function (groupName, rect) {
17085
17135
  const {
17086
17136
  x = 0,
@@ -17099,29 +17149,36 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17099
17149
  // Return cached metrics
17100
17150
  return groupPortsMetrics;
17101
17151
  }
17152
+
17102
17153
  // Calculate the metrics
17103
17154
  groupPortsMetrics = this.resolveGroupPortsMetrics(groupName, new Rect(x, y, width, height));
17104
17155
  this.metrics[groupName] = groupPortsMetrics;
17105
17156
  return groupPortsMetrics;
17106
17157
  },
17107
17158
  resolveGroupPortsMetrics: function (groupName, elBBox) {
17108
- var group = this.getGroup(groupName);
17109
- var ports = this.getPortsByGroup(groupName);
17110
- var groupPosition = group.position || {};
17111
- var groupPositionName = groupPosition.name;
17112
- var namespace = this.portLayoutNamespace;
17113
- if (!namespace[groupPositionName]) {
17114
- groupPositionName = 'left';
17115
- }
17116
- var groupArgs = groupPosition.args || {};
17117
- var portsArgs = ports.map(function (port) {
17159
+ // `groupName` of `undefined` (= not a string) means "the group of ports which do not have the `group` property".
17160
+ const isNoGroup = groupName === undefined;
17161
+ const group = this.getGroup(groupName);
17162
+ const ports = this.getPortsByGroup(groupName);
17163
+ const portsArgs = ports.map(function (port) {
17118
17164
  return port && port.position && port.position.args;
17119
17165
  });
17120
- var groupPortTransformations = namespace[groupPositionName](portsArgs, elBBox, groupArgs);
17121
- var accumulator = {
17166
+
17167
+ // Get an array of transformations of individual ports according to the group's port layout function:
17168
+ let groupPortTransformations;
17169
+ if (isNoGroup) {
17170
+ // Apply default port layout function to the set of ports without `group` property.
17171
+ const noGroup = this._evaluateGroup({});
17172
+ groupPortTransformations = this._getGroupPortTransformations(noGroup, portsArgs, elBBox);
17173
+ } else {
17174
+ groupPortTransformations = this._getGroupPortTransformations(group, portsArgs, elBBox);
17175
+ }
17176
+ let accumulator = {
17122
17177
  ports: ports,
17123
17178
  result: {}
17124
17179
  };
17180
+
17181
+ // For each individual port transformation, find the information necessary to calculate SVG transformations:
17125
17182
  toArray$1(groupPortTransformations).reduce((res, portTransformation, index) => {
17126
17183
  const port = res.ports[index];
17127
17184
  const portId = port.id;
@@ -17129,7 +17186,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17129
17186
  index,
17130
17187
  portId,
17131
17188
  portTransformation: portTransformation,
17132
- labelTransformation: this._getPortLabelLayout(port, Point(portTransformation), elBBox),
17189
+ labelTransformation: this._getPortLabelTransformation(port, Point(portTransformation), elBBox),
17133
17190
  portAttrs: port.attrs,
17134
17191
  portSize: port.size,
17135
17192
  labelSize: port.label.size
@@ -17138,16 +17195,24 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17138
17195
  }, accumulator);
17139
17196
  return accumulator.result;
17140
17197
  },
17141
- _getPortLabelLayout: function (port, portPosition, elBBox) {
17142
- var namespace = this.portLabelLayoutNamespace;
17143
- var labelPosition = port.label.position.name || 'left';
17144
- if (namespace[labelPosition]) {
17145
- return namespace[labelPosition](portPosition, elBBox, port.label.position.args);
17198
+ _getGroupPortTransformations: function (group, portsArgs, elBBox) {
17199
+ const groupPosition = group.position || {};
17200
+ const groupPositionArgs = groupPosition.args || {};
17201
+ const groupPositionLayoutCallback = groupPosition.layoutCallback;
17202
+ return groupPositionLayoutCallback(portsArgs, elBBox, groupPositionArgs);
17203
+ },
17204
+ _getPortLabelTransformation: function (port, portPosition, elBBox) {
17205
+ const portLabelPosition = port.label.position || {};
17206
+ const portLabelPositionArgs = portLabelPosition.args || {};
17207
+ const portLabelPositionLayoutCallback = portLabelPosition.layoutCallback;
17208
+ if (portLabelPositionLayoutCallback) {
17209
+ return portLabelPositionLayoutCallback(portPosition, elBBox, portLabelPositionArgs);
17146
17210
  }
17147
17211
  return null;
17148
17212
  },
17149
17213
  _init: function (data) {
17150
- // prepare groups
17214
+ // Prepare groups:
17215
+ // NOTE: This overwrites passed group properties with evaluated group properties.
17151
17216
  if (isObject(data.groups)) {
17152
17217
  var groups = Object.keys(data.groups);
17153
17218
  for (var i = 0, n = groups.length; i < n; i++) {
@@ -17156,7 +17221,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17156
17221
  }
17157
17222
  }
17158
17223
 
17159
- // prepare ports
17224
+ // Prepare ports:
17225
+ // NOTE: This overwrites passed port properties with evaluated port properties, plus mixed-in evaluated group properties (see above).
17160
17226
  var ports = toArray$1(data.items);
17161
17227
  for (var j = 0, m = ports.length; j < m; j++) {
17162
17228
  const resolvedPort = this._evaluatePort(ports[j]);
@@ -17165,23 +17231,155 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17165
17231
  }
17166
17232
  },
17167
17233
  _evaluateGroup: function (group) {
17168
- return merge(group, {
17169
- position: this._getPosition(group.position, true),
17170
- label: this._getLabel(group, true)
17234
+ return merge({}, group, {
17235
+ position: this._evaluateGroupPositionProperty(group),
17236
+ label: this._evaluateGroupLabelProperty(group)
17237
+ });
17238
+ },
17239
+ _evaluateGroupPositionProperty: function (group) {
17240
+ const namespace = this.portLayoutNamespace;
17241
+ const groupPosition = group.position;
17242
+ if (groupPosition === undefined) {
17243
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, DEFAULT_PORT_POSITION_NAME, 'Default port group');
17244
+ return {
17245
+ layoutCallback
17246
+ };
17247
+ } else if (isFunction(groupPosition)) {
17248
+ return {
17249
+ layoutCallback: groupPosition
17250
+ };
17251
+ } else if (isObject(groupPosition)) {
17252
+ if (groupPosition.name) {
17253
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, groupPosition.name, 'Provided port group');
17254
+ return {
17255
+ layoutCallback,
17256
+ args: groupPosition.args
17257
+ };
17258
+ } else {
17259
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, DEFAULT_PORT_POSITION_NAME, 'Default port group');
17260
+ return {
17261
+ layoutCallback,
17262
+ args: groupPosition.args
17263
+ };
17264
+ }
17265
+ } else if (isString(groupPosition)) {
17266
+ // TODO: Remove legacy signature (see `this._evaluateGroupLabelPositionProperty()`).
17267
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, groupPosition, 'Provided port group');
17268
+ return {
17269
+ layoutCallback
17270
+ };
17271
+ } else if (Array.isArray(groupPosition)) {
17272
+ // TODO: Remove legacy signature (see `this._evaluateGroupLabelPositionProperty()`).
17273
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, DEFAULT_ABSOLUTE_PORT_POSITION_NAME, 'Default absolute port group');
17274
+ return {
17275
+ layoutCallback,
17276
+ args: {
17277
+ x: groupPosition[0],
17278
+ y: groupPosition[1]
17279
+ }
17280
+ };
17281
+ } else {
17282
+ throw new Error('dia.Element: Provided port group position value has an invalid type.');
17283
+ }
17284
+ },
17285
+ _evaluateGroupLabelProperty: function (group) {
17286
+ const groupLabel = group.label;
17287
+ if (!groupLabel) {
17288
+ return {
17289
+ position: this._evaluateGroupLabelPositionProperty({})
17290
+ };
17291
+ }
17292
+ return merge({}, groupLabel, {
17293
+ position: this._evaluateGroupLabelPositionProperty(groupLabel)
17171
17294
  });
17172
17295
  },
17296
+ _evaluateGroupLabelPositionProperty: function (groupLabel) {
17297
+ const namespace = this.portLabelLayoutNamespace;
17298
+ const groupLabelPosition = groupLabel.position;
17299
+ if (groupLabelPosition === undefined) {
17300
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, DEFAULT_PORT_LABEL_POSITION_NAME, 'Default port group label');
17301
+ return {
17302
+ layoutCallback
17303
+ };
17304
+ } else if (isFunction(groupLabelPosition)) {
17305
+ return {
17306
+ layoutCallback: groupLabelPosition
17307
+ };
17308
+ } else if (isObject(groupLabelPosition)) {
17309
+ if (groupLabelPosition.name) {
17310
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, groupLabelPosition.name, 'Provided port group label');
17311
+ return {
17312
+ layoutCallback,
17313
+ args: groupLabelPosition.args
17314
+ };
17315
+ } else {
17316
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, DEFAULT_PORT_LABEL_POSITION_NAME, 'Default port group label');
17317
+ return {
17318
+ layoutCallback,
17319
+ args: groupLabelPosition.args
17320
+ };
17321
+ }
17322
+ } else {
17323
+ throw new Error('dia.Element: Provided port group label position value has an invalid type.');
17324
+ }
17325
+ },
17173
17326
  _evaluatePort: function (port) {
17174
- var evaluated = assign({}, port);
17175
- var group = this.getGroup(port.group);
17327
+ const group = this.getGroup(port.group);
17328
+ const evaluated = assign({}, port);
17176
17329
  evaluated.markup = evaluated.markup || group.markup;
17177
17330
  evaluated.attrs = merge({}, group.attrs, evaluated.attrs);
17178
- evaluated.position = this._createPositionNode(group, evaluated);
17179
- evaluated.label = merge({}, group.label, this._getLabel(evaluated));
17180
- evaluated.z = this._getZIndex(group, evaluated);
17331
+ evaluated.position = this._evaluatePortPositionProperty(group, evaluated);
17332
+ evaluated.label = this._evaluatePortLabelProperty(group, evaluated);
17333
+ evaluated.z = this._evaluatePortZProperty(group, evaluated);
17181
17334
  evaluated.size = assign({}, group.size, evaluated.size);
17182
17335
  return evaluated;
17183
17336
  },
17184
- _getZIndex: function (group, port) {
17337
+ _evaluatePortPositionProperty: function (group, port) {
17338
+ return {
17339
+ args: merge({},
17340
+ // NOTE: `x != null` is equivalent to `x !== null && x !== undefined`.
17341
+ group.position != null ? group.position.args : {},
17342
+ // Port can overwrite `group.position.args` via `port.position.args` or `port.args`.
17343
+ // TODO: Remove `port.args` backwards compatibility.
17344
+ port.position != null && port.position.args != null ? port.position.args : port.args)
17345
+ };
17346
+ },
17347
+ _evaluatePortLabelProperty: function (group, port) {
17348
+ const groupLabel = group.label;
17349
+ const portLabel = port.label;
17350
+ if (!portLabel) {
17351
+ return assign({}, groupLabel);
17352
+ }
17353
+ return merge({}, groupLabel, merge({}, portLabel, {
17354
+ position: this._evaluatePortLabelPositionProperty(portLabel)
17355
+ }));
17356
+ },
17357
+ _evaluatePortLabelPositionProperty: function (portLabel) {
17358
+ const namespace = this.portLabelLayoutNamespace;
17359
+ const portLabelPosition = portLabel.position;
17360
+ if (portLabelPosition === undefined) {
17361
+ return {};
17362
+ } else if (isFunction(portLabelPosition)) {
17363
+ return {
17364
+ layoutCallback: portLabelPosition
17365
+ };
17366
+ } else if (isObject(portLabelPosition)) {
17367
+ if (portLabelPosition.name) {
17368
+ const layoutCallback = this._resolveLayoutCallbackOrThrow(namespace, portLabelPosition.name, 'Provided port label');
17369
+ return {
17370
+ layoutCallback,
17371
+ args: portLabelPosition.args
17372
+ };
17373
+ } else {
17374
+ return {
17375
+ args: portLabelPosition.args
17376
+ };
17377
+ }
17378
+ } else {
17379
+ throw new Error('dia.Element: Provided port label position value has an invalid type.');
17380
+ }
17381
+ },
17382
+ _evaluatePortZProperty: function (group, port) {
17185
17383
  if (isNumber(port.z)) {
17186
17384
  return port.z;
17187
17385
  }
@@ -17190,47 +17388,12 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17190
17388
  }
17191
17389
  return 'auto';
17192
17390
  },
17193
- _createPositionNode: function (group, port) {
17194
- return merge({
17195
- name: 'left',
17196
- args: {}
17197
- }, group.position, {
17198
- // TODO: remove `port.args` backwards compatibility
17199
- // NOTE: `x != null` is equivalent to `x !== null && x !== undefined`
17200
- args: port.position != null && port.position.args != null ? port.position.args : port.args
17201
- });
17202
- },
17203
- _getPosition: function (position, setDefault) {
17204
- var args = {};
17205
- var positionName;
17206
- if (isFunction(position)) {
17207
- positionName = 'fn';
17208
- args.fn = position;
17209
- } else if (isString(position)) {
17210
- positionName = position;
17211
- } else if (position === undefined) {
17212
- positionName = setDefault ? 'left' : null;
17213
- } else if (Array.isArray(position)) {
17214
- positionName = 'absolute';
17215
- args.x = position[0];
17216
- args.y = position[1];
17217
- } else if (isObject(position)) {
17218
- positionName = position.name;
17219
- assign(args, position.args);
17220
- }
17221
- var result = {
17222
- args: args
17223
- };
17224
- if (positionName) {
17225
- result.name = positionName;
17391
+ _resolveLayoutCallbackOrThrow: function (namespace, name, errorSubstring) {
17392
+ const layoutCallback = namespace[name];
17393
+ if (!layoutCallback) {
17394
+ throw new Error(`dia.Element: ${errorSubstring} layout name is not recognized.`);
17226
17395
  }
17227
- return result;
17228
- },
17229
- _getLabel: function (item, setDefaults) {
17230
- var label = item.label || {};
17231
- var ret = label;
17232
- ret.position = this._getPosition(label.position, setDefaults);
17233
- return ret;
17396
+ return layoutCallback;
17234
17397
  }
17235
17398
  };
17236
17399
  const elementPortPrototype = {
@@ -17570,7 +17733,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
17570
17733
  if (this._portSettingsData) {
17571
17734
  prevPortData = this._portSettingsData.getPorts();
17572
17735
  }
17573
- this._portSettingsData = new PortData(this.get('ports'));
17736
+ this._portSettingsData = new PortData(this);
17574
17737
  var curPortData = this._portSettingsData.getPorts();
17575
17738
  if (prevPortData) {
17576
17739
  var added = curPortData.filter(function (item) {
@@ -18278,8 +18441,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
18278
18441
  _fitToElements: function (opt = {}) {
18279
18442
  let minBBox = null;
18280
18443
  if (opt.minRect) {
18281
- // Coerce `opt.minRect` to g.Rect (missing properties = 0).
18282
- minBBox = new Rect(opt.minRect);
18444
+ // Coerce `opt.minRect` to g.Rect
18445
+ // (missing properties are taken from this element's current bbox).
18446
+ const minRect = assign(this.getBBox(), opt.minRect);
18447
+ minBBox = new Rect(minRect);
18283
18448
  }
18284
18449
  const elementsBBox = this.graph.getCellsBBox(opt.elements);
18285
18450
  // If no `opt.elements` were provided, do nothing (but if `opt.minRect` was provided, set that as this element's bbox instead).
@@ -18949,9 +19114,12 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
18949
19114
  svgforeignobject: function () {
18950
19115
  return !!document.createElementNS && /SVGForeignObject/.test({}.toString.call(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')));
18951
19116
  },
18952
- // works for iOS browsers too
18953
- isSafari: function () {
18954
- return /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor);
19117
+ // works for: (1) macOS Safari, (2) any WKWebView, (3) any iOS browser (including Safari, CriOS, EdgiOS, OPR, FxiOS)
19118
+ isAppleWebKit: function () {
19119
+ const userAgent = navigator.userAgent;
19120
+ const isAppleWebKit = /applewebkit/i.test(userAgent);
19121
+ const isChromium = /chrome/i.test(userAgent); // e.g. Chrome, Edge, Opera, SamsungBrowser
19122
+ return isAppleWebKit && !isChromium;
18955
19123
  }
18956
19124
  },
18957
19125
  addTest: function (name, fn) {
@@ -20825,7 +20993,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
20825
20993
  // Creating a ViewBase creates its initial element outside of the DOM,
20826
20994
  // if an existing element is not provided...
20827
20995
  var ViewBase = function (options) {
20828
- this.cid = uniqueId('view');
20996
+ this.cid = options && options.cid || uniqueId('view');
20829
20997
  this.preinitialize.apply(this, arguments);
20830
20998
  assign(this, pick(options, viewOptions));
20831
20999
  this._ensureElement();
@@ -20981,8 +21149,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
20981
21149
  childNodes: null,
20982
21150
  DETACHABLE: true,
20983
21151
  UPDATE_PRIORITY: 2,
21152
+ /** @deprecated is no longer used (moved to Paper) */
20984
21153
  FLAG_INSERT: 1 << 30,
21154
+ /** @deprecated is no longer used */
20985
21155
  FLAG_REMOVE: 1 << 29,
21156
+ /** @deprecated is no longer used */
20986
21157
  FLAG_INIT: 1 << 28,
20987
21158
  constructor: function (options) {
20988
21159
  this.requireSetThemeOverride = options && !!options.theme;
@@ -22094,6 +22265,31 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
22094
22265
  return null;
22095
22266
  }
22096
22267
  },
22268
+ // Check if the cellView has a highlighter with the given `id`.
22269
+ // If no `id` is provided, it checks if the cellView has any highlighter.
22270
+ has(cellView, id = null) {
22271
+ const {
22272
+ cid
22273
+ } = cellView;
22274
+ const {
22275
+ _views
22276
+ } = this;
22277
+ const refs = _views[cid];
22278
+ if (!refs) return false;
22279
+ if (id === null) {
22280
+ // any highlighter
22281
+ for (let hid in refs) {
22282
+ if (refs[hid] instanceof this) return true;
22283
+ }
22284
+ return false;
22285
+ } else {
22286
+ // single highlighter
22287
+ if (id in refs) {
22288
+ if (refs[id] instanceof this) return true;
22289
+ }
22290
+ return false;
22291
+ }
22292
+ },
22097
22293
  add(cellView, nodeSelector, id, opt = {}) {
22098
22294
  if (!id) throw new Error('dia.HighlighterView: An ID required.');
22099
22295
  // Search the existing view amongst all the highlighters
@@ -28837,6 +29033,14 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
28837
29033
  }
28838
29034
  });
28839
29035
 
29036
+ // Internal tag to identify this object as a cell view instance.
29037
+ // Used instead of `instanceof` for performance and cross-frame safety.
29038
+
29039
+ const CELL_VIEW_MARKER = Symbol('joint.cellViewMarker');
29040
+ Object.defineProperty(CellView.prototype, CELL_VIEW_MARKER, {
29041
+ value: true
29042
+ });
29043
+
28840
29044
  const Flags$1 = {
28841
29045
  TOOLS: CellView.Flags.TOOLS,
28842
29046
  UPDATE: 'UPDATE',
@@ -29190,7 +29394,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
29190
29394
  });
29191
29395
  data.initialParentId = parentId;
29192
29396
  } else {
29193
- data.initialParentId = null;
29397
+ // `data.initialParentId` can be explicitly set to a dummy value to enable validation of unembedding.
29398
+ data.initialParentId = data.initialParentId || null;
29194
29399
  }
29195
29400
  },
29196
29401
  processEmbedding: function (data = {}, evt, x, y) {
@@ -29669,6 +29874,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
29669
29874
  _labelCache: null,
29670
29875
  _labelSelectors: null,
29671
29876
  _V: null,
29877
+ _sourceMagnet: null,
29878
+ _targetMagnet: null,
29672
29879
  _dragData: null,
29673
29880
  // deprecated
29674
29881
 
@@ -29705,23 +29912,34 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
29705
29912
  initFlag: [Flags.RENDER, Flags.SOURCE, Flags.TARGET, Flags.TOOLS],
29706
29913
  UPDATE_PRIORITY: 1,
29707
29914
  EPSILON: 1e-6,
29708
- confirmUpdate: function (flags, opt) {
29709
- opt || (opt = {});
29915
+ confirmUpdate: function (flags, opt = {}) {
29916
+ const {
29917
+ paper,
29918
+ model
29919
+ } = this;
29920
+ const {
29921
+ attributes
29922
+ } = model;
29923
+ const {
29924
+ source: {
29925
+ id: sourceId
29926
+ },
29927
+ target: {
29928
+ id: targetId
29929
+ }
29930
+ } = attributes;
29710
29931
  if (this.hasFlag(flags, Flags.SOURCE)) {
29711
- if (!this.updateEndProperties('source')) return flags;
29932
+ this._sourceMagnet = null; // reset cached source magnet
29933
+ this.checkEndModel('source', sourceId);
29712
29934
  flags = this.removeFlag(flags, Flags.SOURCE);
29713
29935
  }
29714
29936
  if (this.hasFlag(flags, Flags.TARGET)) {
29715
- if (!this.updateEndProperties('target')) return flags;
29937
+ this._targetMagnet = null; // reset cached target magnet
29938
+ this.checkEndModel('target', targetId);
29716
29939
  flags = this.removeFlag(flags, Flags.TARGET);
29717
29940
  }
29718
- const {
29719
- paper,
29720
- sourceView,
29721
- targetView
29722
- } = this;
29723
- if (paper && (sourceView && !paper.isViewMounted(sourceView) || targetView && !paper.isViewMounted(targetView))) {
29724
- // Wait for the sourceView and targetView to be rendered
29941
+ if (paper && (sourceId && !paper.isCellVisible(sourceId) || targetId && !paper.isCellVisible(targetId))) {
29942
+ // Wait for the source and target views to be rendered
29725
29943
  return flags;
29726
29944
  }
29727
29945
  if (this.hasFlag(flags, Flags.RENDER)) {
@@ -29729,18 +29947,12 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
29729
29947
  this.updateHighlighters(true);
29730
29948
  this.updateTools(opt);
29731
29949
  flags = this.removeFlag(flags, [Flags.RENDER, Flags.UPDATE, Flags.LABELS, Flags.TOOLS, Flags.CONNECTOR]);
29732
- if (env.test('isSafari')) {
29733
- this.__fixSafariBug268376();
29950
+ if (env.test('isAppleWebKit')) {
29951
+ this.__fixWebKitBug268376();
29734
29952
  }
29735
29953
  return flags;
29736
29954
  }
29737
29955
  let updateHighlighters = false;
29738
- const {
29739
- model
29740
- } = this;
29741
- const {
29742
- attributes
29743
- } = model;
29744
29956
  let updateLabels = this.hasFlag(flags, Flags.LABELS);
29745
29957
  if (updateLabels) {
29746
29958
  this.onLabelsChange(model, attributes.labels, opt);
@@ -29779,8 +29991,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
29779
29991
  }
29780
29992
  return flags;
29781
29993
  },
29782
- __fixSafariBug268376: function () {
29783
- // Safari has a bug where any change after the first render is not reflected in the DOM.
29994
+ __fixWebKitBug268376: function () {
29995
+ // WebKit has a bug where any change after the first render is not reflected in the DOM.
29784
29996
  // https://bugs.webkit.org/show_bug.cgi?id=268376
29785
29997
  const {
29786
29998
  el
@@ -30449,40 +30661,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
30449
30661
  if (id && id in metrics) delete metrics[id].magnetMatrix;
30450
30662
  }
30451
30663
  },
30452
- updateEndProperties: function (endType) {
30453
- const {
30454
- model,
30455
- paper
30456
- } = this;
30457
- const endViewProperty = `${endType}View`;
30458
- const endDef = model.get(endType);
30459
- const endId = endDef && endDef.id;
30460
- if (!endId) {
30461
- // the link end is a point ~ rect 0x0
30462
- this[endViewProperty] = null;
30463
- this.updateEndMagnet(endType);
30464
- return true;
30465
- }
30466
- const endModel = paper.getModelById(endId);
30467
- if (!endModel) throw new Error('LinkView: invalid ' + endType + ' cell.');
30468
- const endView = endModel.findView(paper);
30469
- if (!endView) {
30470
- // A view for a model should always exist
30471
- return false;
30472
- }
30473
- this[endViewProperty] = endView;
30474
- this.updateEndMagnet(endType);
30475
- return true;
30476
- },
30477
- updateEndMagnet: function (endType) {
30478
- const endMagnetProperty = `${endType}Magnet`;
30479
- const endView = this.getEndView(endType);
30480
- if (endView) {
30481
- let connectedMagnet = endView.getMagnetFromLinkEnd(this.model.get(endType));
30482
- if (connectedMagnet === endView.el) connectedMagnet = null;
30483
- this[endMagnetProperty] = connectedMagnet;
30484
- } else {
30485
- this[endMagnetProperty] = null;
30664
+ checkEndModel: function (endType, endId) {
30665
+ if (!endId) return;
30666
+ const endModel = this.paper.getModelById(endId);
30667
+ if (!endModel) {
30668
+ throw new Error(`LinkView: invalid ${endType} cell.`);
30486
30669
  }
30487
30670
  },
30488
30671
  _getLabelPositionProperty: function (idx) {
@@ -31247,52 +31430,30 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
31247
31430
  let isSnapped = false;
31248
31431
  // checking view in close area of the pointer
31249
31432
 
31250
- var r = snapLinks.radius || 50;
31251
- var viewsInArea = paper.findElementViewsInArea({
31252
- x: x - r,
31253
- y: y - r,
31254
- width: 2 * r,
31255
- height: 2 * r
31256
- }, snapLinks.findInAreaOptions);
31257
- var prevClosestView = data.closestView || null;
31258
- var prevClosestMagnet = data.closestMagnet || null;
31259
- var prevMagnetProxy = data.magnetProxy || null;
31433
+ const radius = snapLinks.radius || 50;
31434
+ const findInAreaOptions = snapLinks.findInAreaOptions;
31435
+ const prevClosestView = data.closestView || null;
31436
+ const prevClosestMagnet = data.closestMagnet || null;
31437
+ const prevMagnetProxy = data.magnetProxy || null;
31260
31438
  data.closestView = data.closestMagnet = data.magnetProxy = null;
31261
- var minDistance = Number.MAX_VALUE;
31262
- var pointer = new Point(x, y);
31263
- viewsInArea.forEach(function (view) {
31264
- const candidates = [];
31265
- // skip connecting to the element in case '.': { magnet: false } attribute present
31266
- if (view.el.getAttribute('magnet') !== 'false') {
31267
- candidates.push({
31268
- bbox: view.model.getBBox(),
31269
- magnet: view.el
31270
- });
31439
+ const isValidCandidate = (view, magnet) => {
31440
+ // Do not snap to the current view
31441
+ if (view === this) {
31442
+ return false;
31271
31443
  }
31272
- view.$('[magnet]').toArray().forEach(magnet => {
31273
- candidates.push({
31274
- bbox: view.getNodeBBox(magnet),
31275
- magnet
31276
- });
31277
- });
31278
- candidates.forEach(candidate => {
31279
- const {
31280
- magnet,
31281
- bbox
31282
- } = candidate;
31283
- // find distance from the center of the model to pointer coordinates
31284
- const distance = bbox.center().squaredDistance(pointer);
31285
- // the connection is looked up in a circle area by `distance < r`
31286
- if (distance < minDistance) {
31287
- const isAlreadyValidated = prevClosestMagnet === magnet;
31288
- if (isAlreadyValidated || paper.options.validateConnection.apply(paper, data.validateConnectionArgs(view, view.el === magnet ? null : magnet))) {
31289
- minDistance = distance;
31290
- data.closestView = view;
31291
- data.closestMagnet = magnet;
31292
- }
31293
- }
31294
- });
31295
- }, this);
31444
+ const isAlreadyValidated = prevClosestMagnet === magnet;
31445
+ return isAlreadyValidated || paper.options.validateConnection.apply(paper, data.validateConnectionArgs(view, view.el === magnet ? null : magnet));
31446
+ };
31447
+ const closest = paper.findClosestMagnetToPoint({
31448
+ x,
31449
+ y
31450
+ }, {
31451
+ radius,
31452
+ findInAreaOptions,
31453
+ filter: isValidCandidate
31454
+ });
31455
+ data.closestView = closest ? closest.view : null;
31456
+ data.closestMagnet = closest ? closest.magnet : null;
31296
31457
  var end;
31297
31458
  var magnetProxy = null;
31298
31459
  var closestView = data.closestView;
@@ -31586,6 +31747,72 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
31586
31747
  }, {
31587
31748
  Flags: Flags
31588
31749
  });
31750
+ Object.defineProperty(LinkView.prototype, 'sourceView', {
31751
+ enumerable: true,
31752
+ get: function () {
31753
+ const source = this.model.attributes.source;
31754
+ if (source.id && this.paper) {
31755
+ return this.paper.findViewByModel(source.id);
31756
+ }
31757
+ return null;
31758
+ }
31759
+ });
31760
+ Object.defineProperty(LinkView.prototype, 'targetView', {
31761
+ enumerable: true,
31762
+ get: function () {
31763
+ const target = this.model.attributes.target;
31764
+ if (target.id && this.paper) {
31765
+ return this.paper.findViewByModel(target.id);
31766
+ }
31767
+ return null;
31768
+ }
31769
+ });
31770
+ Object.defineProperty(LinkView.prototype, 'sourceMagnet', {
31771
+ enumerable: true,
31772
+ get: function () {
31773
+ const sourceView = this.sourceView;
31774
+ if (!sourceView) return null;
31775
+ let sourceMagnet = null;
31776
+ // Check if the magnet is already found and cached.
31777
+ // We need to check if the cached magnet is still part of the source view.
31778
+ // The source view might have been disposed and recreated, or the magnet might have been changed.
31779
+ const cachedSourceMagnet = this._sourceMagnet;
31780
+ if (cachedSourceMagnet && sourceView.el.contains(cachedSourceMagnet)) {
31781
+ sourceMagnet = cachedSourceMagnet;
31782
+ } else {
31783
+ // If the cached magnet is not valid, we need to find the magnet.
31784
+ sourceMagnet = sourceView.getMagnetFromLinkEnd(this.model.attributes.source);
31785
+ }
31786
+ this._sourceMagnet = sourceMagnet;
31787
+ if (sourceMagnet === sourceView.el) {
31788
+ // If the source magnet is the element itself, we treat it as no magnet.
31789
+ return null;
31790
+ }
31791
+ return sourceMagnet;
31792
+ }
31793
+ });
31794
+ Object.defineProperty(LinkView.prototype, 'targetMagnet', {
31795
+ enumerable: true,
31796
+ get: function () {
31797
+ const targetView = this.targetView;
31798
+ if (!targetView) return null;
31799
+ let targetMagnet = null;
31800
+ // Check if the magnet is already found and cached (See `sourceMagnet` for explanation).
31801
+ const cachedTargetMagnet = this._targetMagnet;
31802
+ if (cachedTargetMagnet && targetView.el.contains(cachedTargetMagnet)) {
31803
+ targetMagnet = cachedTargetMagnet;
31804
+ } else {
31805
+ // If the cached magnet is not valid, we need to find the magnet.
31806
+ targetMagnet = targetView.getMagnetFromLinkEnd(this.model.attributes.target);
31807
+ }
31808
+ this._targetMagnet = targetMagnet;
31809
+ if (targetMagnet === targetView.el) {
31810
+ // If the target magnet is the element itself, we treat it as no magnet.
31811
+ return null;
31812
+ }
31813
+ return targetMagnet;
31814
+ }
31815
+ });
31589
31816
  Object.defineProperty(LinkView.prototype, 'sourceBBox', {
31590
31817
  enumerable: true,
31591
31818
  get: function () {
@@ -31617,6 +31844,128 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
31617
31844
  }
31618
31845
  });
31619
31846
 
31847
+ /**
31848
+ * Deque implementation for managing a double-ended queue.
31849
+ * This implementation uses a doubly linked list for efficient operations.
31850
+ * It supports operations like push, pop, move to head, and delete.
31851
+ * The deque maintains a map for O(1) access to nodes by key.
31852
+ */
31853
+ class Deque {
31854
+ constructor() {
31855
+ this.head = null;
31856
+ this.tail = null;
31857
+ this.map = new Map(); // key -> node
31858
+ }
31859
+
31860
+ // Return an array of keys in the deque
31861
+ keys() {
31862
+ let current = this.head;
31863
+ const keys = [];
31864
+ while (current) {
31865
+ keys.push(current.key);
31866
+ current = current.next;
31867
+ }
31868
+ return keys;
31869
+ }
31870
+
31871
+ // Return the first node and remove it from the deque
31872
+ popHead() {
31873
+ if (!this.head) return null;
31874
+ const node = this.head;
31875
+ this.map.delete(node.key);
31876
+ this.head = node.next;
31877
+ if (this.head) {
31878
+ this.head.prev = null;
31879
+ } else {
31880
+ this.tail = null;
31881
+ }
31882
+ return node;
31883
+ }
31884
+
31885
+ // Add a new node to the back of the deque
31886
+ pushTail(key, value) {
31887
+ if (this.map.has(key)) {
31888
+ throw new Error(`Key "${key}" already exists in the deque.`);
31889
+ }
31890
+ const node = {
31891
+ key,
31892
+ value,
31893
+ prev: null,
31894
+ next: null
31895
+ };
31896
+ this.map.set(key, node);
31897
+ if (!this.tail) {
31898
+ this.head = this.tail = node;
31899
+ } else {
31900
+ this.tail.next = node;
31901
+ node.prev = this.tail;
31902
+ this.tail = node;
31903
+ }
31904
+ }
31905
+
31906
+ // Move a node from the deque to the head
31907
+ moveToHead(key) {
31908
+ const node = this.map.get(key);
31909
+ if (!node) return;
31910
+ if (node === this.head) return; // already at head
31911
+ // Remove node from its current position
31912
+ if (node.prev) node.prev.next = node.next;
31913
+ if (node.next) node.next.prev = node.prev;
31914
+ if (node === this.tail) this.tail = node.prev; // if it's the tail
31915
+ if (node === this.head) this.head = node.next; // if it's the head
31916
+ // Move node to head
31917
+ node.prev = null;
31918
+ node.next = this.head;
31919
+ if (this.head) {
31920
+ this.head.prev = node; // link old head back to new head
31921
+ }
31922
+ this.head = node; // update head to be the moved node
31923
+ if (!this.tail) {
31924
+ this.tail = node; // if it was the only node, set tail as well
31925
+ }
31926
+ }
31927
+
31928
+ // Return the first node without removing it
31929
+ peekHead() {
31930
+ return this.head || null;
31931
+ }
31932
+
31933
+ // Move the head node to the back of the deque
31934
+ rotate() {
31935
+ if (!this.head || !this.head.next) return;
31936
+ this.tail.next = this.head; // link tail to head
31937
+ this.head.prev = this.tail; // link head back to tail
31938
+ this.tail = this.head; // update tail to be the old head
31939
+ this.head = this.head.next; // move head to the next node
31940
+ this.tail.next = null; // set new tail's next to null
31941
+ this.head.prev = null; // set new head's prev to null
31942
+ }
31943
+
31944
+ // Remove a node from the deque
31945
+ delete(key) {
31946
+ const node = this.map.get(key);
31947
+ if (!node) return;
31948
+ if (node.prev) node.prev.next = node.next;else this.head = node.next;
31949
+ if (node.next) node.next.prev = node.prev;else this.tail = node.prev;
31950
+ this.map.delete(key);
31951
+ }
31952
+
31953
+ // Does the deque contain a node with the given key?
31954
+ has(key) {
31955
+ return this.map.has(key);
31956
+ }
31957
+
31958
+ // Get the node with the given key
31959
+ get(key) {
31960
+ return this.map.get(key) || null;
31961
+ }
31962
+
31963
+ // Number of nodes in the deque
31964
+ get length() {
31965
+ return this.map.size;
31966
+ }
31967
+ }
31968
+
31620
31969
  const GridLayer = PaperLayer.extend({
31621
31970
  style: {
31622
31971
  'pointer-events': 'none'
@@ -31858,6 +32207,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
31858
32207
  }, {
31859
32208
  name: LayersNames.TOOLS
31860
32209
  }];
32210
+ const CELL_VIEW_PLACEHOLDER_MARKER = Symbol('joint.cellViewPlaceholderMarker');
31861
32211
  const Paper = View.extend({
31862
32212
  className: 'paper',
31863
32213
  options: {
@@ -32008,6 +32358,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32008
32358
  sorting: sortingTypes.APPROX,
32009
32359
  frozen: false,
32010
32360
  autoFreeze: false,
32361
+ viewManagement: false,
32011
32362
  // no docs yet
32012
32363
  onViewUpdate: function (view, flag, priority, opt, paper) {
32013
32364
  // Do not update connected links when:
@@ -32015,7 +32366,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32015
32366
  // 2. the view was just mounted (added back to the paper by viewport function)
32016
32367
  // 3. the change was marked as `isolate`.
32017
32368
  // 4. the view model was just removed from the graph
32018
- if (flag & (view.FLAG_INSERT | view.FLAG_REMOVE) || opt.mounting || opt.isolate) return;
32369
+ if (flag & (paper.FLAG_INSERT | paper.FLAG_REMOVE) || opt.mounting || opt.isolate) return;
32019
32370
  paper.requestConnectedLinksUpdate(view, priority, opt);
32020
32371
  },
32021
32372
  // no docs yet
@@ -32114,6 +32465,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32114
32465
  DEFAULT_FIND_BUFFER: 200,
32115
32466
  // Default layer to insert the cell views into.
32116
32467
  DEFAULT_CELL_LAYER: LayersNames.CELLS,
32468
+ // Update flags
32469
+ FLAG_INSERT: 1 << 30,
32470
+ FLAG_REMOVE: 1 << 29,
32471
+ FLAG_INIT: 1 << 28,
32117
32472
  init: function () {
32118
32473
  const {
32119
32474
  options
@@ -32125,6 +32480,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32125
32480
  }
32126
32481
  const model = this.model = options.model || new Graph();
32127
32482
 
32483
+ // This property tells us if we need to keep the compatibility
32484
+ // with the v4 API and behavior.
32485
+ this.legacyMode = !options.viewManagement;
32486
+
32128
32487
  // Layers (SVGGroups)
32129
32488
  this._layers = {
32130
32489
  viewsMap: {},
@@ -32138,6 +32497,8 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32138
32497
 
32139
32498
  // Hash of all cell views.
32140
32499
  this._views = {};
32500
+ this._viewPlaceholders = {};
32501
+ this._idToCid = {};
32141
32502
 
32142
32503
  // Mouse wheel events buffer
32143
32504
  this._mw_evt_buffer = {
@@ -32147,24 +32508,21 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32147
32508
 
32148
32509
  // Render existing cells in the graph
32149
32510
  this.resetViews(model.attributes.cells.models);
32150
- // Start the Rendering Loop
32151
- if (!this.isFrozen() && this.isAsync()) this.updateViewsAsync();
32152
32511
  },
32153
32512
  _resetUpdates: function () {
32154
32513
  if (this._updates && this._updates.id) cancelFrame(this._updates.id);
32155
32514
  return this._updates = {
32156
32515
  id: null,
32157
32516
  priorities: [{}, {}, {}],
32158
- unmountedCids: [],
32159
- mountedCids: [],
32160
- unmounted: {},
32161
- mounted: {},
32517
+ unmountedList: new Deque(),
32518
+ mountedList: new Deque(),
32162
32519
  count: 0,
32163
32520
  keyFrozen: false,
32164
32521
  freezeKey: null,
32165
32522
  sort: false,
32166
32523
  disabled: false,
32167
- idle: false
32524
+ idle: false,
32525
+ freshAfterReset: true
32168
32526
  };
32169
32527
  },
32170
32528
  startListening: function () {
@@ -32187,14 +32545,21 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32187
32545
  }
32188
32546
  },
32189
32547
  onCellRemoved: function (cell, _, opt) {
32190
- const view = this.findViewByModel(cell);
32191
- if (view) this.requestViewUpdate(view, view.FLAG_REMOVE, view.UPDATE_PRIORITY, opt);
32548
+ const viewLike = this._getCellViewLike(cell);
32549
+ if (!viewLike) return;
32550
+ if (viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) {
32551
+ this._unregisterCellViewPlaceholder(viewLike);
32552
+ } else {
32553
+ this.requestViewUpdate(viewLike, this.FLAG_REMOVE, viewLike.UPDATE_PRIORITY, opt);
32554
+ }
32192
32555
  },
32193
32556
  onCellChange: function (cell, opt) {
32194
32557
  if (cell === this.model.attributes.cells) return;
32195
32558
  if (cell.hasChanged('layer') || cell.hasChanged('z') && this.options.sorting === sortingTypes.APPROX) {
32196
- const view = this.findViewByModel(cell);
32197
- if (view) this.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
32559
+ const viewLike = this._getCellViewLike(cell);
32560
+ if (viewLike) {
32561
+ this.requestViewUpdate(viewLike, this.FLAG_INSERT, viewLike.UPDATE_PRIORITY, opt);
32562
+ }
32198
32563
  }
32199
32564
  },
32200
32565
  onGraphReset: function (collection, opt) {
@@ -32206,7 +32571,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32206
32571
  this.sortViews();
32207
32572
  },
32208
32573
  onGraphBatchStop: function (data) {
32209
- if (this.isFrozen()) return;
32574
+ if (this.isFrozen() || this.isIdle()) return;
32210
32575
  var name = data && data.batchName;
32211
32576
  var graph = this.model;
32212
32577
  if (!this.isAsync()) {
@@ -32266,6 +32631,16 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32266
32631
  // Return the default highlighting options into the user specified options.
32267
32632
  options.highlighting = defaultsDeep({}, highlighting, defaultHighlighting);
32268
32633
  }
32634
+ // Copy and set defaults for the view management options.
32635
+ options.viewManagement = defaults({}, options.viewManagement, {
32636
+ // Whether to lazy initialize the cell views.
32637
+ lazyInitialize: !!options.viewManagement,
32638
+ // default `true` if options.viewManagement provided
32639
+ // Whether to add initialized cell views into the unmounted queue.
32640
+ initializeUnmounted: false,
32641
+ // Whether to dispose the cell views that are not visible.
32642
+ disposeHidden: false
32643
+ });
32269
32644
  },
32270
32645
  children: function () {
32271
32646
  var ns = V.namespace;
@@ -32644,47 +33019,46 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32644
33019
  var links = this.model.getConnectedLinks(model);
32645
33020
  for (var j = 0, n = links.length; j < n; j++) {
32646
33021
  var link = links[j];
32647
- var linkView = this.findViewByModel(link);
33022
+ var linkView = this._getCellViewLike(link);
32648
33023
  if (!linkView) continue;
32649
- var flagLabels = ['UPDATE'];
32650
- if (link.getTargetCell() === model) flagLabels.push('TARGET');
32651
- if (link.getSourceCell() === model) flagLabels.push('SOURCE');
33024
+ // We do not have to update placeholder views.
33025
+ // They will be updated on initial render.
33026
+ if (linkView[CELL_VIEW_PLACEHOLDER_MARKER]) continue;
32652
33027
  var nextPriority = Math.max(priority + 1, linkView.UPDATE_PRIORITY);
32653
- this.scheduleViewUpdate(linkView, linkView.getFlag(flagLabels), nextPriority, opt);
33028
+ this.scheduleViewUpdate(linkView, linkView.getFlag(LinkView.Flags.UPDATE), nextPriority, opt);
32654
33029
  }
32655
33030
  }
32656
33031
  },
32657
33032
  forcePostponedViewUpdate: function (view, flag) {
32658
33033
  if (!view || !(view instanceof CellView)) return false;
32659
- var model = view.model;
33034
+ const model = view.model;
32660
33035
  if (model.isElement()) return false;
32661
- if ((flag & view.getFlag(['SOURCE', 'TARGET'])) === 0) {
32662
- var dumpOptions = {
32663
- silent: true
32664
- };
32665
- // LinkView is waiting for the target or the source cellView to be rendered
32666
- // This can happen when the cells are not in the viewport.
32667
- var sourceFlag = 0;
32668
- var sourceView = this.findViewByModel(model.getSourceCell());
32669
- if (sourceView && !this.isViewMounted(sourceView)) {
32670
- sourceFlag = this.dumpView(sourceView, dumpOptions);
32671
- view.updateEndMagnet('source');
32672
- }
32673
- var targetFlag = 0;
32674
- var targetView = this.findViewByModel(model.getTargetCell());
32675
- if (targetView && !this.isViewMounted(targetView)) {
32676
- targetFlag = this.dumpView(targetView, dumpOptions);
32677
- view.updateEndMagnet('target');
32678
- }
32679
- if (sourceFlag === 0 && targetFlag === 0) {
32680
- // If leftover flag is 0, all view updates were done.
32681
- return !this.dumpView(view, dumpOptions);
32682
- }
33036
+ const dumpOptions = {
33037
+ silent: true
33038
+ };
33039
+ // LinkView is waiting for the target or the source cellView to be rendered
33040
+ // This can happen when the cells are not in the viewport.
33041
+ let sourceFlag = 0;
33042
+ const sourceCell = model.getSourceCell();
33043
+ if (sourceCell && !this.isCellVisible(sourceCell)) {
33044
+ const sourceView = this.findViewByModel(sourceCell);
33045
+ sourceFlag = this.dumpView(sourceView, dumpOptions);
33046
+ }
33047
+ let targetFlag = 0;
33048
+ const targetCell = model.getTargetCell();
33049
+ if (targetCell && !this.isCellVisible(targetCell)) {
33050
+ const targetView = this.findViewByModel(targetCell);
33051
+ targetFlag = this.dumpView(targetView, dumpOptions);
33052
+ }
33053
+ if (sourceFlag === 0 && targetFlag === 0) {
33054
+ // If leftover flag is 0, all view updates were done.
33055
+ return !this.dumpView(view, dumpOptions);
32683
33056
  }
32684
33057
  return false;
32685
33058
  },
32686
33059
  requestViewUpdate: function (view, flag, priority, opt) {
32687
33060
  opt || (opt = {});
33061
+ // Note: `scheduleViewUpdate` wakes up the paper if it is idle.
32688
33062
  this.scheduleViewUpdate(view, flag, priority, opt);
32689
33063
  var isAsync = this.isAsync();
32690
33064
  if (this.isFrozen() || isAsync && opt.async !== false) return;
@@ -32697,15 +33071,15 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32697
33071
  _updates: updates,
32698
33072
  options
32699
33073
  } = this;
32700
- if (updates.idle) {
32701
- if (options.autoFreeze) {
32702
- updates.idle = false;
32703
- this.unfreeze();
32704
- }
33074
+ if (updates.idle && options.autoFreeze) {
33075
+ this.legacyMode ? this.unfreeze() // Restart rendering loop without original options
33076
+ : this.wakeUp();
32705
33077
  }
32706
33078
  const {
32707
33079
  FLAG_REMOVE,
32708
- FLAG_INSERT,
33080
+ FLAG_INSERT
33081
+ } = this;
33082
+ const {
32709
33083
  UPDATE_PRIORITY,
32710
33084
  cid
32711
33085
  } = view;
@@ -32752,16 +33126,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32752
33126
  dumpView: function (view, opt = {}) {
32753
33127
  const flag = this.dumpViewUpdate(view);
32754
33128
  if (!flag) return 0;
32755
- const shouldNotify = !opt.silent;
32756
- if (shouldNotify) this.notifyBeforeRender(opt);
33129
+ this.notifyBeforeRender(opt);
32757
33130
  const leftover = this.updateView(view, flag, opt);
32758
- if (shouldNotify) {
32759
- const stats = {
32760
- updated: 1,
32761
- priority: view.UPDATE_PRIORITY
32762
- };
32763
- this.notifyAfterRender(stats, opt);
32764
- }
33131
+ const stats = {
33132
+ updated: 1,
33133
+ priority: view.UPDATE_PRIORITY
33134
+ };
33135
+ this.notifyAfterRender(stats, opt);
32765
33136
  return leftover;
32766
33137
  },
32767
33138
  updateView: function (view, flag, opt) {
@@ -32769,10 +33140,12 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32769
33140
  const {
32770
33141
  FLAG_REMOVE,
32771
33142
  FLAG_INSERT,
32772
- FLAG_INIT,
33143
+ FLAG_INIT
33144
+ } = this;
33145
+ const {
32773
33146
  model
32774
33147
  } = view;
32775
- if (view instanceof CellView) {
33148
+ if (view[CELL_VIEW_MARKER]) {
32776
33149
  if (flag & FLAG_REMOVE) {
32777
33150
  this.removeView(model);
32778
33151
  return 0;
@@ -32798,58 +33171,70 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32798
33171
  registerUnmountedView: function (view) {
32799
33172
  var cid = view.cid;
32800
33173
  var updates = this._updates;
32801
- if (cid in updates.unmounted) return 0;
32802
- var flag = updates.unmounted[cid] |= view.FLAG_INSERT;
32803
- updates.unmountedCids.push(cid);
32804
- delete updates.mounted[cid];
33174
+ if (updates.unmountedList.has(cid)) return 0;
33175
+ const flag = this.FLAG_INSERT;
33176
+ updates.unmountedList.pushTail(cid, flag);
33177
+ updates.mountedList.delete(cid);
32805
33178
  return flag;
32806
33179
  },
32807
33180
  registerMountedView: function (view) {
32808
33181
  var cid = view.cid;
32809
33182
  var updates = this._updates;
32810
- if (cid in updates.mounted) return 0;
32811
- updates.mounted[cid] = true;
32812
- updates.mountedCids.push(cid);
32813
- var flag = updates.unmounted[cid] || 0;
32814
- delete updates.unmounted[cid];
33183
+ if (updates.mountedList.has(cid)) return 0;
33184
+ const unmountedItem = updates.unmountedList.get(cid);
33185
+ const flag = unmountedItem ? unmountedItem.value : 0;
33186
+ updates.unmountedList.delete(cid);
33187
+ updates.mountedList.pushTail(cid);
32815
33188
  return flag;
32816
33189
  },
32817
- isViewMounted: function (view) {
32818
- if (!view) return false;
32819
- var cid = view.cid;
32820
- var updates = this._updates;
32821
- return cid in updates.mounted;
33190
+ isCellVisible: function (cellOrId) {
33191
+ const cid = cellOrId && this._idToCid[cellOrId.id || cellOrId];
33192
+ if (!cid) return false; // The view is not registered.
33193
+ return this.isViewMounted(cid);
33194
+ },
33195
+ isViewMounted: function (viewOrCid) {
33196
+ if (!viewOrCid) return false;
33197
+ let cid;
33198
+ if (viewOrCid[CELL_VIEW_MARKER] || viewOrCid[CELL_VIEW_PLACEHOLDER_MARKER]) {
33199
+ cid = viewOrCid.cid;
33200
+ } else {
33201
+ cid = viewOrCid;
33202
+ }
33203
+ return this._updates.mountedList.has(cid);
32822
33204
  },
33205
+ /**
33206
+ * @deprecated use `updateCellsVisibility` instead.
33207
+ * `paper.updateCellsVisibility({ cellVisibility: () => true });`
33208
+ */
32823
33209
  dumpViews: function (opt) {
32824
- var passingOpt = defaults({}, opt, {
33210
+ // Update cell visibility without `cellVisibility` callback i.e. make the cells visible
33211
+ const passingOpt = defaults({}, opt, {
33212
+ cellVisibility: null,
32825
33213
  viewport: null
32826
33214
  });
32827
- this.checkViewport(passingOpt);
32828
- this.updateViews(passingOpt);
33215
+ this.updateCellsVisibility(passingOpt);
32829
33216
  },
32830
- // Synchronous views update
32831
- updateViews: function (opt) {
33217
+ /**
33218
+ * Process all scheduled updates synchronously.
33219
+ */
33220
+ updateViews: function (opt = {}) {
32832
33221
  this.notifyBeforeRender(opt);
32833
- let batchStats;
32834
- let updateCount = 0;
32835
- let batchCount = 0;
32836
- let priority = MIN_PRIORITY;
32837
- do {
32838
- batchCount++;
32839
- batchStats = this.updateViewsBatch(opt);
32840
- updateCount += batchStats.updated;
32841
- priority = Math.min(batchStats.priority, priority);
32842
- } while (!batchStats.empty);
33222
+ const batchStats = this.updateViewsBatch({
33223
+ ...opt,
33224
+ batchSize: Infinity
33225
+ });
32843
33226
  const stats = {
32844
- updated: updateCount,
32845
- batches: batchCount,
32846
- priority
33227
+ updated: batchStats.updated,
33228
+ priority: batchStats.priority,
33229
+ // For backward compatibility. Will be removed in the future.
33230
+ batches: Number.isFinite(opt.batchSize) ? Math.ceil(batchStats.updated / opt.batchSize) : 1
32847
33231
  };
32848
33232
  this.notifyAfterRender(stats, opt);
32849
33233
  return stats;
32850
33234
  },
32851
33235
  hasScheduledUpdates: function () {
32852
- const priorities = this._updates.priorities;
33236
+ const updates = this._updates;
33237
+ const priorities = updates.priorities;
32853
33238
  const priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
32854
33239
  let i = priorityIndexes.length;
32855
33240
  while (i > 0 && i--) {
@@ -32862,15 +33247,43 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32862
33247
  opt || (opt = {});
32863
33248
  data || (data = {
32864
33249
  processed: 0,
32865
- priority: MIN_PRIORITY
33250
+ priority: MIN_PRIORITY,
33251
+ checkedUnmounted: 0,
33252
+ checkedMounted: 0
32866
33253
  });
32867
33254
  const {
32868
33255
  _updates: updates,
32869
33256
  options
32870
33257
  } = this;
32871
- const id = updates.id;
32872
- if (id) {
33258
+ const {
33259
+ id,
33260
+ mountedList,
33261
+ unmountedList,
33262
+ freshAfterReset
33263
+ } = updates;
33264
+
33265
+ // Should we run the next batch update this frame?
33266
+ let runBatchUpdate = true;
33267
+ if (!id) {
33268
+ // If there's no scheduled frame, no batch update is needed.
33269
+ runBatchUpdate = false;
33270
+ } else {
33271
+ // Cancel any scheduled frame.
32873
33272
  cancelFrame(id);
33273
+ if (freshAfterReset) {
33274
+ // First update after a reset.
33275
+ updates.freshAfterReset = false;
33276
+ // When `initializeUnmounted` is enabled, there are no scheduled updates.
33277
+ // We check whether the `mountedList` and `unmountedList` are empty.
33278
+ if (!this.legacyMode && mountedList.length === 0 && unmountedList.length === 0) {
33279
+ // No updates to process; We trigger before/after render events via `updateViews`.
33280
+ // Note: If `autoFreeze` is enabled, 'idle' event triggers next frame.
33281
+ this.updateViews();
33282
+ runBatchUpdate = false;
33283
+ }
33284
+ }
33285
+ }
33286
+ if (runBatchUpdate) {
32874
33287
  if (data.processed === 0 && this.hasScheduledUpdates()) {
32875
33288
  this.notifyBeforeRender(opt);
32876
33289
  }
@@ -32879,7 +33292,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32879
33292
  mountBatchSize: MOUNT_BATCH_SIZE - stats.mounted,
32880
33293
  unmountBatchSize: MOUNT_BATCH_SIZE - stats.unmounted
32881
33294
  });
32882
- const checkStats = this.checkViewport(passingOpt);
33295
+ const checkStats = this.scheduleCellsVisibilityUpdate(passingOpt);
32883
33296
  const unmountCount = checkStats.unmounted;
32884
33297
  const mountCount = checkStats.mounted;
32885
33298
  let processed = data.processed;
@@ -32900,11 +33313,21 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32900
33313
  } else {
32901
33314
  data.processed = processed;
32902
33315
  }
33316
+ data.checkedUnmounted = 0;
33317
+ data.checkedMounted = 0;
32903
33318
  } else {
32904
- if (!updates.idle) {
32905
- if (options.autoFreeze) {
33319
+ data.checkedUnmounted += Math.max(passingOpt.mountBatchSize, 0);
33320
+ data.checkedMounted += Math.max(passingOpt.unmountBatchSize, 0);
33321
+ // The `scheduleCellsVisibilityUpdate` could have scheduled some insertions
33322
+ // (note that removals are currently done synchronously).
33323
+ if (options.autoFreeze && !this.hasScheduledUpdates()) {
33324
+ // If there are no updates scheduled and we checked all unmounted views,
33325
+ if (data.checkedUnmounted >= unmountedList.length && data.checkedMounted >= mountedList.length) {
33326
+ // We freeze the paper and notify the idle state.
32906
33327
  this.freeze();
32907
- updates.idle = true;
33328
+ updates.idle = {
33329
+ wakeUpOptions: opt
33330
+ };
32908
33331
  this.trigger('render:idle', opt);
32909
33332
  }
32910
33333
  }
@@ -32923,6 +33346,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32923
33346
  updates.id = nextFrame(this.updateViewsAsync, this, opt, data);
32924
33347
  },
32925
33348
  notifyBeforeRender: function (opt = {}) {
33349
+ if (opt.silent) return;
32926
33350
  let beforeFn = opt.beforeRender;
32927
33351
  if (typeof beforeFn !== 'function') {
32928
33352
  beforeFn = this.options.beforeRender;
@@ -32931,6 +33355,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32931
33355
  beforeFn.call(this, opt, this);
32932
33356
  },
32933
33357
  notifyAfterRender: function (stats, opt = {}) {
33358
+ if (opt.silent) return;
32934
33359
  let afterFn = opt.afterRender;
32935
33360
  if (typeof afterFn !== 'function') {
32936
33361
  afterFn = this.options.afterRender;
@@ -32940,6 +33365,58 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32940
33365
  }
32941
33366
  this.trigger('render:done', stats, opt);
32942
33367
  },
33368
+ prioritizeCellViewMount: function (cellOrId) {
33369
+ if (!cellOrId) return false;
33370
+ const cid = this._idToCid[cellOrId.id || cellOrId];
33371
+ if (!cid) return false;
33372
+ const {
33373
+ unmountedList
33374
+ } = this._updates;
33375
+ if (!unmountedList.has(cid)) return false;
33376
+ // Move the view to the head of the mounted list
33377
+ unmountedList.moveToHead(cid);
33378
+ return true;
33379
+ },
33380
+ prioritizeCellViewUnmount: function (cellOrId) {
33381
+ if (!cellOrId) return false;
33382
+ const cid = this._idToCid[cellOrId.id || cellOrId];
33383
+ if (!cid) return false;
33384
+ const {
33385
+ mountedList
33386
+ } = this._updates;
33387
+ if (!mountedList.has(cid)) return false;
33388
+ // Move the view to the head of the unmounted list
33389
+ mountedList.moveToHead(cid);
33390
+ return true;
33391
+ },
33392
+ _evalCellVisibility: function (viewLike, isMounted, visibilityCallback) {
33393
+ if (!visibilityCallback || !viewLike.DETACHABLE) return true;
33394
+ if (this.legacyMode) {
33395
+ return visibilityCallback.call(this, viewLike, isMounted, this);
33396
+ }
33397
+ // The visibility check runs for CellView only.
33398
+ if (!viewLike[CELL_VIEW_MARKER] && !viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) return true;
33399
+ // The cellView model must be a member of this graph.
33400
+ if (viewLike.model.graph !== this.model) {
33401
+ // It could have been removed from the graph.
33402
+ // If the view was mounted, we keep it mounted.
33403
+ return isMounted;
33404
+ }
33405
+ return visibilityCallback.call(this, viewLike.model, isMounted, this);
33406
+ },
33407
+ _getCellVisibilityCallback: function (opt) {
33408
+ const {
33409
+ options
33410
+ } = this;
33411
+ if (this.legacyMode) {
33412
+ const viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
33413
+ if (typeof viewportFn === 'function') return viewportFn;
33414
+ } else {
33415
+ const isVisibleFn = 'cellVisibility' in opt ? opt.cellVisibility : options.cellVisibility;
33416
+ if (typeof isVisibleFn === 'function') return isVisibleFn;
33417
+ }
33418
+ return null;
33419
+ },
32943
33420
  updateViewsBatch: function (opt) {
32944
33421
  opt || (opt = {});
32945
33422
  var batchSize = opt.batchSize || UPDATE_BATCH_SIZE;
@@ -32952,8 +33429,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32952
33429
  var empty = true;
32953
33430
  var options = this.options;
32954
33431
  var priorities = updates.priorities;
32955
- var viewportFn = 'viewport' in opt ? opt.viewport : options.viewport;
32956
- if (typeof viewportFn !== 'function') viewportFn = null;
33432
+ const visibilityCb = this._getCellVisibilityCallback(opt);
32957
33433
  var postponeViewFn = options.onViewPostponed;
32958
33434
  if (typeof postponeViewFn !== 'function') postponeViewFn = null;
32959
33435
  var priorityIndexes = Object.keys(priorities); // convert priorities to a dense array
@@ -32967,31 +33443,53 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
32967
33443
  }
32968
33444
  var view = views[cid];
32969
33445
  if (!view) {
32970
- // This should not occur
32971
- delete priorityUpdates[cid];
32972
- continue;
33446
+ view = this._viewPlaceholders[cid];
33447
+ if (!view) {
33448
+ /**
33449
+ * This can occur when:
33450
+ * - the model is removed and a new model with the same id is added
33451
+ * - the view `initialize` method was overridden and the view was not registered
33452
+ * - an mvc.View scheduled an update, was removed and paper was not notified
33453
+ */
33454
+ delete priorityUpdates[cid];
33455
+ continue;
33456
+ }
32973
33457
  }
32974
33458
  var currentFlag = priorityUpdates[cid];
32975
- if ((currentFlag & view.FLAG_REMOVE) === 0) {
33459
+ if ((currentFlag & this.FLAG_REMOVE) === 0) {
32976
33460
  // We should never check a view for viewport if we are about to remove the view
32977
- var isDetached = cid in updates.unmounted;
32978
- if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, !isDetached, this)) {
33461
+ const isMounted = !updates.unmountedList.has(cid);
33462
+ if (!this._evalCellVisibility(view, isMounted, visibilityCb)) {
32979
33463
  // Unmount View
32980
- if (!isDetached) {
33464
+ if (isMounted) {
33465
+ // The view is currently mounted. Hide the view (detach or remove it).
32981
33466
  this.registerUnmountedView(view);
32982
- this.detachView(view);
33467
+ this._hideView(view);
33468
+ } else {
33469
+ // The view is not mounted. We can just update the unmounted list.
33470
+ // We ADD the current flag to the flag that was already scheduled.
33471
+ this._mergeUnmountedViewScheduledUpdates(cid, currentFlag);
32983
33472
  }
32984
- updates.unmounted[cid] |= currentFlag;
33473
+ // Delete the current update as it has been processed.
32985
33474
  delete priorityUpdates[cid];
32986
33475
  unmountCount++;
32987
33476
  continue;
32988
33477
  }
32989
33478
  // Mount View
32990
- if (isDetached) {
32991
- currentFlag |= view.FLAG_INSERT;
33479
+ if (view[CELL_VIEW_PLACEHOLDER_MARKER]) {
33480
+ view = this._resolveCellViewPlaceholder(view);
33481
+ // Newly initialized view needs to be initialized
33482
+ currentFlag |= this.getCellViewInitFlag(view);
33483
+ }
33484
+ if (!isMounted) {
33485
+ currentFlag |= this.FLAG_INSERT;
32992
33486
  mountCount++;
32993
33487
  }
32994
33488
  currentFlag |= this.registerMountedView(view);
33489
+ } else if (view[CELL_VIEW_PLACEHOLDER_MARKER]) {
33490
+ // We are trying to remove a placeholder view.
33491
+ // This should not occur as the placeholder should have been unregistered
33492
+ continue;
32995
33493
  }
32996
33494
  var leftoverFlag = this.updateView(view, currentFlag, opt);
32997
33495
  if (leftoverFlag > 0) {
@@ -33017,102 +33515,127 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33017
33515
  empty: empty
33018
33516
  };
33019
33517
  },
33518
+ getCellViewInitFlag: function (cellView) {
33519
+ return this.FLAG_INIT | cellView.getFlag(result(cellView, 'initFlag'));
33520
+ },
33521
+ /**
33522
+ * @ignore This method returns an array of cellViewLike objects and therefore
33523
+ * is meant for internal/test use only.
33524
+ * The view placeholders are not exposed via public API.
33525
+ */
33020
33526
  getUnmountedViews: function () {
33021
33527
  const updates = this._updates;
33022
- const unmountedCids = Object.keys(updates.unmounted);
33023
- const n = unmountedCids.length;
33024
- const unmountedViews = new Array(n);
33025
- for (var i = 0; i < n; i++) {
33026
- unmountedViews[i] = views[unmountedCids[i]];
33528
+ const unmountedViews = new Array(updates.unmountedList.length);
33529
+ const unmountedCids = updates.unmountedList.keys();
33530
+ let i = 0;
33531
+ for (const cid of unmountedCids) {
33532
+ // If the view is a placeholder, it won't be in the global views map
33533
+ // If the view is not a cell view, it won't be in the viewPlaceholders map
33534
+ unmountedViews[i++] = views[cid] || this._viewPlaceholders[cid];
33027
33535
  }
33028
33536
  return unmountedViews;
33029
33537
  },
33538
+ /**
33539
+ * @ignore This method returns an array of cellViewLike objects and therefore
33540
+ * is meant for internal/test use only.
33541
+ * The view placeholders are not exposed via public API.
33542
+ */
33030
33543
  getMountedViews: function () {
33031
33544
  const updates = this._updates;
33032
- const mountedCids = Object.keys(updates.mounted);
33033
- const n = mountedCids.length;
33034
- const mountedViews = new Array(n);
33035
- for (var i = 0; i < n; i++) {
33036
- mountedViews[i] = views[mountedCids[i]];
33545
+ const mountedViews = new Array(updates.mountedList.length);
33546
+ const mountedCids = updates.mountedList.keys();
33547
+ let i = 0;
33548
+ for (const cid of mountedCids) {
33549
+ mountedViews[i++] = views[cid] || this._viewPlaceholders[cid];
33037
33550
  }
33038
33551
  return mountedViews;
33039
33552
  },
33040
- checkUnmountedViews: function (viewportFn, opt) {
33553
+ checkUnmountedViews: function (visibilityCb, opt) {
33041
33554
  opt || (opt = {});
33042
33555
  var mountCount = 0;
33043
- if (typeof viewportFn !== 'function') viewportFn = null;
33556
+ if (typeof visibilityCb !== 'function') visibilityCb = null;
33044
33557
  var batchSize = 'mountBatchSize' in opt ? opt.mountBatchSize : Infinity;
33045
33558
  var updates = this._updates;
33046
- var unmountedCids = updates.unmountedCids;
33047
- var unmounted = updates.unmounted;
33048
- for (var i = 0, n = Math.min(unmountedCids.length, batchSize); i < n; i++) {
33049
- var cid = unmountedCids[i];
33050
- if (!(cid in unmounted)) continue;
33051
- var view = views[cid];
33052
- if (!view) continue;
33053
- if (view.DETACHABLE && viewportFn && !viewportFn.call(this, view, false, this)) {
33559
+ var unmountedList = updates.unmountedList;
33560
+ for (var i = 0, n = Math.min(unmountedList.length, batchSize); i < n; i++) {
33561
+ const {
33562
+ key: cid
33563
+ } = unmountedList.peekHead();
33564
+ let view = views[cid] || this._viewPlaceholders[cid];
33565
+ if (!view) {
33566
+ // This should not occur
33567
+ continue;
33568
+ }
33569
+ if (!this._evalCellVisibility(view, false, visibilityCb)) {
33054
33570
  // Push at the end of all unmounted ids, so this can be check later again
33055
- unmountedCids.push(cid);
33571
+ unmountedList.rotate();
33056
33572
  continue;
33057
33573
  }
33574
+ // Remove the view from the unmounted list
33575
+ const {
33576
+ value: prevFlag
33577
+ } = unmountedList.popHead();
33058
33578
  mountCount++;
33059
- var flag = this.registerMountedView(view);
33579
+ const flag = this.registerMountedView(view) | prevFlag;
33060
33580
  if (flag) this.scheduleViewUpdate(view, flag, view.UPDATE_PRIORITY, {
33061
33581
  mounting: true
33062
33582
  });
33063
33583
  }
33064
- // Get rid of views, that have been mounted
33065
- unmountedCids.splice(0, i);
33066
33584
  return mountCount;
33067
33585
  },
33068
- checkMountedViews: function (viewportFn, opt) {
33586
+ checkMountedViews: function (visibilityCb, opt) {
33069
33587
  opt || (opt = {});
33070
33588
  var unmountCount = 0;
33071
- if (typeof viewportFn !== 'function') return unmountCount;
33589
+ if (typeof visibilityCb !== 'function') return unmountCount;
33072
33590
  var batchSize = 'unmountBatchSize' in opt ? opt.unmountBatchSize : Infinity;
33073
33591
  var updates = this._updates;
33074
- var mountedCids = updates.mountedCids;
33075
- var mounted = updates.mounted;
33076
- for (var i = 0, n = Math.min(mountedCids.length, batchSize); i < n; i++) {
33077
- var cid = mountedCids[i];
33078
- if (!(cid in mounted)) continue;
33079
- var view = views[cid];
33080
- if (!view) continue;
33081
- if (!view.DETACHABLE || viewportFn.call(this, view, true, this)) {
33592
+ const mountedList = updates.mountedList;
33593
+ for (var i = 0, n = Math.min(mountedList.length, batchSize); i < n; i++) {
33594
+ const {
33595
+ key: cid
33596
+ } = mountedList.peekHead();
33597
+ const view = views[cid];
33598
+ if (!view) {
33599
+ // A view (not a cell view) has been removed from the paper.
33600
+ // Remove it from the mounted list and continue.
33601
+ mountedList.popHead();
33602
+ continue;
33603
+ }
33604
+ if (this._evalCellVisibility(view, true, visibilityCb)) {
33082
33605
  // Push at the end of all mounted ids, so this can be check later again
33083
- mountedCids.push(cid);
33606
+ mountedList.rotate();
33084
33607
  continue;
33085
33608
  }
33609
+ // Remove the view from the mounted list
33610
+ mountedList.popHead();
33086
33611
  unmountCount++;
33087
33612
  var flag = this.registerUnmountedView(view);
33088
- if (flag) this.detachView(view);
33613
+ if (flag) {
33614
+ this._hideView(view);
33615
+ }
33089
33616
  }
33090
- // Get rid of views, that have been unmounted
33091
- mountedCids.splice(0, i);
33092
33617
  return unmountCount;
33093
33618
  },
33094
33619
  checkViewVisibility: function (cellView, opt = {}) {
33095
- let viewportFn = 'viewport' in opt ? opt.viewport : this.options.viewport;
33096
- if (typeof viewportFn !== 'function') viewportFn = null;
33620
+ const visibilityCb = this._getCellVisibilityCallback(opt);
33097
33621
  const updates = this._updates;
33098
33622
  const {
33099
- mounted,
33100
- unmounted
33623
+ mountedList,
33624
+ unmountedList
33101
33625
  } = updates;
33102
- const visible = !cellView.DETACHABLE || !viewportFn || viewportFn.call(this, cellView, false, this);
33626
+ const visible = this._evalCellVisibility(cellView, false, visibilityCb);
33103
33627
  let isUnmounted = false;
33104
33628
  let isMounted = false;
33105
- if (cellView.cid in mounted && !visible) {
33629
+ if (mountedList.has(cellView.cid) && !visible) {
33106
33630
  const flag = this.registerUnmountedView(cellView);
33107
- if (flag) this.detachView(cellView);
33108
- const i = updates.mountedCids.indexOf(cellView.cid);
33109
- updates.mountedCids.splice(i, 1);
33631
+ if (flag) this._hideView(cellView);
33632
+ mountedList.delete(cellView.cid);
33110
33633
  isUnmounted = true;
33111
33634
  }
33112
- if (!isUnmounted && cellView.cid in unmounted && visible) {
33113
- const i = updates.unmountedCids.indexOf(cellView.cid);
33114
- updates.unmountedCids.splice(i, 1);
33115
- var flag = this.registerMountedView(cellView);
33635
+ if (!isUnmounted && unmountedList.has(cellView.cid) && visible) {
33636
+ const unmountedItem = unmountedList.get(cellView.cid);
33637
+ unmountedList.delete(cellView.cid);
33638
+ const flag = unmountedItem.value | this.registerMountedView(cellView);
33116
33639
  if (flag) this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, {
33117
33640
  mounting: true
33118
33641
  });
@@ -33123,24 +33646,64 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33123
33646
  unmounted: isUnmounted ? 1 : 0
33124
33647
  };
33125
33648
  },
33126
- checkViewport: function (opt) {
33127
- var passingOpt = defaults({}, opt, {
33649
+ /**
33650
+ * @public
33651
+ * Update the visibility of a single cell.
33652
+ */
33653
+ updateCellVisibility: function (cell, opt = {}) {
33654
+ const cellViewLike = this._getCellViewLike(cell);
33655
+ if (!cellViewLike) return;
33656
+ const stats = this.checkViewVisibility(cellViewLike, opt);
33657
+ // Note: `unmounted` views are removed immediately
33658
+ if (stats.mounted > 0) {
33659
+ // Mounting is scheduled. Run the update.
33660
+ // Note: the view might be a placeholder.
33661
+ this.requireView(cell, opt);
33662
+ }
33663
+ },
33664
+ /**
33665
+ * @public
33666
+ * Update the visibility of all cells.
33667
+ */
33668
+ updateCellsVisibility: function (opt = {}) {
33669
+ // Check the visibility of all cells and schedule their updates.
33670
+ this.scheduleCellsVisibilityUpdate(opt);
33671
+ // Perform the scheduled updates while avoiding re-evaluating the visibility.
33672
+ const keepCurrentVisibility = (_, isVisible) => isVisible;
33673
+ this.updateViews({
33674
+ ...opt,
33675
+ cellVisibility: keepCurrentVisibility
33676
+ });
33677
+ },
33678
+ /**
33679
+ * @protected
33680
+ * Run visibility checks for all cells and schedule their updates.
33681
+ */
33682
+ scheduleCellsVisibilityUpdate(opt) {
33683
+ const passingOpt = defaults({}, opt, {
33128
33684
  mountBatchSize: Infinity,
33129
33685
  unmountBatchSize: Infinity
33130
33686
  });
33131
- var viewportFn = 'viewport' in passingOpt ? passingOpt.viewport : this.options.viewport;
33132
- var unmountedCount = this.checkMountedViews(viewportFn, passingOpt);
33687
+ const visibilityCb = this._getCellVisibilityCallback(passingOpt);
33688
+ const unmountedCount = this.checkMountedViews(visibilityCb, passingOpt);
33133
33689
  if (unmountedCount > 0) {
33134
33690
  // Do not check views, that have been just unmounted and pushed at the end of the cids array
33135
- var unmountedCids = this._updates.unmountedCids;
33136
- passingOpt.mountBatchSize = Math.min(unmountedCids.length - unmountedCount, passingOpt.mountBatchSize);
33691
+ var unmountedList = this._updates.unmountedList;
33692
+ passingOpt.mountBatchSize = Math.min(unmountedList.length - unmountedCount, passingOpt.mountBatchSize);
33137
33693
  }
33138
- var mountedCount = this.checkUnmountedViews(viewportFn, passingOpt);
33694
+ const mountedCount = this.checkUnmountedViews(visibilityCb, passingOpt);
33139
33695
  return {
33140
33696
  mounted: mountedCount,
33141
33697
  unmounted: unmountedCount
33142
33698
  };
33143
33699
  },
33700
+ /**
33701
+ * @deprecated use `updateCellsVisibility` instead
33702
+ * This method will be renamed and made private in the future.
33703
+ */
33704
+ checkViewport: function (opt) {
33705
+ return this.scheduleCellsVisibilityUpdate(opt);
33706
+ },
33144
33707
  freeze: function (opt) {
33145
33708
  opt || (opt = {});
33146
33709
  var updates = this._updates;
@@ -33156,6 +33719,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33156
33719
  this.options.frozen = true;
33157
33720
  var id = updates.id;
33158
33721
  updates.id = null;
33722
+ if (!this.legacyMode) {
33723
+ // Make sure the `freeze()` method ends the idle state.
33724
+ updates.idle = false;
33725
+ }
33159
33726
  if (this.isAsync() && id) cancelFrame(id);
33160
33727
  },
33161
33728
  unfreeze: function (opt) {
@@ -33168,6 +33735,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33168
33735
  updates.freezeKey = null;
33169
33736
  // key passed, but the paper is already freezed
33170
33737
  if (key && key === freezeKey && updates.keyFrozen) return;
33738
+ updates.idle = false;
33171
33739
  if (this.isAsync()) {
33172
33740
  this.freeze();
33173
33741
  this.updateViewsAsync(opt);
@@ -33180,11 +33748,22 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33180
33748
  updates.sort = false;
33181
33749
  }
33182
33750
  },
33751
+ wakeUp: function () {
33752
+ if (!this.isIdle()) return;
33753
+ this.unfreeze(this._updates.idle.wakeUpOptions);
33754
+ },
33183
33755
  isAsync: function () {
33184
33756
  return !!this.options.async;
33185
33757
  },
33186
33758
  isFrozen: function () {
33187
- return !!this.options.frozen;
33759
+ return !!this.options.frozen && !this.isIdle();
33760
+ },
33761
+ isIdle: function () {
33762
+ if (this.legacyMode) {
33763
+ // Not implemented in the legacy mode.
33764
+ return false;
33765
+ }
33766
+ return !!(this._updates && this._updates.idle);
33188
33767
  },
33189
33768
  isExactSorting: function () {
33190
33769
  return this.options.sorting === sortingTypes.EXACT;
@@ -33464,21 +34043,65 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33464
34043
  }
33465
34044
  return restrictedArea;
33466
34045
  },
33467
- createViewForModel: function (cell) {
34046
+ _resolveCellViewPlaceholder: function (placeholder) {
34047
+ const {
34048
+ model,
34049
+ viewClass,
34050
+ cid
34051
+ } = placeholder;
34052
+ const view = this._initializeCellView(viewClass, model, cid);
34053
+ this._registerCellView(view);
34054
+ this._unregisterCellViewPlaceholder(placeholder);
34055
+ return view;
34056
+ },
34057
+ _registerCellViewPlaceholder: function (cell, cid = uniqueId('view')) {
34058
+ const ViewClass = this._resolveCellViewClass(cell);
34059
+ const placeholder = {
34060
+ // A tag to identify the placeholder from a CellView.
34061
+ [CELL_VIEW_PLACEHOLDER_MARKER]: true,
34062
+ cid,
34063
+ model: cell,
34064
+ DETACHABLE: true,
34065
+ viewClass: ViewClass,
34066
+ UPDATE_PRIORITY: ViewClass.prototype.UPDATE_PRIORITY
34067
+ };
34068
+ this._viewPlaceholders[cid] = placeholder;
34069
+ return placeholder;
34070
+ },
34071
+ _registerCellView: function (cellView) {
34072
+ cellView.paper = this;
34073
+ this._views[cellView.model.id] = cellView;
34074
+ },
34075
+ _unregisterCellViewPlaceholder: function (placeholder) {
34076
+ delete this._viewPlaceholders[placeholder.cid];
34077
+ },
34078
+ _initializeCellView: function (ViewClass, cell, cid) {
34079
+ const {
34080
+ options
34081
+ } = this;
34082
+ const {
34083
+ interactive,
34084
+ labelsLayer
34085
+ } = options;
34086
+ return new ViewClass({
34087
+ cid,
34088
+ model: cell,
34089
+ interactive,
34090
+ labelsLayer: labelsLayer === true ? LayersNames.LABELS : labelsLayer
34091
+ });
34092
+ },
34093
+ _resolveCellViewClass: function (cell) {
33468
34094
  const {
33469
34095
  options
33470
34096
  } = this;
34097
+ const {
34098
+ cellViewNamespace
34099
+ } = options;
34100
+ const type = cell.get('type') + 'View';
34101
+ const namespaceViewClass = getByPath(cellViewNamespace, type, '.');
33471
34102
  // A class taken from the paper options.
33472
- var optionalViewClass;
33473
-
33474
- // A default basic class (either dia.ElementView or dia.LinkView)
33475
- var defaultViewClass;
33476
-
33477
- // A special class defined for this model in the corresponding namespace.
33478
- // e.g. joint.shapes.standard.Rectangle searches for joint.shapes.standard.RectangleView
33479
- var namespace = options.cellViewNamespace;
33480
- var type = cell.get('type') + 'View';
33481
- var namespaceViewClass = getByPath(namespace, type, '.');
34103
+ let optionalViewClass;
34104
+ let defaultViewClass;
33482
34105
  if (cell.isLink()) {
33483
34106
  optionalViewClass = options.linkView;
33484
34107
  defaultViewClass = LinkView;
@@ -33486,7 +34109,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33486
34109
  optionalViewClass = options.elementView;
33487
34110
  defaultViewClass = ElementView;
33488
34111
  }
33489
-
33490
34112
  // a) the paper options view is a class (deprecated)
33491
34113
  // 1. search the namespace for a view
33492
34114
  // 2. if no view was found, use view from the paper options
@@ -33494,12 +34116,33 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33494
34116
  // 1. call the function from the paper options
33495
34117
  // 2. if no view was return, search the namespace for a view
33496
34118
  // 3. if no view was found, use the default
33497
- var ViewClass = optionalViewClass.prototype instanceof ViewBase ? namespaceViewClass || optionalViewClass : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass;
33498
- return new ViewClass({
33499
- model: cell,
33500
- interactive: options.interactive,
33501
- labelsLayer: options.labelsLayer === true ? LayersNames.LABELS : options.labelsLayer
33502
- });
34119
+ return optionalViewClass.prototype instanceof ViewBase ? namespaceViewClass || optionalViewClass : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass;
34120
+ },
34121
+ // Returns a CellView instance or its placeholder for the given cell.
34122
+ _getCellViewLike: function (cell) {
34123
+ let id;
34124
+ if (isString(cell) || isNumber(cell)) {
34125
+ // If the cell is a string or number, it is an id of the view.
34126
+ id = cell;
34127
+ } else if (cell) {
34128
+ // If the cell is an object, it should have an id property.
34129
+ id = cell.id;
34130
+ } else {
34131
+ // If the cell is falsy, return null.
34132
+ return null;
34133
+ }
34134
+ const view = this._views[id];
34135
+ if (view) return view;
34136
+
34137
+ // If the view is not found, it may be a placeholder
34138
+ const cid = this._idToCid[id];
34139
+ if (cid) {
34140
+ return this._viewPlaceholders[cid];
34141
+ }
34142
+ return null;
34143
+ },
34144
+ createViewForModel: function (cell, cid) {
34145
+ return this._initializeCellView(this._resolveCellViewClass(cell), cell, cid);
33503
34146
  },
33504
34147
  removeView: function (cell) {
33505
34148
  const {
@@ -33515,13 +34158,14 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33515
34158
  cid
33516
34159
  } = view;
33517
34160
  const {
33518
- mounted,
33519
- unmounted
34161
+ mountedList,
34162
+ unmountedList
33520
34163
  } = _updates;
33521
34164
  view.remove();
33522
34165
  delete _views[id];
33523
- delete mounted[cid];
33524
- delete unmounted[cid];
34166
+ delete this._idToCid[id];
34167
+ mountedList.delete(cid);
34168
+ unmountedList.delete(cid);
33525
34169
  }
33526
34170
  return view;
33527
34171
  },
@@ -33535,7 +34179,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33535
34179
  if (id in views) {
33536
34180
  view = views[id];
33537
34181
  if (view.model === cell) {
33538
- flag = view.FLAG_INSERT;
34182
+ flag = this.FLAG_INSERT;
33539
34183
  create = false;
33540
34184
  } else {
33541
34185
  // The view for this `id` already exist.
@@ -33545,13 +34189,42 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33545
34189
  }
33546
34190
  }
33547
34191
  if (create) {
33548
- view = views[id] = this.createViewForModel(cell);
33549
- view.paper = this;
33550
- flag = this.registerUnmountedView(view) | this.FLAG_INIT | view.getFlag(result(view, 'initFlag'));
34192
+ const {
34193
+ viewManagement
34194
+ } = this.options;
34195
+ const cid = uniqueId('view');
34196
+ this._idToCid[cell.id] = cid;
34197
+ if (viewManagement.lazyInitialize) {
34198
+ // Register only a placeholder for the view
34199
+ view = this._registerCellViewPlaceholder(cell, cid);
34200
+ flag = this.registerUnmountedView(view);
34201
+ } else {
34202
+ // Create a new view instance
34203
+ view = this.createViewForModel(cell, cid);
34204
+ this._registerCellView(view);
34205
+ flag = this.registerUnmountedView(view);
34206
+ // The newly created view needs to be initialized
34207
+ flag |= this.getCellViewInitFlag(view);
34208
+ }
34209
+ if (viewManagement.initializeUnmounted) {
34210
+ // Save the initialization flags for later and exit early
34211
+ this._mergeUnmountedViewScheduledUpdates(cid, flag);
34212
+ return view;
34213
+ }
33551
34214
  }
33552
34215
  this.requestViewUpdate(view, flag, view.UPDATE_PRIORITY, opt);
33553
34216
  return view;
33554
34217
  },
34218
+ // Update the view flags in the `unmountedList` using the bitwise OR operation
34219
+ _mergeUnmountedViewScheduledUpdates: function (cid, flag) {
34220
+ const {
34221
+ unmountedList
34222
+ } = this._updates;
34223
+ const unmountedItem = unmountedList.get(cid);
34224
+ if (unmountedItem) {
34225
+ unmountedItem.value |= flag;
34226
+ }
34227
+ },
33555
34228
  onImageDragStart: function () {
33556
34229
  // This is the only way to prevent image dragging in Firefox that works.
33557
34230
  // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help.
@@ -33561,11 +34234,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33561
34234
  resetViews: function (cells, opt) {
33562
34235
  opt || (opt = {});
33563
34236
  cells || (cells = []);
34237
+ // Allows to unfreeze normally while in the idle state using autoFreeze option
34238
+ const key = (this.legacyMode ? this.options.autoFreeze : this.isIdle()) ? null : 'reset';
33564
34239
  this._resetUpdates();
33565
34240
  // clearing views removes any event listeners
33566
34241
  this.removeViews();
33567
- // Allows to unfreeze normally while in the idle state using autoFreeze option
33568
- const key = this.options.autoFreeze ? null : 'reset';
33569
34242
  this.freeze({
33570
34243
  key
33571
34244
  });
@@ -33578,15 +34251,23 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33578
34251
  this.sortViews();
33579
34252
  },
33580
34253
  removeViews: function () {
33581
- invoke(this._views, 'remove');
34254
+ // Remove all views and their references from the paper.
34255
+ for (const id in this._views) {
34256
+ const view = this._views[id];
34257
+ if (view) {
34258
+ view.remove();
34259
+ }
34260
+ }
33582
34261
  this._views = {};
34262
+ this._viewPlaceholders = {};
34263
+ this._idToCid = {};
33583
34264
  },
33584
34265
  sortViews: function () {
33585
34266
  if (!this.isExactSorting()) {
33586
34267
  // noop
33587
34268
  return;
33588
34269
  }
33589
- if (this.isFrozen()) {
34270
+ if (this.isFrozen() || this.isIdle()) {
33590
34271
  // sort views once unfrozen
33591
34272
  this._updates.sort = true;
33592
34273
  return;
@@ -33625,9 +34306,54 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33625
34306
  }
33626
34307
  view.onMount(isInitialInsert);
33627
34308
  },
33628
- detachView(view) {
33629
- view.unmount();
33630
- view.onDetach();
34309
+ _hideView: function (viewLike) {
34310
+ if (!viewLike || viewLike[CELL_VIEW_PLACEHOLDER_MARKER]) {
34311
+ // A placeholder view was never mounted
34312
+ return;
34313
+ }
34314
+ if (viewLike[CELL_VIEW_MARKER]) {
34315
+ this._hideCellView(viewLike);
34316
+ } else {
34317
+ // A generic view that is not a cell view.
34318
+ viewLike.unmount();
34319
+ }
34320
+ },
34321
+ // If `cellVisibility` returns `false`, the view will be hidden using this method.
34322
+ _hideCellView: function (cellView) {
34323
+ if (this.options.viewManagement.disposeHidden) {
34324
+ if (this._disposeCellView(cellView)) return;
34325
+ }
34326
+ // Detach the view from the paper, but keep it in memory
34327
+ this._detachCellView(cellView);
34328
+ },
34329
+ _disposeCellView: function (cellView) {
34330
+ if (HighlighterView.has(cellView) || cellView.hasTools()) {
34331
+ // We currently do not dispose views which has a highlighter or tools attached
34332
+ // Note: Possible improvement would be to serialize highlighters/tools and
34333
+ // restore them on view re-mount.
34334
+ return false;
34335
+ }
34336
+ const cell = cellView.model;
34337
+ // Remove the view from the paper and dispose it
34338
+ cellView.remove();
34339
+ delete this._views[cell.id];
34340
+ this._registerCellViewPlaceholder(cell, cellView.cid);
34341
+ return true;
34342
+ },
34343
+ // Dispose (release resources) all hidden views.
34344
+ disposeHiddenCellViews: function () {
34345
+ // Only cell views can be in the unmounted list (not in the legacy mode).
34346
+ if (this.legacyMode) return;
34347
+ const unmountedCids = this._updates.unmountedList.keys();
34348
+ for (const cid of unmountedCids) {
34349
+ const cellView = views[cid];
34350
+ cellView && this._disposeCellView(cellView);
34351
+ }
34352
+ },
34353
+ // Detach a view from the paper, but keep it in memory.
34354
+ _detachCellView(cellView) {
34355
+ cellView.unmount();
34356
+ cellView.onDetach();
33631
34357
  },
33632
34358
  // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also
33633
34359
  // be a selector or a jQuery object.
@@ -33638,9 +34364,31 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33638
34364
  return undefined;
33639
34365
  },
33640
34366
  // Find a view for a model `cell`. `cell` can also be a string or number representing a model `id`.
33641
- findViewByModel: function (cell) {
33642
- var id = isString(cell) || isNumber(cell) ? cell : cell && cell.id;
33643
- return this._views[id];
34367
+ findViewByModel: function (cellOrId) {
34368
+ const cellViewLike = this._getCellViewLike(cellOrId);
34369
+ if (!cellViewLike) return undefined;
34370
+ if (cellViewLike[CELL_VIEW_MARKER]) {
34371
+ // If the view is not a placeholder, return it directly
34372
+ return cellViewLike;
34373
+ }
34374
+ // We do not expose placeholder views directly. We resolve them before returning.
34375
+ const cellView = this._resolveCellViewPlaceholder(cellViewLike);
34376
+ const flag = this.getCellViewInitFlag(cellView);
34377
+ if (this.isViewMounted(cellView)) {
34378
+ // The view was acting as a placeholder and is already present in the `mounted` list,
34379
+ // indicating that its visibility has been checked, but the update hasn't occurred yet.
34380
+ // Placeholders are resolved during the update routine. Since we're handling it
34381
+ // manually here, we must ensure the view is properly initialized on the next update.
34382
+ this.scheduleViewUpdate(cellView, flag, cellView.UPDATE_PRIORITY, {
34383
+ // It's important to run in isolation to avoid triggering the update of
34384
+ // connected links
34385
+ isolate: true
34386
+ });
34387
+ } else {
34388
+ // Update the flags in the `unmounted` list
34389
+ this._mergeUnmountedViewScheduledUpdates(cellView.cid, flag);
34390
+ }
34391
+ return cellView;
33644
34392
  },
33645
34393
  // Find all views at given point
33646
34394
  findViewsFromPoint: function (p) {
@@ -33687,6 +34435,93 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
33687
34435
  // because the `strict` option works differently for querying at a point
33688
34436
  extArea => this.model.findCellsInArea(extArea), opt);
33689
34437
  },
34438
+ findClosestMagnetToPoint: function (point, options = {}) {
34439
+ let minDistance = Number.MAX_SAFE_INTEGER;
34440
+ let bestPriority = -Infinity;
34441
+ const pointer = new Point(point);
34442
+ const radius = options.radius || Number.MAX_SAFE_INTEGER;
34443
+ const viewsInArea = this.findCellViewsInArea({
34444
+ x: pointer.x - radius,
34445
+ y: pointer.y - radius,
34446
+ width: 2 * radius,
34447
+ height: 2 * radius
34448
+ }, options.findInAreaOptions);
34449
+ // Enable all connections by default
34450
+ const filterFn = typeof options.filter === 'function' ? options.filter : null;
34451
+ let closestView = null;
34452
+ let closestMagnet = null;
34453
+
34454
+ // Note: If snapRadius is smaller than magnet size, views will not be found.
34455
+ viewsInArea.forEach(view => {
34456
+ const candidates = [];
34457
+ const {
34458
+ model
34459
+ } = view;
34460
+ // skip connecting to the element in case '.': { magnet: false } attribute present
34461
+ if (view.el.getAttribute('magnet') !== 'false') {
34462
+ if (model.isLink()) {
34463
+ const connection = view.getConnection();
34464
+ candidates.push({
34465
+ // find distance from the closest point of a link to pointer coordinates
34466
+ priority: 0,
34467
+ distance: connection.closestPoint(pointer).squaredDistance(pointer),
34468
+ magnet: view.el
34469
+ });
34470
+ } else {
34471
+ candidates.push({
34472
+ // Set the priority to the level of nested elements of the model
34473
+ // To ensure that the embedded cells get priority over the parent cells
34474
+ priority: model.getAncestors().length,
34475
+ // find distance from the center of the model to pointer coordinates
34476
+ distance: model.getBBox().center().squaredDistance(pointer),
34477
+ magnet: view.el
34478
+ });
34479
+ }
34480
+ }
34481
+ view.$('[magnet]').toArray().forEach(magnet => {
34482
+ const magnetBBox = view.getNodeBBox(magnet);
34483
+ let magnetDistance = magnetBBox.pointNearestToPoint(pointer).squaredDistance(pointer);
34484
+ if (magnetBBox.containsPoint(pointer)) {
34485
+ // Pointer sits inside this magnet.
34486
+ // Push its distance far into the negative range so any
34487
+ // "under-pointer" magnet outranks magnets that are only nearby
34488
+ // (positive distance) and every non-magnet candidate.
34489
+ // We add the original distance back to keep ordering among
34490
+ // overlapping magnets: the one whose border is closest to the
34491
+ // pointer (smaller original distance) still wins.
34492
+ magnetDistance = -Number.MAX_SAFE_INTEGER + magnetDistance;
34493
+ }
34494
+
34495
+ // Check if magnet is inside the snap radius.
34496
+ if (magnetDistance <= radius * radius) {
34497
+ candidates.push({
34498
+ // Give magnets priority over other candidates.
34499
+ priority: Number.MAX_SAFE_INTEGER,
34500
+ distance: magnetDistance,
34501
+ magnet
34502
+ });
34503
+ }
34504
+ });
34505
+ candidates.forEach(candidate => {
34506
+ const {
34507
+ magnet,
34508
+ distance,
34509
+ priority
34510
+ } = candidate;
34511
+ const isBetterCandidate = priority > bestPriority || priority === bestPriority && distance < minDistance;
34512
+ if (isBetterCandidate && (!filterFn || filterFn(view, magnet))) {
34513
+ bestPriority = priority;
34514
+ minDistance = distance;
34515
+ closestView = view;
34516
+ closestMagnet = magnet;
34517
+ }
34518
+ });
34519
+ });
34520
+ return closestView ? {
34521
+ view: closestView,
34522
+ magnet: closestMagnet
34523
+ } : null;
34524
+ },
33690
34525
  _findInExtendedArea: function (area, findCellsFn, opt = {}) {
33691
34526
  const {
33692
34527
  buffer = this.DEFAULT_FIND_BUFFER
@@ -34069,8 +34904,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
34069
34904
  if (mousemoved <= this.options.moveThreshold) return;
34070
34905
  evt = normalizeEvent(evt);
34071
34906
  var localPoint = this.snapToGrid(evt.clientX, evt.clientY);
34072
- var view = data.sourceView;
34907
+ let view = data.sourceView;
34073
34908
  if (view) {
34909
+ // The view could have been disposed during dragging
34910
+ // e.g. dragged outside of the viewport and hidden
34911
+ view = this.findViewByModel(view.model);
34074
34912
  view.pointermove(evt, localPoint.x, localPoint.y);
34075
34913
  } else {
34076
34914
  this.trigger('blank:pointermove', evt, localPoint.x, localPoint.y);
@@ -34081,8 +34919,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
34081
34919
  this.undelegateDocumentEvents();
34082
34920
  var normalizedEvt = normalizeEvent(evt);
34083
34921
  var localPoint = this.snapToGrid(normalizedEvt.clientX, normalizedEvt.clientY);
34084
- var view = this.eventData(evt).sourceView;
34922
+ let view = this.eventData(evt).sourceView;
34085
34923
  if (view) {
34924
+ // The view could have been disposed during dragging
34925
+ // e.g. dragged outside of the viewport and hidden
34926
+ view = this.findViewByModel(view.model);
34086
34927
  view.pointerup(normalizedEvt, localPoint.x, localPoint.y);
34087
34928
  } else {
34088
34929
  this.trigger('blank:pointerup', normalizedEvt, localPoint.x, localPoint.y);
@@ -34948,6 +35789,9 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
34948
35789
  isVisible: function () {
34949
35790
  return !!this._visible;
34950
35791
  },
35792
+ isOverlay: function () {
35793
+ return !!this.parentView && this.parentView.hasLayer();
35794
+ },
34951
35795
  focus: function () {
34952
35796
  var opacity = this.options.focusOpacity;
34953
35797
  if (isFinite(opacity)) this.el.style.opacity = opacity;
@@ -35093,6 +35937,15 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
35093
35937
  }
35094
35938
  this.tools = null;
35095
35939
  },
35940
+ getLayer() {
35941
+ const {
35942
+ layer = LayersNames.TOOLS
35943
+ } = this.options;
35944
+ return layer;
35945
+ },
35946
+ hasLayer() {
35947
+ return !!this.getLayer();
35948
+ },
35096
35949
  mount: function () {
35097
35950
  const {
35098
35951
  options,
@@ -35100,12 +35953,11 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
35100
35953
  } = this;
35101
35954
  const {
35102
35955
  relatedView,
35103
- layer = LayersNames.TOOLS,
35104
35956
  z
35105
35957
  } = options;
35106
35958
  if (relatedView) {
35107
- if (layer) {
35108
- relatedView.paper.getLayerView(layer).insertSortedNode(el, z);
35959
+ if (this.hasLayer()) {
35960
+ relatedView.paper.getLayerView(this.getLayer()).insertSortedNode(el, z);
35109
35961
  } else {
35110
35962
  relatedView.el.appendChild(el);
35111
35963
  }
@@ -35116,6 +35968,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
35116
35968
 
35117
35969
  var index$2 = {
35118
35970
  __proto__: null,
35971
+ CELL_VIEW_MARKER: CELL_VIEW_MARKER,
35119
35972
  Cell: Cell,
35120
35973
  CellView: CellView,
35121
35974
  Element: Element$1,
@@ -35464,12 +36317,57 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
35464
36317
  VertexHandle: VertexHandle // keep as class property
35465
36318
  });
35466
36319
 
35467
- function getViewBBox(view, useModelGeometry) {
36320
+ /**
36321
+ * Common helper for getting a cell view’s bounding box,
36322
+ * configurable with `useModelGeometry`, `relative`, and `el`.
36323
+ */
36324
+ function getViewBBox(view, {
36325
+ useModelGeometry = false,
36326
+ relative = false,
36327
+ el = view.el
36328
+ } = {}) {
35468
36329
  const {
35469
36330
  model
35470
36331
  } = view;
35471
- if (useModelGeometry) return model.getBBox();
35472
- return model.isLink() ? view.getConnection().bbox() : view.getNodeUnrotatedBBox(view.el);
36332
+ let bbox;
36333
+ if (useModelGeometry) {
36334
+ // cell model bbox
36335
+ bbox = model.getBBox();
36336
+ } else if (model.isLink()) {
36337
+ // link view bbox
36338
+ bbox = view.getConnection().bbox();
36339
+ } else {
36340
+ // element view bbox
36341
+ bbox = view.getNodeUnrotatedBBox(el);
36342
+ }
36343
+ if (relative) {
36344
+ // Relative to the element position.
36345
+ const position = model.position();
36346
+ bbox.x -= position.x;
36347
+ bbox.y -= position.y;
36348
+ }
36349
+ return bbox;
36350
+ }
36351
+
36352
+ /**
36353
+ * Retrieves the tool options.
36354
+ * Automatically overrides `useModelGeometry` and `rotate`
36355
+ * if the tool is positioned relative to the element.
36356
+ */
36357
+ function getToolOptions(toolView) {
36358
+ // Positioning is relative if the tool is drawn within the element view.
36359
+ const relative = !toolView.isOverlay();
36360
+ const {
36361
+ useModelGeometry,
36362
+ rotate,
36363
+ ...otherOptions
36364
+ } = toolView.options;
36365
+ return {
36366
+ ...otherOptions,
36367
+ useModelGeometry: useModelGeometry || relative,
36368
+ rotate: rotate || relative,
36369
+ relative
36370
+ };
35473
36371
  }
35474
36372
  function getAnchor(coords, view, magnet) {
35475
36373
  // take advantage of an existing logic inside of the
@@ -36307,29 +37205,40 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
36307
37205
  return this;
36308
37206
  },
36309
37207
  updateHandle: function (handleNode) {
37208
+ const {
37209
+ options: {
37210
+ handleAttributes
37211
+ }
37212
+ } = this;
37213
+ handleNode.setAttribute('transform', this.getHandleTransformString());
37214
+ if (handleAttributes) {
37215
+ for (let attrName in handleAttributes) {
37216
+ handleNode.setAttribute(attrName, handleAttributes[attrName]);
37217
+ }
37218
+ }
37219
+ },
37220
+ getHandleTransformString() {
36310
37221
  const {
36311
37222
  relatedView,
36312
37223
  options
36313
37224
  } = this;
37225
+ const {
37226
+ scale
37227
+ } = options;
36314
37228
  const {
36315
37229
  model
36316
37230
  } = relatedView;
36317
37231
  const relativePos = this.getPosition(relatedView, this);
36318
- const absolutePos = model.getAbsolutePointFromRelative(relativePos);
36319
- const {
36320
- handleAttributes,
36321
- scale
36322
- } = options;
36323
- let transformString = `translate(${absolutePos.x},${absolutePos.y})`;
37232
+ const translate = this.isOverlay()
37233
+ // The tool is rendered in the coordinate system of the paper
37234
+ ? model.getAbsolutePointFromRelative(relativePos)
37235
+ // The tool is rendered in the coordinate system of the relatedView
37236
+ : relativePos;
37237
+ let transformString = `translate(${translate.x},${translate.y})`;
36324
37238
  if (scale) {
36325
37239
  transformString += ` scale(${scale})`;
36326
37240
  }
36327
- handleNode.setAttribute('transform', transformString);
36328
- if (handleAttributes) {
36329
- for (let attrName in handleAttributes) {
36330
- handleNode.setAttribute(attrName, handleAttributes[attrName]);
36331
- }
36332
- }
37241
+ return transformString;
36333
37242
  },
36334
37243
  updateExtras: function (extrasNode) {
36335
37244
  const {
@@ -36337,19 +37246,40 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
36337
37246
  options
36338
37247
  } = this;
36339
37248
  const {
36340
- selector
36341
- } = this.options;
37249
+ selector,
37250
+ relative,
37251
+ useModelGeometry
37252
+ } = getToolOptions(this);
36342
37253
  if (!selector) {
37254
+ // Hide the extras if no selector is given.
36343
37255
  this.toggleExtras(false);
36344
37256
  return;
36345
37257
  }
36346
- const magnet = relatedView.findNode(selector);
36347
- if (!magnet) throw new Error('Control: invalid selector.');
37258
+ // Get the size for the extras rectangle and update it.
37259
+ let bbox;
37260
+ if (useModelGeometry) {
37261
+ if (selector !== 'root') {
37262
+ // A selector other than null or `root` was provided.
37263
+ console.warn('Control: selector will be ignored when `useModelGeometry` is used.');
37264
+ }
37265
+ bbox = getViewBBox(relatedView, {
37266
+ useModelGeometry,
37267
+ relative
37268
+ });
37269
+ } else {
37270
+ // The reference node for calculating the bounding box of the extras.
37271
+ const el = relatedView.findNode(selector);
37272
+ if (!el) throw new Error('Control: invalid selector.');
37273
+ bbox = getViewBBox(relatedView, {
37274
+ el
37275
+ });
37276
+ }
36348
37277
  let padding = options.padding;
36349
37278
  if (!isFinite(padding)) padding = 0;
36350
- const bbox = relatedView.getNodeUnrotatedBBox(magnet);
36351
37279
  const model = relatedView.model;
36352
- const angle = model.angle();
37280
+ // With relative positioning, rotation is implicit
37281
+ // (the tool rotates along with the element).
37282
+ const angle = relative ? 0 : model.angle();
36353
37283
  const center = bbox.center();
36354
37284
  if (angle) center.rotate(model.getCenter(), -angle);
36355
37285
  bbox.inflate(padding);
@@ -36560,8 +37490,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
36560
37490
  },
36561
37491
  getElementMatrix() {
36562
37492
  const {
36563
- relatedView: view,
36564
- options
37493
+ relatedView: view
36565
37494
  } = this;
36566
37495
  let {
36567
37496
  x = 0,
@@ -36569,9 +37498,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
36569
37498
  offset = {},
36570
37499
  useModelGeometry,
36571
37500
  rotate,
36572
- scale
36573
- } = options;
36574
- let bbox = getViewBBox(view, useModelGeometry);
37501
+ scale,
37502
+ relative
37503
+ } = getToolOptions(this);
37504
+ let bbox = getViewBBox(view, {
37505
+ useModelGeometry,
37506
+ relative
37507
+ });
36575
37508
  const angle = view.model.angle();
36576
37509
  if (!rotate) bbox = bbox.bbox(angle);
36577
37510
  const {
@@ -36589,7 +37522,9 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
36589
37522
  y = Number(evalCalcExpression(y, bbox));
36590
37523
  }
36591
37524
  let matrix = V.createSVGMatrix().translate(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2);
36592
- if (rotate) matrix = matrix.rotate(angle);
37525
+ // With relative positioning, rotation is implicit
37526
+ // (the tool rotates along with the element).
37527
+ if (rotate && !relative) matrix = matrix.rotate(angle);
36593
37528
  matrix = matrix.translate(x + offsetX - bbox.width / 2, y + offsetY - bbox.height / 2);
36594
37529
  if (scale) matrix = matrix.scale(scale);
36595
37530
  return matrix;
@@ -36687,26 +37622,32 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
36687
37622
  update: function () {
36688
37623
  const {
36689
37624
  relatedView: view,
36690
- options,
36691
37625
  vel
36692
37626
  } = this;
36693
37627
  const {
36694
37628
  useModelGeometry,
36695
- rotate
36696
- } = options;
36697
- const padding = normalizeSides(options.padding);
36698
- let bbox = getViewBBox(view, useModelGeometry).moveAndExpand({
36699
- x: -padding.left,
36700
- y: -padding.top,
36701
- width: padding.left + padding.right,
36702
- height: padding.top + padding.bottom
37629
+ rotate,
37630
+ relative,
37631
+ padding
37632
+ } = getToolOptions(this);
37633
+ const normalizedPadding = normalizeSides(padding);
37634
+ let bbox = getViewBBox(view, {
37635
+ useModelGeometry,
37636
+ relative
37637
+ }).moveAndExpand({
37638
+ x: -normalizedPadding.left,
37639
+ y: -normalizedPadding.top,
37640
+ width: normalizedPadding.left + normalizedPadding.right,
37641
+ height: normalizedPadding.top + normalizedPadding.bottom
36703
37642
  });
36704
- var model = view.model;
36705
- if (model.isElement()) {
36706
- var angle = model.angle();
37643
+ const model = view.model;
37644
+ // With relative positioning, rotation is implicit
37645
+ // (the tool rotates along with the element).
37646
+ if (model.isElement() && !relative) {
37647
+ const angle = model.angle();
36707
37648
  if (angle) {
36708
37649
  if (rotate) {
36709
- var origin = model.getCenter();
37650
+ const origin = model.getCenter();
36710
37651
  vel.rotate(angle, origin.x, origin.y, {
36711
37652
  absolute: true
36712
37653
  });
@@ -36932,13 +37873,16 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
36932
37873
  getTrackMatrix() {
36933
37874
  return V.createSVGMatrix();
36934
37875
  },
37876
+ getTrackMatrixAbsolute() {
37877
+ return this.getTrackMatrix();
37878
+ },
36935
37879
  getTrackRatioFromEvent(evt) {
36936
37880
  const {
36937
37881
  relatedView,
36938
37882
  trackPath
36939
37883
  } = this;
36940
37884
  const localPoint = relatedView.paper.clientToLocalPoint(evt.clientX, evt.clientY);
36941
- const trackPoint = V.transformPoint(localPoint, this.getTrackMatrix().inverse());
37885
+ const trackPoint = V.transformPoint(localPoint, this.getTrackMatrixAbsolute().inverse());
36942
37886
  return trackPath.closestPointLength(trackPoint);
36943
37887
  },
36944
37888
  canShowButton() {
@@ -36992,32 +37936,40 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
36992
37936
  const HoverConnect = HoverConnect$1.extend({
36993
37937
  getTrackPath() {
36994
37938
  const {
36995
- relatedView: view,
36996
- options
37939
+ relatedView: view
36997
37940
  } = this;
36998
37941
  let {
36999
37942
  useModelGeometry,
37943
+ relative,
37000
37944
  trackPath = 'M 0 0 H calc(w) V calc(h) H 0 Z'
37001
- } = options;
37945
+ } = getToolOptions(this);
37002
37946
  if (typeof trackPath === 'function') {
37003
37947
  trackPath = trackPath.call(this, view);
37004
37948
  }
37005
37949
  if (isCalcExpression(trackPath)) {
37006
- const bbox = getViewBBox(view, useModelGeometry);
37950
+ const bbox = getViewBBox(view, {
37951
+ useModelGeometry,
37952
+ relative
37953
+ });
37007
37954
  trackPath = evalCalcExpression(trackPath, bbox);
37008
37955
  }
37009
37956
  return new Path$1(V.normalizePathData(trackPath));
37010
37957
  },
37011
37958
  getTrackMatrix() {
37959
+ if (this.isOverlay()) return this.getTrackMatrixAbsolute();
37960
+ return V.createSVGMatrix();
37961
+ },
37962
+ getTrackMatrixAbsolute() {
37012
37963
  const {
37013
- relatedView: view,
37014
- options
37964
+ relatedView: view
37015
37965
  } = this;
37016
37966
  let {
37017
37967
  useModelGeometry,
37018
37968
  rotate
37019
- } = options;
37020
- let bbox = getViewBBox(view, useModelGeometry);
37969
+ } = getToolOptions(this);
37970
+ let bbox = getViewBBox(view, {
37971
+ useModelGeometry
37972
+ });
37021
37973
  const angle = view.model.angle();
37022
37974
  if (!rotate) bbox = bbox.bbox(angle);
37023
37975
  let matrix = V.createSVGMatrix().translate(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2);
@@ -37037,7 +37989,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
37037
37989
  Remove: Remove
37038
37990
  };
37039
37991
 
37040
- var version = "4.2.0-alpha.0";
37992
+ var version = "4.2.0-alpha.1";
37041
37993
 
37042
37994
  const Vectorizer = V;
37043
37995
  const layout$1 = {