Skip to content

Commit b62479c

Browse files
authored
feat(dia.Graph): add transferCellEmbeds() and transferCellConnectedLinks() (#2752)
feat(dia.Graph): add `reparent` option to embed() (#2752)
1 parent 6d12e65 commit b62479c

File tree

5 files changed

+192
-3
lines changed

5 files changed

+192
-3
lines changed

packages/joint-core/src/dia/Cell.mjs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,12 +339,25 @@ export const Cell = Model.extend({
339339
return this.set('parent', parent, opt);
340340
},
341341

342-
embed: function(cell, opt) {
342+
embed: function(cell, opt = {}) {
343343
const cells = Array.isArray(cell) ? cell : [cell];
344344
if (!this.canEmbed(cells)) {
345345
throw new Error('Recursive embedding not allowed.');
346346
}
347-
if (cells.some(c => c.isEmbedded() && this.id !== c.parent())) {
347+
if (opt.reparent) {
348+
const parents = uniq(cells.map(c => c.getParentCell()));
349+
350+
// Unembed cells from their current parents.
351+
parents.forEach((parent) => {
352+
// Cell doesn't have to be embedded.
353+
if (!parent) return;
354+
355+
// Pass all the `cells` since the `dia.Cell._unembedCells` method can handle cases
356+
// where not all elements of `cells` are embedded in the same parent.
357+
parent._unembedCells(cells, opt);
358+
});
359+
360+
} else if (cells.some(c => c.isEmbedded() && this.id !== c.parent())) {
348361
throw new Error('Embedding of already embedded cells is not allowed.');
349362
}
350363
this._embedCells(cells, opt);

packages/joint-core/src/dia/Graph.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,39 @@ export const Graph = Model.extend({
396396
this.get('cells').remove(cell, { silent: true });
397397
},
398398

399+
transferCellEmbeds: function(sourceCell, targetCell, opt = {}) {
400+
401+
const batchName = 'transfer-embeds';
402+
this.startBatch(batchName);
403+
404+
// Embed children of the source cell in the target cell.
405+
const children = sourceCell.getEmbeddedCells();
406+
targetCell.embed(children, { ...opt, reparent: true });
407+
408+
this.stopBatch(batchName);
409+
},
410+
411+
transferCellConnectedLinks: function(sourceCell, targetCell, opt = {}) {
412+
413+
const batchName = 'transfer-connected-links';
414+
this.startBatch(batchName);
415+
416+
// Reconnect all the links connected to the old cell to the new cell.
417+
const connectedLinks = this.getConnectedLinks(sourceCell, opt);
418+
connectedLinks.forEach((link) => {
419+
420+
if (link.getSourceCell() === sourceCell) {
421+
link.prop(['source', 'id'], targetCell.id, opt);
422+
}
423+
424+
if (link.getTargetCell() === sourceCell) {
425+
link.prop(['target', 'id'], targetCell.id, opt);
426+
}
427+
});
428+
429+
this.stopBatch(batchName);
430+
},
431+
399432
// Get a cell by `id`.
400433
getCell: function(id) {
401434

packages/joint-core/test/jointjs/cell.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,36 @@ QUnit.module('cell', function(hooks) {
256256

257257
assert.raises(() => { cell3.embed(cell2); }, /Embedding of already embedded cells is not allowed/, 'throws exception on embedding of embedded cell');
258258
});
259+
260+
QUnit.test('opt.reparent = true', function(assert) {
261+
262+
const cell1 = new joint.shapes.standard.Rectangle({
263+
position: { x: 20, y: 20 },
264+
size: { width: 60, height: 60 }
265+
});
266+
const cell2 = new joint.shapes.standard.Rectangle({
267+
position: { x: 20, y: 20 },
268+
size: { width: 60, height: 60 }
269+
});
270+
const cell3 = new joint.shapes.standard.Rectangle({
271+
position: { x: 20, y: 20 },
272+
size: { width: 60, height: 60 }
273+
});
274+
const cell4 = new joint.shapes.standard.Rectangle({
275+
position: { x: 20, y: 20 },
276+
size: { width: 60, height: 60 }
277+
});
278+
279+
this.graph.addCells([cell1, cell2, cell3, cell4]);
280+
281+
cell1.embed(cell2);
282+
cell3.embed([cell2, cell4], { reparent: true });
283+
284+
assert.equal(cell1.getEmbeddedCells().length, 0);
285+
assert.equal(cell2.parent(), cell3.id);
286+
assert.equal(cell3.getEmbeddedCells()[0].id, cell2.id);
287+
assert.equal(cell3.getEmbeddedCells()[1].id, cell4.id);
288+
});
259289
});
260290

261291
QUnit.module('remove attributes', function(hooks) {

packages/joint-core/test/jointjs/graph.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,4 +1546,109 @@ QUnit.module('graph', function(hooks) {
15461546
assert.notOk(graph.hasActiveBatch());
15471547
});
15481548
});
1549+
1550+
QUnit.module('graph.transferCellEmbeds()', function() {
1551+
1552+
QUnit.test('should transfer embeds from one element to another', function(assert) {
1553+
1554+
const originalElement = new joint.shapes.standard.Rectangle();
1555+
const child = new joint.shapes.standard.Rectangle();
1556+
const replacementElement = new joint.shapes.standard.Rectangle();
1557+
1558+
originalElement.embed(child);
1559+
1560+
this.graph.addCells([originalElement, child, replacementElement]);
1561+
this.graph.transferCellEmbeds(originalElement, replacementElement);
1562+
1563+
assert.equal(replacementElement.getEmbeddedCells()[0], child);
1564+
assert.equal(originalElement.getEmbeddedCells().length, 0);
1565+
});
1566+
1567+
QUnit.test('should transfer embeds from an element to a link', function(assert) {
1568+
1569+
const link = new joint.shapes.standard.Link();
1570+
const child = new joint.shapes.standard.Rectangle();
1571+
const element = new joint.shapes.standard.Rectangle();
1572+
1573+
element.embed(child);
1574+
1575+
this.graph.addCells([link, child, element]);
1576+
this.graph.transferCellEmbeds(element, link);
1577+
1578+
assert.equal(link.getEmbeddedCells()[0], child);
1579+
assert.equal(element.getEmbeddedCells().length, 0);
1580+
});
1581+
});
1582+
1583+
QUnit.module('graph.transferCellConnectedLinks()', function() {
1584+
1585+
QUnit.test('should transfer links of an element', function(assert) {
1586+
1587+
const originalElement = new joint.shapes.standard.Rectangle();
1588+
const link1 = new joint.shapes.standard.Link({ source: { id: originalElement.id }});
1589+
const link2 = new joint.shapes.standard.Link({ target: { id: originalElement.id }});
1590+
const replacementElement = new joint.shapes.standard.Rectangle();
1591+
1592+
this.graph.addCells([originalElement, link1, link2, replacementElement]);
1593+
this.graph.transferCellConnectedLinks(originalElement, replacementElement);
1594+
1595+
assert.equal(link1.source().id, replacementElement.id);
1596+
assert.equal(link2.target().id, replacementElement.id);
1597+
});
1598+
1599+
QUnit.test('should transfer links of a link', function(assert) {
1600+
1601+
const originalLink = new joint.shapes.standard.Link();
1602+
const link1 = new joint.shapes.standard.Link({ source: { id: originalLink.id }});
1603+
const link2 = new joint.shapes.standard.Link({ target: { id: originalLink.id }});
1604+
const replacementLink = new joint.shapes.standard.Link();
1605+
1606+
this.graph.addCells([originalLink, link1, link2, replacementLink]);
1607+
this.graph.transferCellConnectedLinks(originalLink, replacementLink);
1608+
1609+
assert.equal(link1.source().id, replacementLink.id);
1610+
assert.equal(link2.target().id, replacementLink.id);
1611+
});
1612+
1613+
QUnit.test('should work when transferring links from a link to an element', function(assert) {
1614+
1615+
const originalLink = new joint.shapes.standard.Link();
1616+
const link1 = new joint.shapes.standard.Link({ source: { id: originalLink.id }});
1617+
const link2 = new joint.shapes.standard.Link({ target: { id: originalLink.id }});
1618+
const element = new joint.shapes.standard.Rectangle();
1619+
1620+
this.graph.addCells([originalLink, link1, link2, element]);
1621+
this.graph.transferCellConnectedLinks(originalLink, element);
1622+
1623+
assert.equal(link1.source().id, element.id);
1624+
assert.equal(link2.target().id, element.id);
1625+
});
1626+
1627+
QUnit.test('should work when transferring links from an element to a link', function(assert) {
1628+
1629+
const originalElement = new joint.shapes.standard.Rectangle();
1630+
const link1 = new joint.shapes.standard.Link({ source: { id: originalElement.id }});
1631+
const link2 = new joint.shapes.standard.Link({ target: { id: originalElement.id }});
1632+
const replacementLink = new joint.shapes.standard.Link();
1633+
1634+
this.graph.addCells([originalElement, link1, link2, replacementLink]);
1635+
this.graph.transferCellConnectedLinks(originalElement, replacementLink);
1636+
1637+
assert.equal(link1.source().id, replacementLink.id);
1638+
assert.equal(link2.target().id, replacementLink.id);
1639+
});
1640+
1641+
QUnit.test('should work with loop links', function(assert) {
1642+
1643+
const originalElement = new joint.shapes.standard.Rectangle();
1644+
const link = new joint.shapes.standard.Link({ source: { id: originalElement.id }, target: { id: originalElement.id }});
1645+
const replacementElement = new joint.shapes.standard.Rectangle();
1646+
1647+
this.graph.addCells([originalElement, link, replacementElement]);
1648+
this.graph.transferCellConnectedLinks(originalElement, replacementElement);
1649+
1650+
assert.equal(link.source().id, replacementElement.id);
1651+
assert.equal(link.target().id, replacementElement.id);
1652+
});
1653+
});
15491654
});

packages/joint-core/types/joint.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ export namespace dia {
263263

264264
removeCells(cells: Cell[], opt?: Cell.DisconnectableOptions): this;
265265

266+
transferCellEmbeds(sourceCell: Cell, targetCell: Cell, opt?: S): void;
267+
268+
transferCellConnectedLinks(sourceCell: Cell, targetCell: Cell, opt?: Graph.ConnectionOptions): void;
269+
266270
resize(width: number, height: number, opt?: S): this;
267271

268272
resizeCells(width: number, height: number, cells: Cell[], opt?: S): this;
@@ -307,6 +311,10 @@ export namespace dia {
307311
[key: string]: any;
308312
}
309313

314+
interface EmbedOptions extends Options {
315+
reparent?: boolean;
316+
}
317+
310318
interface EmbeddableOptions<T = boolean> extends Options {
311319
deep?: T;
312320
}
@@ -439,7 +447,7 @@ export namespace dia {
439447

440448
stopTransitions(path?: string, delim?: string): this;
441449

442-
embed(cell: Cell | Cell[], opt?: Graph.Options): this;
450+
embed(cell: Cell | Cell[], opt?: Cell.EmbedOptions): this;
443451

444452
unembed(cell: Cell | Cell[], opt?: Graph.Options): this;
445453

0 commit comments

Comments
 (0)