@joint/core 4.1.3 → 4.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +4 -2
  2. package/dist/geometry.js +129 -124
  3. package/dist/geometry.min.js +4 -3
  4. package/dist/joint.d.ts +352 -160
  5. package/dist/joint.js +3654 -2191
  6. package/dist/joint.min.js +4 -3
  7. package/dist/joint.nowrap.js +3653 -2188
  8. package/dist/joint.nowrap.min.js +4 -3
  9. package/dist/vectorizer.js +489 -279
  10. package/dist/vectorizer.min.js +4 -3
  11. package/dist/version.mjs +1 -1
  12. package/package.json +33 -27
  13. package/src/V/create.mjs +51 -0
  14. package/src/V/index.mjs +89 -159
  15. package/src/V/namespace.mjs +9 -0
  16. package/src/V/transform.mjs +183 -0
  17. package/src/V/traverse.mjs +16 -0
  18. package/src/alg/Deque.mjs +126 -0
  19. package/src/anchors/index.mjs +140 -33
  20. package/src/cellTools/Boundary.mjs +15 -13
  21. package/src/cellTools/Button.mjs +7 -5
  22. package/src/cellTools/Control.mjs +38 -15
  23. package/src/cellTools/HoverConnect.mjs +5 -1
  24. package/src/cellTools/helpers.mjs +44 -3
  25. package/src/config/index.mjs +8 -0
  26. package/src/connectionPoints/index.mjs +24 -9
  27. package/src/connectionStrategies/index.mjs +1 -1
  28. package/src/connectors/jumpover.mjs +1 -1
  29. package/src/dia/Cell.mjs +32 -12
  30. package/src/dia/CellView.mjs +53 -38
  31. package/src/dia/Element.mjs +81 -35
  32. package/src/dia/ElementView.mjs +2 -1
  33. package/src/dia/HighlighterView.mjs +54 -11
  34. package/src/dia/LinkView.mjs +118 -98
  35. package/src/dia/Paper.mjs +831 -231
  36. package/src/dia/PaperLayer.mjs +9 -2
  37. package/src/dia/ToolView.mjs +4 -0
  38. package/src/dia/ToolsView.mjs +12 -3
  39. package/src/dia/attributes/text.mjs +16 -5
  40. package/src/dia/layers/GridLayer.mjs +5 -0
  41. package/src/dia/ports.mjs +344 -111
  42. package/src/elementTools/HoverConnect.mjs +14 -8
  43. package/src/env/index.mjs +7 -4
  44. package/src/g/rect.mjs +7 -0
  45. package/src/highlighters/stroke.mjs +1 -1
  46. package/src/layout/ports/port.mjs +30 -15
  47. package/src/layout/ports/portLabel.mjs +1 -1
  48. package/src/linkAnchors/index.mjs +2 -2
  49. package/src/linkTools/Anchor.mjs +2 -2
  50. package/src/linkTools/Vertices.mjs +4 -6
  51. package/src/mvc/View.mjs +4 -0
  52. package/src/mvc/ViewBase.mjs +1 -1
  53. package/src/util/util.mjs +1 -1
  54. package/src/util/utilHelpers.mjs +2 -0
  55. package/types/geometry.d.ts +65 -59
  56. package/types/joint.d.ts +278 -102
  57. package/types/vectorizer.d.ts +11 -1
  58. package/src/V/annotation.mjs +0 -0
