diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index 7c95246d3..e0cebc9cb 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -339,12 +339,25 @@ export const Cell = Model.extend({ return this.set('parent', parent, opt); }, - embed: function(cell, opt) { + embed: function(cell, opt = {}) { const cells = Array.isArray(cell) ? cell : [cell]; if (!this.canEmbed(cells)) { throw new Error('Recursive embedding not allowed.'); } - if (cells.some(c => c.isEmbedded() && this.id !== c.parent())) { + if (opt.reparent) { + const parents = uniq(cells.map(c => c.getParentCell())); + + // Unembed cells from their current parents. + parents.forEach((parent) => { + // Cell doesn't have to be embedded. + if (!parent) return; + + // Pass all the `cells` since the `dia.Cell._unembedCells` method can handle cases + // where not all elements of `cells` are embedded in the same parent. + parent._unembedCells(cells, opt); + }); + + } else if (cells.some(c => c.isEmbedded() && this.id !== c.parent())) { throw new Error('Embedding of already embedded cells is not allowed.'); } this._embedCells(cells, opt); diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index a027fbd3c..bc32ff251 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -396,6 +396,39 @@ export const Graph = Model.extend({ this.get('cells').remove(cell, { silent: true }); }, + transferCellEmbeds: function(sourceCell, targetCell, opt = {}) { + + const batchName = 'transfer-embeds'; + this.startBatch(batchName); + + // Embed children of the source cell in the target cell. + const children = sourceCell.getEmbeddedCells(); + targetCell.embed(children, { ...opt, reparent: true }); + + this.stopBatch(batchName); + }, + + transferCellConnectedLinks: function(sourceCell, targetCell, opt = {}) { + + const batchName = 'transfer-connected-links'; + this.startBatch(batchName); + + // Reconnect all the links connected to the old cell to the new cell. + const connectedLinks = this.getConnectedLinks(sourceCell, opt); + connectedLinks.forEach((link) => { + + if (link.getSourceCell() === sourceCell) { + link.prop(['source', 'id'], targetCell.id, opt); + } + + if (link.getTargetCell() === sourceCell) { + link.prop(['target', 'id'], targetCell.id, opt); + } + }); + + this.stopBatch(batchName); + }, + // Get a cell by `id`. getCell: function(id) { diff --git a/packages/joint-core/test/jointjs/cell.js b/packages/joint-core/test/jointjs/cell.js index 609119ab5..82d089479 100644 --- a/packages/joint-core/test/jointjs/cell.js +++ b/packages/joint-core/test/jointjs/cell.js @@ -256,6 +256,36 @@ QUnit.module('cell', function(hooks) { assert.raises(() => { cell3.embed(cell2); }, /Embedding of already embedded cells is not allowed/, 'throws exception on embedding of embedded cell'); }); + + QUnit.test('opt.reparent = true', function(assert) { + + const cell1 = new joint.shapes.standard.Rectangle({ + position: { x: 20, y: 20 }, + size: { width: 60, height: 60 } + }); + const cell2 = new joint.shapes.standard.Rectangle({ + position: { x: 20, y: 20 }, + size: { width: 60, height: 60 } + }); + const cell3 = new joint.shapes.standard.Rectangle({ + position: { x: 20, y: 20 }, + size: { width: 60, height: 60 } + }); + const cell4 = new joint.shapes.standard.Rectangle({ + position: { x: 20, y: 20 }, + size: { width: 60, height: 60 } + }); + + this.graph.addCells([cell1, cell2, cell3, cell4]); + + cell1.embed(cell2); + cell3.embed([cell2, cell4], { reparent: true }); + + assert.equal(cell1.getEmbeddedCells().length, 0); + assert.equal(cell2.parent(), cell3.id); + assert.equal(cell3.getEmbeddedCells()[0].id, cell2.id); + assert.equal(cell3.getEmbeddedCells()[1].id, cell4.id); + }); }); QUnit.module('remove attributes', function(hooks) { diff --git a/packages/joint-core/test/jointjs/graph.js b/packages/joint-core/test/jointjs/graph.js index 6a7475fa0..fc79caf72 100644 --- a/packages/joint-core/test/jointjs/graph.js +++ b/packages/joint-core/test/jointjs/graph.js @@ -1546,4 +1546,109 @@ QUnit.module('graph', function(hooks) { assert.notOk(graph.hasActiveBatch()); }); }); + + QUnit.module('graph.transferCellEmbeds()', function() { + + QUnit.test('should transfer embeds from one element to another', function(assert) { + + const originalElement = new joint.shapes.standard.Rectangle(); + const child = new joint.shapes.standard.Rectangle(); + const replacementElement = new joint.shapes.standard.Rectangle(); + + originalElement.embed(child); + + this.graph.addCells([originalElement, child, replacementElement]); + this.graph.transferCellEmbeds(originalElement, replacementElement); + + assert.equal(replacementElement.getEmbeddedCells()[0], child); + assert.equal(originalElement.getEmbeddedCells().length, 0); + }); + + QUnit.test('should transfer embeds from an element to a link', function(assert) { + + const link = new joint.shapes.standard.Link(); + const child = new joint.shapes.standard.Rectangle(); + const element = new joint.shapes.standard.Rectangle(); + + element.embed(child); + + this.graph.addCells([link, child, element]); + this.graph.transferCellEmbeds(element, link); + + assert.equal(link.getEmbeddedCells()[0], child); + assert.equal(element.getEmbeddedCells().length, 0); + }); + }); + + QUnit.module('graph.transferCellConnectedLinks()', function() { + + QUnit.test('should transfer links of an element', function(assert) { + + const originalElement = new joint.shapes.standard.Rectangle(); + const link1 = new joint.shapes.standard.Link({ source: { id: originalElement.id }}); + const link2 = new joint.shapes.standard.Link({ target: { id: originalElement.id }}); + const replacementElement = new joint.shapes.standard.Rectangle(); + + this.graph.addCells([originalElement, link1, link2, replacementElement]); + this.graph.transferCellConnectedLinks(originalElement, replacementElement); + + assert.equal(link1.source().id, replacementElement.id); + assert.equal(link2.target().id, replacementElement.id); + }); + + QUnit.test('should transfer links of a link', function(assert) { + + const originalLink = new joint.shapes.standard.Link(); + const link1 = new joint.shapes.standard.Link({ source: { id: originalLink.id }}); + const link2 = new joint.shapes.standard.Link({ target: { id: originalLink.id }}); + const replacementLink = new joint.shapes.standard.Link(); + + this.graph.addCells([originalLink, link1, link2, replacementLink]); + this.graph.transferCellConnectedLinks(originalLink, replacementLink); + + assert.equal(link1.source().id, replacementLink.id); + assert.equal(link2.target().id, replacementLink.id); + }); + + QUnit.test('should work when transferring links from a link to an element', function(assert) { + + const originalLink = new joint.shapes.standard.Link(); + const link1 = new joint.shapes.standard.Link({ source: { id: originalLink.id }}); + const link2 = new joint.shapes.standard.Link({ target: { id: originalLink.id }}); + const element = new joint.shapes.standard.Rectangle(); + + this.graph.addCells([originalLink, link1, link2, element]); + this.graph.transferCellConnectedLinks(originalLink, element); + + assert.equal(link1.source().id, element.id); + assert.equal(link2.target().id, element.id); + }); + + QUnit.test('should work when transferring links from an element to a link', function(assert) { + + const originalElement = new joint.shapes.standard.Rectangle(); + const link1 = new joint.shapes.standard.Link({ source: { id: originalElement.id }}); + const link2 = new joint.shapes.standard.Link({ target: { id: originalElement.id }}); + const replacementLink = new joint.shapes.standard.Link(); + + this.graph.addCells([originalElement, link1, link2, replacementLink]); + this.graph.transferCellConnectedLinks(originalElement, replacementLink); + + assert.equal(link1.source().id, replacementLink.id); + assert.equal(link2.target().id, replacementLink.id); + }); + + QUnit.test('should work with loop links', function(assert) { + + const originalElement = new joint.shapes.standard.Rectangle(); + const link = new joint.shapes.standard.Link({ source: { id: originalElement.id }, target: { id: originalElement.id }}); + const replacementElement = new joint.shapes.standard.Rectangle(); + + this.graph.addCells([originalElement, link, replacementElement]); + this.graph.transferCellConnectedLinks(originalElement, replacementElement); + + assert.equal(link.source().id, replacementElement.id); + assert.equal(link.target().id, replacementElement.id); + }); + }); }); diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 64acef7f1..722b18e8e 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -263,6 +263,10 @@ export namespace dia { removeCells(cells: Cell[], opt?: Cell.DisconnectableOptions): this; + transferCellEmbeds(sourceCell: Cell, targetCell: Cell, opt?: S): void; + + transferCellConnectedLinks(sourceCell: Cell, targetCell: Cell, opt?: Graph.ConnectionOptions): void; + resize(width: number, height: number, opt?: S): this; resizeCells(width: number, height: number, cells: Cell[], opt?: S): this; @@ -307,6 +311,10 @@ export namespace dia { [key: string]: any; } + interface EmbedOptions extends Options { + reparent?: boolean; + } + interface EmbeddableOptions extends Options { deep?: T; } @@ -439,7 +447,7 @@ export namespace dia { stopTransitions(path?: string, delim?: string): this; - embed(cell: Cell | Cell[], opt?: Graph.Options): this; + embed(cell: Cell | Cell[], opt?: Cell.EmbedOptions): this; unembed(cell: Cell | Cell[], opt?: Graph.Options): this;