Skip to content

Commit

Permalink
feat(dia.Cell): add foregroundEmbeds option to the Cell.toFront and C…
Browse files Browse the repository at this point in the history
…ell.toBack (#2206);

fix(dia.Cell): cell.toFront() and cell.toBack() keep the embedded elements on top of parents by default
  • Loading branch information
Tharos authored Jun 22, 2023
1 parent e0e9144 commit ddc5345
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 50 deletions.
54 changes: 33 additions & 21 deletions demo/embedding/front-and-back.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,45 @@

<link rel="stylesheet" type="text/css" href="../../build/joint.css"/>
<style>
body {
background: #f7f7f7;
}
#demo {
width: 400px;
margin: 40px auto;
text-align: center;
}
#paper {
-webkit-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.25);
-moz-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.25);
box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.25);
border-radius: 10px;
margin-bottom: 30px;
background: white;
}
button {
padding: 10px 15px;
margin: 0 8px 8px 0;
font-family: monospace;
}
body {
background: #f7f7f7;
}

input[type=checkbox] {
vertical-align: middle;
}

#demo {
width: 400px;
margin: 40px auto;
text-align: center;
}

#paper {
-webkit-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.25);
-moz-box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.25);
box-shadow: 0 0 14px 0 rgba(0, 0, 0, 0.25);
border-radius: 10px;
margin-bottom: 30px;
background: white;
}

button {
padding: 10px 15px;
margin: 0 8px 8px 0;
font-family: monospace;
}
</style>
</head>
<body>
<div id="demo">
<div id="paper"></div>

