Skip to content

Commit 4d71685

Browse files
authored
dia.Paper: add validateUnembedding() option (#1489)
1 parent 3a311c2 commit 4d71685

5 files changed

Lines changed: 160 additions & 18 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<code>validateUnembedding</code> - a function that allows you to control which elements can be unembedded when the paper is put into the <code>embeddingMode</code> <i>(see above)</i>.
2+
<br>
3+
The function signature is <code>function(childView)</code> and should return
4+
<code>true</code> if the <code>childView</code> element can be unembedded from the parent and become a new root.
5+
<br>
6+
By default, all elements can become roots (the function returns <code>true</code> no matter what).
7+
<br>
8+
The callback is called at the end of drag & drop action and if <code>false</code> is returned, the position of the element, as well as the parent reference is reverted to the values before dragging.
9+
<br>
10+
The callback is called only on elements which were previously embedded in another element.

src/dia/ElementView.mjs

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { config } from '../config/index.mjs';
2-
import { assign, invoke, isFunction, toArray } from '../util/index.mjs';
2+
import { assign, isFunction, toArray } from '../util/index.mjs';
33
import { CellView } from './CellView.mjs';
44
import { Cell } from './Cell.mjs';
55
import V from '../V/index.mjs';
@@ -367,9 +367,13 @@ export const ElementView = CellView.extend({
367367
model.stopBatch('to-front');
368368

369369
// Before we start looking for suitable parent we remove the current one.
370-
var parentId = model.parent();
370+
const parentId = model.parent();
371371
if (parentId) {
372-
graph.getCell(parentId).unembed(model, { ui: true });
372+
const parent = graph.getCell(parentId);
373+
parent.unembed(model, { ui: true });
374+
data.initialParentId = parentId;
375+
} else {
376+
data.initialParentId = null;
373377
}
374378
},
375379

@@ -452,27 +456,63 @@ export const ElementView = CellView.extend({
452456
}
453457
},
454458

455-
finalizeEmbedding: function(data) {
456-
457-
data || (data = {});
459+
finalizeEmbedding: function(data = {}) {
458460

459-
var candidateView = data.candidateEmbedView;
460-
var model = data.model || this.model;
461-
var paper = data.paper || this.paper;
461+
const candidateView = data.candidateEmbedView;
462+
const element = data.model || this.model;
463+
const paper = data.paper || this.paper;
462464

463465
if (candidateView) {
464466

465467
// We finished embedding. Candidate view is chosen to become the parent of the model.
466-
candidateView.model.embed(model, { ui: true });
467-
candidateView.unhighlight(
468-
candidateView.findProxyNode(null, 'container'),
469-
{ embedding: true }
470-
);
468+
candidateView.model.embed(element, { ui: true });
469+
candidateView.unhighlight(candidateView.findProxyNode(null, 'container'), { embedding: true });
471470

472471
data.candidateEmbedView = null;
472+
473+
} else {
474+
475+
const { validateUnembedding } = paper.options;
476+
const { initialParentId } = data;
477+
// The element was originally embedded into another element.
478+
// The interaction would unembed the element. Let's validate
479+
// if the element can be unembedded.
480+
if (
481+
initialParentId &&
482+
typeof validateUnembedding === 'function' &&
483+
!validateUnembedding.call(paper, this)
484+
) {
485+
this._disallowUnembed(data);
486+
}
473487
}
474488

475-
invoke(paper.model.getConnectedLinks(model, { deep: true }), 'reparent', { ui: true });
489+
paper.model.getConnectedLinks(element, { deep: true }).forEach(link => {
490+
link.reparent({ ui: true });
491+
});
492+
},
493+
494+
_disallowUnembed: function(data) {
495+
const { model, whenNotAllowed = 'revert' } = data;
496+
const element = model || this.model;
497+
const paper = data.paper || this.paper;
498+
switch (whenNotAllowed) {
499+
case 'remove': {
500+
element.remove({ ui: true });
501+
break;
502+
}
503+
case 'revert': {
504+
const { initialParentId, initialPosition } = data;
505+
if (initialPosition) {
506+
const { x, y } = initialPosition;
507+
element.position(x, y, { ui: true });
508+
}
509+
const parent = paper.model.getCell(initialParentId);
510+
if (parent) {
511+
parent.embed(element, { ui: true });
512+
}
513+
break;
514+
}
515+
}
476516
},
477517

478518
getDelegatedView: function() {
@@ -649,8 +689,10 @@ export const ElementView = CellView.extend({
649689
delegatedView: view
650690
});
651691

692+
const position = view.model.position();
652693
view.eventData(evt, {
653-
pointerOffset: view.model.position().difference(x, y),
694+
initialPosition: position,
695+
pointerOffset: position.difference(x, y),
654696
restrictedArea: this.paper.getRestrictedArea(view, x, y)
655697
});
656698
},

src/dia/Paper.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ export const Paper = View.extend({
189189
return true;
190190
},
191191

192+
// Check whether to allow or disallow an embedded element to be unembedded / to become a root.
193+
validateUnembedding: function(childView) {
194+
// by default all elements can become roots
195+
return true;
196+
},
197+
192198
// Determines the way how a cell finds a suitable parent when it's dragged over the paper.
193199
// The cell with the highest z-index (visually on the top) will be chosen.
194200
findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft'

test/jointjs/embedding.js

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ QUnit.module('embedding', function(hooks) {
88
this.graph = new joint.dia.Graph;
99
this.paper = new joint.dia.Paper({
1010
el: $paper,
11-
gridSize: 10,
11+
gridSize: 1,
1212
model: this.graph,
1313
embeddingMode: true
1414
});
@@ -65,6 +65,89 @@ QUnit.module('embedding', function(hooks) {
6565
assert.notEqual(r2.get('parent'), r1.id, 'validating function denying all element pairs provided: element not embedded.');
6666
});
6767

68+
QUnit.test('validateUnembedding option', function(assert) {
69+
70+
var evt;
71+
var paper = this.paper;
72+
var graph = this.graph;
73+
var unembeddingIsValid = true;
74+
var validateUnembeddingSpy = sinon.spy(function() {
75+
return unembeddingIsValid;
76+
});
77+
78+
paper.options.validateUnembedding = validateUnembeddingSpy;
79+
80+
var r1 = new joint.shapes.standard.Rectangle({
81+
position: { x: 100, y: 101 },
82+
size: { width: 100, height: 100 }
83+
});
84+
var r2 = new joint.shapes.standard.Rectangle({
85+
position: { x: 500, y: 501 },
86+
size: { width: 100, height: 100 }
87+
});
88+
89+
graph.addCells([r1, r2]);
90+
91+
var v1 = r1.findView(paper);
92+
var v2 = r2.findView(paper);
93+
94+
// Make sure validateUnembedding() is not called when embedding
95+
var newPosition0 = { x: 100, y: 101 };
96+
97+
evt = { target: v1.el };
98+
v2.pointerdown(evt, r2.attributes.position.x, r2.attributes.position.y);
99+
v2.pointermove(evt, newPosition0.x, newPosition0.y);
100+
v2.pointerup(evt, newPosition0.x, newPosition0.y);
101+
102+
assert.equal(r2.parent(), r1.id);
103+
assert.ok(validateUnembeddingSpy.notCalled);
104+
105+
// Try To Unembed (Valid)
106+
unembeddingIsValid = true;
107+
var newPosition1 = { x: 300, y: 301 };
108+
109+
evt = { target: paper.el };
110+
v2.pointerdown(evt, newPosition0.x, newPosition0.y);
111+
v2.pointermove(evt, newPosition1.x, newPosition1.y);
112+
v2.pointerup(evt, newPosition1.x, newPosition1.y);
113+
114+
assert.ok(validateUnembeddingSpy.calledOnce);
115+
assert.ok(validateUnembeddingSpy.calledOn(paper));
116+
assert.ok(validateUnembeddingSpy.calledWithExactly(v2));
117+
assert.notOk(r2.isEmbeddedIn(r1));
118+
assert.notEqual(r2.parent(), r1.id);
119+
assert.deepEqual(r2.position().toJSON(), newPosition1);
120+
121+
// Try To Unembed (Invalid)
122+
unembeddingIsValid = false;
123+
r1.embed(r2);
124+
var newPosition2 = { x: 600, y: 601 };
125+
126+
evt = { target: paper.el };
127+
v2.pointerdown(evt, newPosition1.x, newPosition1.y);
128+
v2.pointermove(evt, newPosition2.x, newPosition2.y);
129+
v2.pointerup(evt, newPosition2.x, newPosition2.y);
130+
131+
assert.ok(validateUnembeddingSpy.calledTwice);
132+
assert.ok(validateUnembeddingSpy.calledOn(paper));
133+
assert.ok(validateUnembeddingSpy.calledWithExactly(v2));
134+
// make sure the position and the parent are reverted
135+
assert.ok(r2.isEmbeddedIn(r1));
136+
assert.equal(r2.parent(), r1.id);
137+
assert.deepEqual(r2.position().toJSON(), newPosition1); // not newPosition2
138+
139+
// Try to Unembedded (And remove when invalid)
140+
assert.ok(graph.getCell(r2.id));
141+
evt = { target: paper.el };
142+
v2.eventData(evt, { whenNotAllowed: 'remove' });
143+
v2.pointerdown(evt, newPosition1.x, newPosition1.y);
144+
v2.pointermove(evt, newPosition2.x, newPosition2.y);
145+
v2.pointerup(evt, newPosition2.x, newPosition2.y);
146+
147+
assert.ok(validateUnembeddingSpy.calledThrice);
148+
assert.notOk(graph.getCell(r2.id));
149+
});
150+
68151
QUnit.test('passing UI flag', function(assert) {
69152

70153
var r1 = new joint.shapes.basic.Rect({ position: { x: 100, y: 100 }, size: { width: 100, height: 100 }});

types/joint.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1058,7 +1058,8 @@ export namespace dia {
10581058
embeddingMode?: boolean;
10591059
frontParentOnly?: boolean,
10601060
findParentBy?: 'bbox' | 'center' | 'origin' | 'corner' | 'topRight' | 'bottomLeft' | ((elementView: ElementView) => Element[]);
1061-
validateEmbedding?: (childView: ElementView, parentView: ElementView) => boolean;
1061+
validateEmbedding?: (this: Paper, childView: ElementView, parentView: ElementView) => boolean;
1062+
validateUnembedding?: (this: Paper, childView: ElementView) => boolean;
10621063
// default views, models & attributes
10631064
cellViewNamespace?: any;
10641065
highlighterNamespace?: any;

0 commit comments

Comments
 (0)