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