@@ -21,7 +21,7 @@ function pinnedElementEnd(relative, end, view, magnet, coords) {
21
21
 
22
22
  var angle = view.model.angle();
23
23
  var bbox = view.getNodeUnrotatedBBox(magnet);
24
- var origin = view.model.getBBox().center();
24
+ var origin = view.model.getCenter();
25
25
  coords.rotate(origin, angle);
26
26
  var dx = coords.x - bbox.x;
27
27
  var dy = coords.y - bbox.y;
@@ -337,7 +337,7 @@ function buildRoundedSegment(offset, path, curr, prev, next) {
337
337
  * @property {number} size optional size of a jump arc
338
338
  * @return {string} created `D` attribute of SVG path
339
339
  */
340
- export const jumpover = function(sourcePoint, targetPoint, route, opt) { // eslint-disable-line max-params
340
+ export const jumpover = function(sourcePoint, targetPoint, route, opt) {
341
341
 
342
342
  setupUpdating(this);
343
343
 
package/src/dia/Cell.mjs CHANGED
@@ -32,14 +32,14 @@ import { Model } from '../mvc/Model.mjs';
32
32
  import { cloneCells } from '../util/cloneCells.mjs';
33
33
  import { attributes } from './attributes/index.mjs';
34
34
  import * as g from '../g/index.mjs';
35
-
35
+ import { config } from '../config/index.mjs';
36
36
 
37
37
  // Cell base model.
38
38
  // --------------------------
39
39
 
40
40
  const attributesMerger = function(a, b) {
41
41
  if (Array.isArray(a)) {
42
- return b;
42
+ return cloneDeep(b);
43
43
  }
44
44
  };
45
45
 
@@ -78,12 +78,20 @@ export const Cell = Model.extend({
78
78
  if ((defaults = result(this, 'defaults'))) {
79
79
  //<custom code>
80
80
  // Replaced the call to _.defaults with util.merge.
81
- const customizer = (options && options.mergeArrays === true) ? false : attributesMerger;
81
+ const customizer = (options && options.mergeArrays === true)
82
+ ? false
83
+ : config.cellDefaultsMergeStrategy || attributesMerger;
82
84
  attrs = merge({}, defaults, attrs, customizer);
83
85
  //</custom code>
84
86
  }
85
87
  this.set(attrs, options);
86
88
  this.changed = {};
89
+ if (options && options.portLayoutNamespace) {
90
+ this.portLayoutNamespace = options.portLayoutNamespace;
91
+ }
92
+ if (options && options.portLabelLayoutNamespace) {
93
+ this.portLabelLayoutNamespace = options.portLabelLayoutNamespace;
94
+ }
87
95
  this.initialize.apply(this, arguments);
88
96
  },
89
97
 
@@ -543,7 +551,11 @@ export const Cell = Model.extend({
543
551
  if (!opt.deep) {
544
552
  // Shallow cloning.
545
553
 
546
- var clone = Model.prototype.clone.apply(this, arguments);
554
+ // Preserve the original's `portLayoutNamespace` and `portLabelLayoutNamespace`.
555
+ const clone = new this.constructor(this.attributes, {
556
+ portLayoutNamespace: this.portLayoutNamespace,
557
+ portLabelLayoutNamespace: this.portLabelLayoutNamespace
558
+ });
547
559
  // We don't want the clone to have the same ID as the original.
548
560
  clone.set(this.getIdAttribute(), this.generateId());
549
561
  // A shallow cloned element does not carry over the original embeds.
@@ -557,7 +569,7 @@ export const Cell = Model.extend({
557
569
  } else {
558
570
  // Deep cloning.
559
571
 
560
- // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells.
572
+ // For a deep clone, simply call `util.cloneCells()` with the cell and all its embedded cells.
561
573
  return toArray(cloneCells([this].concat(this.getEmbeddedCells({ deep: true }))));
562
574
  }
563
575
  },
@@ -630,7 +642,7 @@ export const Cell = Model.extend({
630
642
  options.rewrite && unsetByPath(baseAttributes, path, '/');
631
643
 
632
644
  // Merge update with the model attributes.
633
- var attributes = merge(baseAttributes, update);
645
+ var attributes = merge(baseAttributes, update, config.cellMergeStrategy);
634
646
  // Finally, set the property to the updated attributes.
635
647
  return this.set(property, attributes[property], options);
636
648
 
@@ -653,7 +665,11 @@ export const Cell = Model.extend({
653
665
  const changedAttributes = {};
654
666
  for (const key in props) {
655
667
  // Merging the values of changed attributes with the current ones.
656
- const { changedValue } = merge({}, { changedValue: this.attributes[key] }, { changedValue: props[key] });
668
+ const { changedValue } = merge(
669
+ merge({}, { changedValue: this.attributes[key] }),
670
+ { changedValue: props[key] },
671
+ config.cellMergeStrategy
672
+ );
657
673
  changedAttributes[key] = changedValue;
658
674
  }
659
675
 
@@ -921,9 +937,13 @@ export const Cell = Model.extend({
921
937
  return new g.Rect(0, 0, 0, 0);
922
938
  },
923
939
 
940
+ getCenter: function() {
941
+ return this.getBBox().center();
942
+ },
943
+
924
944
  getPointRotatedAroundCenter(angle, x, y) {
925
945
  const point = new g.Point(x, y);
926
- if (angle) point.rotate(this.getBBox().center(), angle);
946
+ if (angle) point.rotate(this.getCenter(), angle);
927
947
  return point;
928
948
  },
929
949
 
@@ -948,9 +968,10 @@ export const Cell = Model.extend({
948
968
 
949
969
  getAttributeDefinition: function(attrName) {
950
970
 
951
- var defNS = this.attributes;
952
- var globalDefNS = attributes;
953
- return (defNS && defNS[attrName]) || globalDefNS[attrName];
971
+ const defNS = this.attributes;
972
+ const globalDefNS = attributes;
973
+ const definition = (defNS && defNS[attrName]) || globalDefNS[attrName];
974
+ return definition !== undefined ? definition : null;
954
975
  },
955
976
 
956
977
  define: function(type, defaults, protoProps, staticProps) {
@@ -969,4 +990,3 @@ export const Cell = Model.extend({
969
990
  return Cell;
970
991
  }
971
992
  });
972
-
@@ -795,7 +795,15 @@ export const CellView = View.extend({
795
795
  getNodeBoundingRect: function(magnet) {
796
796
 
797
797
  var metrics = this.nodeCache(magnet);
798
- if (metrics.boundingRect === undefined) metrics.boundingRect = V(magnet).getBBox();
798
+ if (metrics.boundingRect === undefined) {
799
+ const { measureNode } = this.paper.options;
800
+ if (typeof measureNode === 'function') {
801
+ // Measure the node bounding box using the paper's measureNode method.
802
+ metrics.boundingRect = measureNode(magnet, this);
803
+ } else {
804
+ metrics.boundingRect = V(magnet).getBBox();
805
+ }
806
+ }
799
807
  return new Rect(metrics.boundingRect);
800
808
  },
801
809
 
@@ -810,7 +818,12 @@ export const CellView = View.extend({
810
818
  } else {
811
819
  target = el;
812
820
  }
813
- metrics.magnetMatrix = V(magnet).getTransformToElement(target);
821
+ metrics.magnetMatrix = V(magnet).getTransformToElement(target, {
822
+ // We use `safe` mode if the magnet is not visible (not in the DOM render tree).
823
+ // The browser would not be able to calculate the transformation matrix
824
+ // using `getScreenCTM()` method.
825
+ safe: !magnet.checkVisibility()
826
+ });
814
827
  }
815
828
  return V.createSVGMatrix(metrics.magnetMatrix);
816
829
  },
@@ -994,33 +1007,38 @@ export const CellView = View.extend({
994
1007
  const refNodeId = refNode ? V.ensureId(refNode) : '';
995
1008
  let refBBox = bboxCache[refNodeId];
996
1009
  if (!refBBox) {
997
- // Get the bounding box of the reference element using to the common ancestor
998
- // transformation space.
999
- //
1000
- // @example 1
1001
- // <g transform="translate(11, 13)">
1002
- // <rect @selector="b" x="1" y="2" width="3" height="4"/>
1003
- // <rect @selector="a"/>
1004
- // </g>
1005
- //
1006
- // In this case, the reference bounding box can not be affected
1007
- // by the `transform` attribute of the `<g>` element,
1008
- // because the exact transformation will be applied to the `a` element
1009
- // as well as to the `b` element.
1010
- //
1011
- // @example 2
1012
- // <g transform="translate(11, 13)">
1013
- // <rect @selector="b" x="1" y="2" width="3" height="4"/>
1014
- // </g>
1015
- // <rect @selector="a"/>
1016
- //
1017
- // In this case, the reference bounding box have to be affected by the
1018
- // `transform` attribute of the `<g>` element, because the `a` element
1019
- // is not descendant of the `<g>` element and will not be affected
1020
- // by the transformation.
1021
- refBBox = bboxCache[refNodeId] = (refNode)
1022
- ? V(refNode).getBBox({ target: getCommonAncestorNode(node, refNode) })
1023
- : opt.rootBBox;
1010
+ if (refNode) {
1011
+ // Get the bounding box of the reference element using to the common ancestor
1012
+ // transformation space.
1013
+ //
1014
+ // @example 1
1015
+ // <g transform="translate(11, 13)">
1016
+ // <rect @selector="b" x="1" y="2" width="3" height="4"/>
1017
+ // <rect @selector="a"/>
1018
+ // </g>
1019
+ //
1020
+ // In this case, the reference bounding box can not be affected
1021
+ // by the `transform` attribute of the `<g>` element,
1022
+ // because the exact transformation will be applied to the `a` element
1023
+ // as well as to the `b` element.
1024
+ //
1025
+ // @example 2
1026
+ // <g transform="translate(11, 13)">
1027
+ // <rect @selector="b" x="1" y="2" width="3" height="4"/>
1028
+ // </g>
1029
+ // <rect @selector="a"/>
1030
+ //
1031
+ // In this case, the reference bounding box have to be affected by the
1032
+ // `transform` attribute of the `<g>` element, because the `a` element
1033
+ // is not descendant of the `<g>` element and will not be affected
1034
+ // by the transformation.
1035
+ const refRect = this.getNodeBoundingRect(refNode);
1036
+ const refTMatrix = V(refNode).getTransformToElement(V.getCommonAncestor(node, refNode));
1037
+ refBBox = V.transformRect(refRect, refTMatrix);
1038
+ } else {
1039
+ refBBox = opt.rootBBox;
1040
+ }
1041
+ bboxCache[refNodeId] = refBBox;
1024
1042
  }
1025
1043
 
1026
1044
  if (roAttrs) {
@@ -1375,14 +1393,11 @@ Object.defineProperty(CellView.prototype, 'useCSSSelectors', {
1375
1393
  }
1376
1394
  });
1377
1395
 
1378
- // TODO: Move to Vectorizer library.
1379
- function getCommonAncestorNode(node1, node2) {
1380
- let parent = node1;
1381
- do {
1382
- if (parent.contains(node2)) return parent;
1383
- parent = parent.parentNode;
1384
- } while (parent);
1385
- return null;
1386
- }
1396
+ // Internal tag to identify this object as a cell view instance.
1397
+ // Used instead of `instanceof` for performance and cross-frame safety.
1387
1398
 
1399
+ export const CELL_VIEW_MARKER = Symbol('joint.cellViewMarker');
1388
1400
 
1401
+ Object.defineProperty(CellView.prototype, CELL_VIEW_MARKER, {
1402
+ value: true,
1403
+ });
@@ -1,6 +1,6 @@
1
1
  import { Cell } from './Cell.mjs';
2
2
  import { Point, toRad, normalizeAngle, Rect } from '../g/index.mjs';
3
- import { isNumber, isObject, interpolate, assign, invoke, normalizeSides } from '../util/index.mjs';
3
+ import { isNumber, isObject, interpolate, assign, invoke, normalizeSides, omit } from '../util/index.mjs';
4
4
  import { elementPortPrototype } from './ports.mjs';
5
5
 
6
6
  // Element base model.
@@ -344,22 +344,24 @@ export const Element = Cell.extend({
344
344
  const { graph } = this;
345
345
  if (!graph) throw new Error('Element must be part of a graph.');
346
346
 
347
- const childElements = this.getEmbeddedCells().filter(cell => cell.isElement());
348
- if (childElements.length === 0) return this;
347
+ // Get element children, optionally filtered according to `opt.filter`.
348
+ const filteredChildElements = this._getFilteredChildElements(opt.filter);
349
349
 
350
350
  this.startBatch('fit-embeds', opt);
351
351
 
352
352
  if (opt.deep) {
353
353
  // `opt.deep = true` means "fit to all descendants".
354
354
  // As the first action of the fitting algorithm, recursively apply `fitToChildren()` on all descendants.
355
- // - i.e. the algorithm is applied in reverse-depth order - start from deepest descendant, then go up (= this element).
356
- invoke(childElements, 'fitToChildren', opt);
355
+ // - i.e. the algorithm is applied in reverse-depth order - start from deepest descendant, then go up (= this element)
356
+ // - omit `opt.minRect` - it only makes sense for the first level of recursion if there are no filtered children, but in this case we do have filtered children
357
+ invoke(filteredChildElements, 'fitToChildren', omit(opt, 'minRect'));
357
358
  }
358
359
 
359
360
  // Set new size and position of this element, based on:
360
- // - union of bboxes of all children
361
+ // - union of bboxes of filtered element children
361
362
  // - inflated by given `opt.padding`
362
- this._fitToElements(Object.assign({ elements: childElements }, opt));
363
+ // - containing at least `opt.minRect` (if this is the first level of recursion and there are no filtered children)
364
+ this._fitToElements(Object.assign({ elements: filteredChildElements }, opt));
363
365
 
364
366
  this.stopBatch('fit-embeds');
365
367
 
@@ -375,25 +377,27 @@ export const Element = Cell.extend({
375
377
  // If the current element is `opt.terminator`, it means that this element has already been processed as parent so we can exit now.
376
378
  if (opt.deep && opt.terminator && ((opt.terminator === this) || (opt.terminator === this.id))) return this;
377
379
 
380
+ // If this element has no parent, there is nothing for us to do.
378
381
  const parentElement = this.getParentCell();
379
382
  if (!parentElement || !parentElement.isElement()) return this;
380
383
 
381
- // Get all children of parent element (i.e. this element + any sibling elements).
382
- const siblingElements = parentElement.getEmbeddedCells().filter(cell => cell.isElement());
383
- if (siblingElements.length === 0) return this;
384
+ // Get element children of parent element (i.e. this element + any sibling elements), optionally filtered according to `opt.filter`.
385
+ const filteredSiblingElements = parentElement._getFilteredChildElements(opt.filter);
384
386
 
385
387
  this.startBatch('fit-parent', opt);
386
388
 
387
389
  // Set new size and position of parent element, based on:
388
- // - union of bboxes of all children of parent element (i.e. this element + any sibling elements)
390
+ // - union of bboxes of filtered element children of parent element (i.e. this element + any sibling elements)
389
391
  // - inflated by given `opt.padding`
390
- parentElement._fitToElements(Object.assign({ elements: siblingElements }, opt));
392
+ // - containing at least `opt.minRect` (if this is the first level of recursion and there are no filtered siblings)
393
+ parentElement._fitToElements(Object.assign({ elements: filteredSiblingElements }, opt));
391
394
 
392
395
  if (opt.deep) {
393
396
  // `opt.deep = true` means "fit all ancestors to their respective children".
394
397
  // As the last action of the fitting algorithm, recursively apply `fitParent()` on all ancestors.
395
- // - i.e. the algorithm is applied in reverse-depth order - start from deepest descendant (= this element), then go up.
396
- parentElement.fitParent(opt);
398
+ // - i.e. the algorithm is applied in reverse-depth order - start from deepest descendant (= this element), then go up
399
+ // - omit `opt.minRect` - `minRect` is not relevant for the parent of parent element (and upwards)
400
+ parentElement.fitParent(omit(opt, 'minRect'));
397
401
  }
398
402
 
399
403
  this.stopBatch('fit-parent');
@@ -401,45 +405,87 @@ export const Element = Cell.extend({
401
405
  return this;
402
406
  },
403
407
 
408
+ _getFilteredChildElements: function(filter) {
409
+
410
+ let filterFn;
411
+ if (typeof filter === 'function') {
412
+ filterFn = (cell) => (cell.isElement() && filter(cell));
413
+ } else {
414
+ filterFn = (cell) => (cell.isElement());
415
+ }
416
+ return this.getEmbeddedCells().filter(filterFn);
417
+ },
418
+
404
419
  // Assumption: This element is part of a graph.
405
420
  _fitToElements: function(opt = {}) {
406
421
 
422
+ let minBBox = null;
423
+ if (opt.minRect) {
424
+ // Coerce `opt.minRect` to g.Rect
425
+ // (missing properties are taken from this element's current bbox).
426
+ const minRect = assign(this.getBBox(), opt.minRect);
427
+ minBBox = new Rect(minRect);
428
+ }
429
+
407
430
  const elementsBBox = this.graph.getCellsBBox(opt.elements);
408
- // If no `opt.elements` were provided, do nothing.
409
- if (!elementsBBox) return;
431
+ // If no `opt.elements` were provided, do nothing (but if `opt.minRect` was provided, set that as this element's bbox instead).
432
+ if (!elementsBBox) {
433
+ this._setBBox(minBBox, opt);
434
+ return;
435
+ }
410
436
 
411
437
  const { expandOnly, shrinkOnly } = opt;
412
- // This combination is meaningless, do nothing.
413
- if (expandOnly && shrinkOnly) return;
438
+ // This combination is meaningless, do nothing (but if `opt.minRect` was provided, set that as this element's bbox instead).
439
+ if (expandOnly && shrinkOnly) {
440
+ this._setBBox(minBBox, opt);
441
+ return;
442
+ }
414
443
 
415
444
  // Calculate new size and position of this element based on:
416
445
  // - union of bboxes of `opt.elements`
417
- // - inflated by `opt.padding` (if not provided, all four properties = 0)
446
+ // - inflated by normalized `opt.padding` (missing sides = 0)
418
447
  let { x, y, width, height } = elementsBBox;
419
448
  const { left, right, top, bottom } = normalizeSides(opt.padding);
420
449
  x -= left;
421
450
  y -= top;
422
451
  width += left + right;
423
452
  height += bottom + top;
424
- let resultBBox = new Rect(x, y, width, height);
453
+ let contentBBox = new Rect(x, y, width, height);
425
454
 
426
455
  if (expandOnly) {
427
456
  // Non-shrinking is enforced by taking union of this element's current bbox with bbox calculated from `opt.elements`.
428
- resultBBox = this.getBBox().union(resultBBox);
457
+ contentBBox = this.getBBox().union(contentBBox);
429
458
 
430
459
  } else if (shrinkOnly) {
431
460
  // Non-expansion is enforced by taking intersection of this element's current bbox with bbox calculated from `opt.elements`.
432
- const intersectionBBox = this.getBBox().intersect(resultBBox);
433
- // If all children are outside this element's current bbox, then `intersectionBBox` is `null` - does not make sense, do nothing.
434
- if (!intersectionBBox) return;
461
+ const intersectionBBox = this.getBBox().intersect(contentBBox);
462
+ // If all children are outside this element's current bbox, then `intersectionBBox` is `null`.
463
+ // That does not make sense, do nothing (but if `opt.minRect` was provided, set that as this element's bbox instead).
464
+ if (!intersectionBBox) {
465
+ this._setBBox(minBBox, opt);
466
+ return;
467
+ }
435
468
 
436
- resultBBox = intersectionBBox;
469
+ contentBBox = intersectionBBox;
437
470
  }
438
471
 
439
472
  // Set the new size and position of this element.
473
+ // - if `opt.minRect` was provided, add it via union to calculated bbox
474
+ let resultBBox = contentBBox;
475
+ if (minBBox) {
476
+ resultBBox = resultBBox.union(minBBox);
477
+ }
478
+ this._setBBox(resultBBox, opt);
479
+ },
480
+
481
+ _setBBox: function(bbox, opt) {
482
+
483
+ if (!bbox) return;
484
+
485
+ const { x, y, width, height } = bbox;
440
486
  this.set({
441
- position: { x: resultBBox.x, y: resultBBox.y },
442
- size: { width: resultBBox.width, height: resultBBox.height }
487
+ position: { x, y },
488
+ size: { width, height }
443
489
  }, opt);
444
490
  },
445
491
 
@@ -451,7 +497,7 @@ export const Element = Cell.extend({
451
497
 
452
498
  if (origin) {
453
499
 
454
- var center = this.getBBox().center();
500
+ var center = this.getCenter();
455
501
  var size = this.get('size');
456
502
  var position = this.get('position');
457
503
  center.rotate(origin, this.get('angle') - angle);
@@ -497,6 +543,11 @@ export const Element = Cell.extend({
497
543
  return bbox;
498
544
  },
499
545
 
546
+ getCenter: function() {
547
+ const { position: { x, y }, size: { width, height }} = this.attributes;
548
+ return new Point(x + width / 2, y + height / 2);
549
+ },
550
+
500
551
  getPointFromConnectedLink: function(link, endType) {
501
552
  // Center of the model
502
553
  var bbox = this.getBBox();
@@ -506,14 +557,9 @@ export const Element = Cell.extend({
506
557
  if (!endDef) return center;
507
558
  var portId = endDef.port;
508
559
  if (!portId || !this.hasPort(portId)) return center;
509
- var portGroup = this.portProp(portId, ['group']);
510
- var portsPositions = this.getPortsPositions(portGroup);
511
- var portCenter = new Point(portsPositions[portId]).offset(bbox.origin());
512
- var angle = this.angle();
513
- if (angle) portCenter.rotate(center, -angle);
514
- return portCenter;
560
+ return this.getPortCenter(portId);
515
561
  }
562
+
516
563
  });
517
564
 
518
565
  assign(Element.prototype, elementPortPrototype);
519
-
@@ -389,7 +389,8 @@ export const ElementView = CellView.extend({
389
389
  parent.unembed(element, { ui: true });
390
390
  data.initialParentId = parentId;
391
391
  } else {
392
- data.initialParentId = null;
392
+ // `data.initialParentId` can be explicitly set to a dummy value to enable validation of unembedding.
393
+ data.initialParentId = data.initialParentId || null;
393
394
  }
394
395
  },
395
396
 
@@ -1,6 +1,6 @@
1
1
  import * as mvc from '../mvc/index.mjs';
2
2
  import V from '../V/index.mjs';
3
- import { isPlainObject, result } from '../util/util.mjs';
3
+ import { isNumber, isPlainObject, result } from '../util/util.mjs';
4
4
 
5
5
  function toArray(obj) {
6
6
  if (!obj) return [];
@@ -71,7 +71,7 @@ export const HighlighterView = mvc.View.extend({
71
71
  }
72
72
  } else if (nodeSelector) {
73
73
  el = V.toNode(nodeSelector);
74
- if (!(el instanceof SVGElement)) el = null;
74
+ if (!(el instanceof SVGElement) || !cellView.el.contains(el)) el = null;
75
75
  }
76
76
  return el ? el : null;
77
77
  },
@@ -106,8 +106,8 @@ export const HighlighterView = mvc.View.extend({
106
106
  this.transform();
107
107
  return;
108
108
  }
109
- const { vel: cellViewRoot, paper } = cellView;
110
- const { layer: layerName } = options;
109
+ const { paper } = cellView;
110
+ const { layer: layerName, z } = options;
111
111
  if (layerName) {
112
112
  let vGroup;
113
113
  if (detachedTransformGroup) {
@@ -117,20 +117,41 @@ export const HighlighterView = mvc.View.extend({
117
117
  vGroup = V('g').addClass('highlight-transform').append(el);
118
118
  }
119
119
  this.transformGroup = vGroup;
120
- paper.getLayerView(layerName).insertSortedNode(vGroup.node, options.z);
120
+ paper.getLayerView(layerName).insertSortedNode(vGroup.node, z);
121
121
  } else {
122
- // TODO: prepend vs append
123
- if (!el.parentNode || el.nextSibling) {
124
- // Not appended yet or not the last child
125
- cellViewRoot.append(el);
122
+ const children = cellView.el.children;
123
+
124
+ const index = Math.max(z, 0);
125
+ const beforeChild = children[index];
126
+
127
+ // If the provided `z` is a number and there is an element on the index,
128
+ // we need to insert the highlighter before the element on the index.
129
+ // Otherwise, the highlighter will be appended as the last child.
130
+ const toBeInserted = isNumber(z) && beforeChild;
131
+ const isElementAtTargetPosition = toBeInserted
132
+ // If the element is being inserted, check if it is not already at the correct position.
133
+ ? el === beforeChild
134
+ // If the element is being appended, check if it is not already last child.
135
+ : !el.nextElementSibling;
136
+
137
+ // If the element is already mounted and does not require repositioning, do nothing.
138
+ if (el.parentNode && isElementAtTargetPosition) return;
139
+
140
+ if (toBeInserted) {
141
+ cellView.el.insertBefore(el, beforeChild);
142
+ } else {
143
+ cellView.el.appendChild(el);
126
144
  }
127
145
  }
128
146
  },
129
147
 
130
148
  unmount() {
131
- const { MOUNTABLE, transformGroup, vel } = this;
149
+ const { MOUNTABLE, transformGroup, vel, options } = this;
132
150
  if (!MOUNTABLE) return;
133
- if (transformGroup) {
151
+ if (options.layer) {
152
+ if (!transformGroup) return;
153
+ // else: if `transformGroup` is not null, it means the highlighter
154
+ // has not been mounted yet
134
155
  this.transformGroup = null;
135
156
  this.detachedTransformGroup = transformGroup;
136
157
  transformGroup.remove();
@@ -241,6 +262,28 @@ export const HighlighterView = mvc.View.extend({
241
262
  }
242
263
  },
243
264
 
265
+ // Check if the cellView has a highlighter with the given `id`.
266
+ // If no `id` is provided, it checks if the cellView has any highlighter.
267
+ has(cellView, id = null) {
268
+ const { cid } = cellView;
269
+ const { _views } = this;
270
+ const refs = _views[cid];
271
+ if (!refs) return false;
272
+ if (id === null) {
273
+ // any highlighter
274
+ for (let hid in refs) {
275
+ if (refs[hid] instanceof this) return true;
276
+ }
277
+ return false;
278
+ } else {
279
+ // single highlighter
280
+ if (id in refs) {
281
+ if (refs[id] instanceof this) return true;
282
+ }
283
+ return false;
284
+ }
285
+ },
286
+
244
287
  add(cellView, nodeSelector, id, opt = {}) {
245
288
  if (!id) throw new Error('dia.HighlighterView: An ID required.');
246
289
  // Search the existing view amongst all the highlighters