<p>
<label>
<input id="foregroundEmbeds" type="checkbox" checked> <code>opt.foregroundEmbeds</code>
</label>
</p>
<p>
<button id="toFrontButton">red.toFront()</button>
<button id="toBackButton">red.toBack()</button>
Expand Down
30 changes: 21 additions & 9 deletions demo/embedding/front-and-back.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,56 +13,68 @@ new joint.dia.Paper({


var r1 = new joint.shapes.standard.Rectangle({
position: { x: 40, y: 40 },
position: { x: 75, y: 36 },
size: { width: 250, height: 300 },
z: 2,
attrs: {
body: { fill: '#E74C3C' }
}
});
var r2 = new joint.shapes.standard.Rectangle({
position: { x: 60, y: 50 },
position: { x: 198, y: 49 },
size: { width: 100, height: 100 },
z: 10,
attrs: {
body: { fill: '#F1C40F' }
}
});
var r3 = new joint.shapes.standard.Rectangle({
position: { x: 140, y: 80 },
position: { x: 278, y: 100 },
size: { width: 100, height: 90 },
z: 5,
z: 1,
attrs: {
body: { fill: '#46acd9' }
}
});
var r4 = new joint.shapes.standard.Rectangle({
position: { x: 260, y: 210 },
position: { x: 34, y: 264 },
size: { width: 100, height: 100 },
z: 5,
attrs: {
body: { fill: '#7ac949' },
}
});
var r5 = new joint.shapes.standard.Rectangle({
position: { x: 112, y: 65 },
size: { width: 100, height: 60 },
z: 8,
attrs: {
body: { fill: '#bf7feb' },
}
});

const updateLabels = () => {
[r1, r2, r3, r4].forEach((element) => {
[r1, r2, r3, r4, r5].forEach((element) => {
element.attr('label/text', 'Z = ' + element.get('z'));
});
};

r1.embed(r2);
r1.embed(r3);
graph.addCells([r1, r2, r3, r4]);
r2.embed(r5);
graph.addCells([r1, r2, r3, r4, r5]);

updateLabels();

const foregroundEmbeds = document.getElementById('foregroundEmbeds');

document.getElementById('toFrontButton').addEventListener('click', () => {
r1.toFront({ deep: true });
r1.toFront({ deep: true, foregroundEmbeds: foregroundEmbeds.checked });
updateLabels();
});


document.getElementById('toBackButton').addEventListener('click', () => {
r1.toBack({ deep: true });
r1.toBack({ deep: true, foregroundEmbeds: foregroundEmbeds.checked });
updateLabels();
});
14 changes: 9 additions & 5 deletions docs/src/joint/api/dia/Element/prototype/toBack.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<pre class="docs-method-signature"><code>element.toBack([opt])</code></pre>

<p>Move the element so it is behind all other cells (elements/links).
If <code>opt.deep</code> is <code>true</code>, all the embedded cells of this
element will get higher <code>z</code> index than that of this element in a Breadth-first search (BFS) fashion. This
is especially useful in hierarchical diagrams where if you want to send an element to the back, you don't want
its children (embedded cells) to be hidden behind that element.</p>
<p>Move the element so it is behind all other cells (elements/links). If <code>opt.deep</code> is
<code>true</code>, all the embedded cells of this element will be updated in a Breadth-first
search (BFS) fashion.
</p>

<p>If <code>opt.foregroundEmbeds</code> is <code>true</code> (as by default), all the embedded
cells will get a higher <code>z</code> index than that of this element. This is especially useful in hierarchical diagrams where if you want to send an element to the back, you don't want its children (embedded cells) to be hidden behind that element. If
<code>opt.foregroundEmbeds</code> is <code>false</code>, the original order within the group is preserved, allowing children to remain behind their parents.
</p>

<p>Set <code>opt.breadthFirst</code> to <code>false</code> to index the elements using Depth-first search (DFS).</p>
12 changes: 8 additions & 4 deletions docs/src/joint/api/dia/Element/prototype/toFront.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<pre class="docs-method-signature"><code>element.toFront([opt])</code></pre>

<p>Move the element so it is on top of all other cells (element/links).
If <code>opt.deep</code> is <code>true</code>, all the embedded cells of this
element will get higher <code>z</code> index than that of this element in a Breadth-first search (BFS) fashion. This
is especially useful in hierarchical diagrams where if you want to send an element to the front, you don't want
its children (embedded cells) to be hidden behind that element.</p>
If <code>opt.deep</code> is <code>true</code>, all the embedded cells of this element will be updated
in a Breadth-first search (BFS) fashion.</p>

<p>If <code>opt.foregroundEmbeds</code> is <code>true</code> (as by default), all the embedded
cells will get a higher <code>z</code> index than that of this element. This is especially useful
in hierarchical diagrams where if you want to send an element to the front, you don't want its children (embedded cells)
to be hidden behind that element. If <code>opt.foregroundEmbeds</code> is <code>false</code>,
the original order within the group is preserved, allowing children to remain behind their parents.</p>

<p>Set <code>opt.breadthFirst</code> to <code>false</code> to index the elements using Depth-first search (DFS).</p>

Expand Down
19 changes: 10 additions & 9 deletions src/dia/Cell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
cancelFrame,
defaultsDeep,
has,
sortBy
sortBy,
defaults
} from '../util/util.mjs';
import { cloneCells } from '../util/cloneCells.mjs';
import { attributes } from './attributes/index.mjs';
Expand Down Expand Up @@ -227,24 +228,24 @@ export const Cell = Backbone.Model.extend({
toFront: function(opt) {
var graph = this.graph;
if (graph) {
opt = opt || {};
opt = defaults(opt || {}, { foregroundEmbeds: true });

let cells;
if (opt.deep) {
cells = this.getEmbeddedCells({ deep: true, breadthFirst: opt.breadthFirst !== false });
cells = this.getEmbeddedCells({ deep: true, breadthFirst: opt.breadthFirst !== false, sortSiblings: opt.foregroundEmbeds });
cells.unshift(this);
} else {
cells = [this];
}

const sortedCells = sortBy(cells, cell => cell.z());
const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z());

const maxZ = graph.maxZIndex();
let z = maxZ - cells.length + 1;

const collection = graph.get('cells');

let shouldUpdate = (collection.indexOf(this) !== (collection.length - cells.length));
let shouldUpdate = (collection.indexOf(sortedCells[0]) !== (collection.length - cells.length));
if (!shouldUpdate) {
shouldUpdate = sortedCells.some(function(cell, index) {
return cell.z() !== z + index;
Expand All @@ -270,23 +271,23 @@ export const Cell = Backbone.Model.extend({
toBack: function(opt) {
var graph = this.graph;
if (graph) {
opt = opt || {};
opt = defaults(opt || {}, { foregroundEmbeds: true });

let cells;
if (opt.deep) {
cells = this.getEmbeddedCells({ deep: true, breadthFirst: opt.breadthFirst !== false });
cells = this.getEmbeddedCells({ deep: true, breadthFirst: opt.breadthFirst !== false, sortSiblings: opt.foregroundEmbeds });
cells.unshift(this);
} else {
cells = [this];
}

const sortedCells = sortBy(cells, cell => cell.z());
const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z());

let z = graph.minZIndex();

var collection = graph.get('cells');

let shouldUpdate = (collection.indexOf(this) !== 0);
let shouldUpdate = (collection.indexOf(sortedCells[0]) !== 0);
if (!shouldUpdate) {
shouldUpdate = sortedCells.some(function(cell, index) {
return cell.z() !== z + index;
Expand Down
64 changes: 64 additions & 0 deletions test/jointjs/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,70 @@ QUnit.module('basic', function(hooks) {
assert.equal(r4.get('z'), -2);
});

QUnit.test('toFront() with foregroundEmbeds: false', function(assert) {
const Rect = joint.shapes.standard.Rectangle;

const r1 = new Rect({ z: 2 });
const r2 = new Rect({ z: 6 });
const r3 = new Rect({ z: 4 });
const r4 = new Rect({ z: 1 });

const r5 = new Rect({ z: 3 }); // this rectangle forces r1 to go front

r1.embed(r2);
r1.embed(r3);
r1.embed(r4);

this.graph.addCells([r1, r2, r3, r4, r5]);

r1.toFront({ deep: true, foregroundEmbeds: false });

assert.equal(r1.get('z'), 8);
assert.equal(r2.get('z'), 10);
assert.equal(r3.get('z'), 9);
assert.equal(r4.get('z'), 7);

r1.toFront({ deep: true, foregroundEmbeds: false });
r1.toFront({ deep: true, foregroundEmbeds: false }); // calling toFront again doesn't change anything

assert.equal(r1.get('z'), 8);
assert.equal(r2.get('z'), 10);
assert.equal(r3.get('z'), 9);
assert.equal(r4.get('z'), 7);
});

QUnit.test('toBack() with foregroundEmbeds: false', function(assert) {
const Rect = joint.shapes.standard.Rectangle;

const r1 = new Rect({ z: 3 });
const r2 = new Rect({ z: 7 });
const r3 = new Rect({ z: 5 });
const r4 = new Rect({ z: 2 });

const r5 = new Rect({ z: 1 }); // this rectangle forces r1 to go back

r1.embed(r2);
r1.embed(r3);
r1.embed(r4);

this.graph.addCells([r1, r2, r3, r4, r5]);

r1.toBack({ deep: true, foregroundEmbeds: false });

assert.equal(r1.get('z'), -2);
assert.equal(r2.get('z'), 0);
assert.equal(r3.get('z'), -1);
assert.equal(r4.get('z'), -3);

r1.toBack({ deep: true, foregroundEmbeds: false });
r1.toBack({ deep: true, foregroundEmbeds: false }); // calling toBack again doesn't change anything

assert.equal(r1.get('z'), -2);
assert.equal(r2.get('z'), 0);
assert.equal(r3.get('z'), -1);
assert.equal(r4.get('z'), -3);
});

// tests for `dia.Element.fitToChildren()` can be found in `/test/jointjs/elements.js`
QUnit.test('fitEmbeds()', function(assert) {
// structure of objects:
Expand Down
8 changes: 6 additions & 2 deletions types/joint.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ export namespace dia {
sortSiblings?: boolean;
}

interface ToFrontAndBackOptions extends GetEmbeddedCellsOptions {
foregroundEmbeds?: boolean;
}

interface TransitionOptions extends Options {
delay?: number;
duration?: number;
Expand All @@ -335,9 +339,9 @@ export namespace dia {

remove(opt?: Cell.DisconnectableOptions): this;

toFront(opt?: Cell.GetEmbeddedCellsOptions): this;
toFront(opt?: Cell.ToFrontAndBackOptions): this;

toBack(opt?: Cell.GetEmbeddedCellsOptions): this;
toBack(opt?: Cell.ToFrontAndBackOptions): this;

parent(): string;
parent(parentId: Cell.ID): this;
Expand Down

0 comments on commit ddc5345

Please sign in to comment.