Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dia.Graph): add transferCellEmbeds() and transferCellConnectedLinks() #2752

Merged
merged 10 commits into from
Oct 23, 2024
17 changes: 15 additions & 2 deletions packages/joint-core/src/dia/Cell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions packages/joint-core/src/dia/Graph.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
MartinKanera marked this conversation as resolved.
Show resolved Hide resolved
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) {

Expand Down
30 changes: 30 additions & 0 deletions packages/joint-core/test/jointjs/cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
105 changes: 105 additions & 0 deletions packages/joint-core/test/jointjs/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
10 changes: 9 additions & 1 deletion packages/joint-core/types/joint.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -307,6 +311,10 @@ export namespace dia {
[key: string]: any;
}

interface EmbedOptions extends Options {
reparent?: boolean;
}

interface EmbeddableOptions<T = boolean> extends Options {
deep?: T;
}
Expand Down Expand Up @@ -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;

Expand Down
Loading