diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs
index e0cebc9cba..ee112446c2 100644
--- a/packages/joint-core/src/dia/Cell.mjs
+++ b/packages/joint-core/src/dia/Cell.mjs
@@ -239,7 +239,7 @@ export const Cell = Model.extend({
}
}
- this.trigger('remove', this, graph.attributes.cells, opt);
+ this.trigger('remove', this, graph.cellCollection, opt);
graph.stopBatch('remove');
@@ -247,7 +247,7 @@ export const Cell = Model.extend({
},
toFront: function(opt) {
- var graph = this.graph;
+ const { graph } = this;
if (graph) {
opt = defaults(opt || {}, { foregroundEmbeds: true });
@@ -261,12 +261,14 @@ export const Cell = Model.extend({
const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z());
- const maxZ = graph.maxZIndex();
+ const layerId = this.layer();
+
+ const maxZ = graph.maxZIndex(layerId);
let z = maxZ - cells.length + 1;
- const collection = graph.get('cells');
+ const layerCells = graph.getCellLayerCells(layerId);
- let shouldUpdate = (collection.toArray().indexOf(sortedCells[0]) !== (collection.length - cells.length));
+ let shouldUpdate = (layerCells.indexOf(sortedCells[0]) !== (layerCells.length - cells.length));
if (!shouldUpdate) {
shouldUpdate = sortedCells.some(function(cell, index) {
return cell.z() !== z + index;
@@ -290,7 +292,7 @@ export const Cell = Model.extend({
},
toBack: function(opt) {
- var graph = this.graph;
+ const { graph } = this;
if (graph) {
opt = defaults(opt || {}, { foregroundEmbeds: true });
@@ -304,11 +306,13 @@ export const Cell = Model.extend({
const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z());
- let z = graph.minZIndex();
+ const layerId = this.layer();
+
+ let z = graph.minZIndex(layerId);
- var collection = graph.get('cells');
+ const layerCells = graph.getCellLayerCells(layerId);
- let shouldUpdate = (collection.toArray().indexOf(sortedCells[0]) !== 0);
+ let shouldUpdate = (layerCells.indexOf(sortedCells[0]) !== 0);
if (!shouldUpdate) {
shouldUpdate = sortedCells.some(function(cell, index) {
return cell.z() !== z + index;
@@ -942,6 +946,31 @@ export const Cell = Model.extend({
.getPointRotatedAroundCenter(this.angle(), x, y)
// Transform the absolute position into relative
.difference(this.position());
+ },
+
+ layer: function(layerId, opt) {
+ // if strictly null unset the layer
+ if (layerId === null) {
+ return this.unset('layer', opt);
+ }
+
+ // if undefined return the current layer id
+ if (layerId === undefined) {
+ layerId = this.get('layer') || null;
+ // If the cell is part of a graph, use the graph's default cell layer.
+ if (layerId == null && this.graph) {
+ layerId = this.graph.getDefaultCellLayer().id;
+ }
+
+ return layerId;
+ }
+
+ // otherwise set the layer id
+ if (!isString(layerId)) {
+ throw new Error('Layer id must be a string.');
+ }
+
+ return this.set('layer', layerId, opt);
}
}, {
diff --git a/packages/joint-core/src/dia/ElementView.mjs b/packages/joint-core/src/dia/ElementView.mjs
index 1f20daa125..79175faaf4 100644
--- a/packages/joint-core/src/dia/ElementView.mjs
+++ b/packages/joint-core/src/dia/ElementView.mjs
@@ -365,7 +365,12 @@ export const ElementView = CellView.extend({
element.startBatch('to-front');
// Bring the model to the front with all his embeds.
- element.toFront({ deep: true, ui: true });
+ if (paper.options.useLayersForEmbedding) {
+ // don't use deep: true, because embedded cells are already on top of the container because of the layer
+ element.toFront({ ui: true });
+ } else {
+ element.toFront({ deep: true, ui: true });
+ }
// Note that at this point cells in the collection are not sorted by z index (it's running in the batch, see
// the dia.Graph._sortOnChangeZ), so we can't assume that the last cell in the collection has the highest z.
diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs
index 41615e5942..efc04e5153 100644
--- a/packages/joint-core/src/dia/Graph.mjs
+++ b/packages/joint-core/src/dia/Graph.mjs
@@ -5,6 +5,7 @@ import { Model } from '../mvc/Model.mjs';
import { Collection } from '../mvc/Collection.mjs';
import { wrappers, wrapWith } from '../util/wrappers.mjs';
import { cloneCells } from '../util/index.mjs';
+import { CellLayersController } from './controllers/CellLayersController.mjs';
const GraphCells = Collection.extend({
@@ -19,7 +20,6 @@ const GraphCells = Collection.extend({
/* eslint-enable no-undef */
}
-
this.graph = opt.graph;
},
@@ -55,12 +55,6 @@ const GraphCells = Collection.extend({
model.graph = null;
}
},
-
- // `comparator` makes it easy to sort cells based on their `z` index.
- comparator: function(model) {
-
- return model.get('z') || 0;
- }
});
@@ -70,23 +64,20 @@ export const Graph = Model.extend({
opt = opt || {};
+ this.cellLayersController = new CellLayersController({ graph: this });
+
// Passing `cellModel` function in the options object to graph allows for
// setting models based on attribute objects. This is especially handy
// when processing JSON graphs that are in a different than JointJS format.
- var cells = new GraphCells([], {
+ this.cellCollection = new GraphCells([], {
model: opt.cellModel,
cellNamespace: opt.cellNamespace,
graph: this
});
- Model.prototype.set.call(this, 'cells', cells);
// Make all the events fired in the `cells` collection available.
// to the outside world.
- cells.on('all', this.trigger, this);
-
- // JointJS automatically doesn't trigger re-sort if models attributes are changed later when
- // they're already in the collection. Therefore, we're triggering sort manually here.
- this.on('change:z', this._sortOnChangeZ, this);
+ this.cellCollection.on('all', this.trigger, this);
// `joint.dia.Graph` keeps an internal data structure (an adjacency list)
// for fast graph queries. All changes that affect the structure of the graph
@@ -112,17 +103,12 @@ export const Graph = Model.extend({
this._batches = {};
- cells.on('add', this._restructureOnAdd, this);
- cells.on('remove', this._restructureOnRemove, this);
- cells.on('reset', this._restructureOnReset, this);
- cells.on('change:source', this._restructureOnChangeSource, this);
- cells.on('change:target', this._restructureOnChangeTarget, this);
- cells.on('remove', this._removeCell, this);
- },
-
- _sortOnChangeZ: function() {
-
- this.get('cells').sort();
+ this.cellCollection.on('add', this._restructureOnAdd, this);
+ this.cellCollection.on('remove', this._restructureOnRemove, this);
+ this.cellCollection.on('reset', this._restructureOnReset, this);
+ this.cellCollection.on('change:source', this._restructureOnChangeSource, this);
+ this.cellCollection.on('change:target', this._restructureOnChangeTarget, this);
+ this.cellCollection.on('remove', this._removeCell, this);
},
_restructureOnAdd: function(cell) {
@@ -157,10 +143,10 @@ export const Graph = Model.extend({
}
},
- _restructureOnReset: function(cells) {
+ _restructureOnReset: function(collection) {
- // Normalize into an array of cells. The original `cells` is GraphCells mvc collection.
- cells = cells.models;
+ // Normalize into an array of cells. The original `collection` is GraphCells mvc collection.
+ const cells = collection.models;
this._out = {};
this._in = {};
@@ -213,7 +199,7 @@ export const Graph = Model.extend({
// JointJS does not recursively call `toJSON()` on attributes that are themselves models/collections.
// It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitly.
var json = Model.prototype.toJSON.apply(this, arguments);
- json.cells = this.get('cells').toJSON(opt.cellAttributes);
+ json.cells = this.cellCollection.toJSON(opt.cellAttributes);
return json;
},
@@ -227,6 +213,15 @@ export const Graph = Model.extend({
return this.set(json, opt);
},
+ get: function(attr) {
+ if (attr === 'cells') {
+ // Backwards compatibility with the old `cells` attribute.
+ // Return the cells collection from the default cell layer.
+ return this.cellLayersController.getDefaultCellLayer().cells;
+ }
+ return Model.prototype.get.call(this, attr);
+ },
+
set: function(key, val, opt) {
var attrs;
@@ -239,21 +234,30 @@ export const Graph = Model.extend({
(attrs = {})[key] = val;
}
+ let cells = attrs.cells;
// Make sure that `cells` attribute is handled separately via resetCells().
- if (attrs.hasOwnProperty('cells')) {
- this.resetCells(attrs.cells, opt);
+ if (cells) {
attrs = util.omit(attrs, 'cells');
}
// The rest of the attributes are applied via original set method.
- return Model.prototype.set.call(this, attrs, opt);
+ // 'cellLayers' attribute is processed in the `cellLayersController`.
+ Model.prototype.set.call(this, attrs, opt);
+
+ // Resetting cells after the `cellLayers` attribute is processed.
+ if (cells) {
+ // Reset the cells collection.
+ this.resetCells(cells, opt);
+ }
+
+ return this;
},
clear: function(opt) {
opt = util.assign({}, opt, { clear: true });
- var collection = this.get('cells');
+ var collection = this.cellCollection;
if (collection.length === 0) return this;
@@ -295,16 +299,12 @@ export const Graph = Model.extend({
return cell;
},
- minZIndex: function() {
-
- var firstCell = this.get('cells').first();
- return firstCell ? (firstCell.get('z') || 0) : 0;
+ minZIndex: function(layerId) {
+ return this.cellLayersController.minZIndex(layerId);
},
- maxZIndex: function() {
-
- var lastCell = this.get('cells').last();
- return lastCell ? (lastCell.get('z') || 0) : 0;
+ maxZIndex: function(layerId) {
+ return this.cellLayersController.maxZIndex(layerId);
},
addCell: function(cell, opt) {
@@ -314,18 +314,7 @@ export const Graph = Model.extend({
return this.addCells(cell, opt);
}
- if (cell instanceof Model) {
-
- if (!cell.has('z')) {
- cell.set('z', this.maxZIndex() + 1);
- }
-
- } else if (cell.z === undefined) {
-
- cell.z = this.maxZIndex() + 1;
- }
-
- this.get('cells').add(this._prepareCell(cell, opt), opt || {});
+ this.cellCollection.add(this._prepareCell(cell, opt), opt || {});
return this;
},
@@ -338,10 +327,10 @@ export const Graph = Model.extend({
opt.maxPosition = opt.position = cells.length - 1;
this.startBatch('add', opt);
- cells.forEach(function(cell) {
+ cells.forEach((cell) => {
this.addCell(cell, opt);
opt.position--;
- }, this);
+ });
this.stopBatch('add', opt);
return this;
@@ -352,10 +341,15 @@ export const Graph = Model.extend({
// Useful for bulk operations and optimizations.
resetCells: function(cells, opt) {
+ this.startBatch('reset', opt);
+
var preparedCells = util.toArray(cells).map(function(cell) {
return this._prepareCell(cell, opt);
}, this);
- this.get('cells').reset(preparedCells, opt);
+
+ this.cellCollection.reset(preparedCells, opt);
+
+ this.stopBatch('reset', opt);
return this;
},
@@ -393,7 +387,7 @@ export const Graph = Model.extend({
// `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is
// then propagated to the graph model. If we didn't remove the cell silently, two `remove` events
// would be triggered on the graph model.
- this.get('cells').remove(cell, { silent: true });
+ this.cellCollection.remove(cell, { silent: true });
},
transferCellEmbeds: function(sourceCell, targetCell, opt = {}) {
@@ -429,35 +423,79 @@ export const Graph = Model.extend({
this.stopBatch(batchName);
},
+ addCellLayer(cellLayer, opt) {
+ this.cellLayersController.addCellLayer(cellLayer, opt);
+ },
+
+ insertCellLayer(cellLayer, insertAt) {
+ this.cellLayersController.insertCellLayer(cellLayer, insertAt);
+ },
+
+ removeCellLayer(cellLayer, opt) {
+ this.cellLayersController.removeCellLayer(cellLayer.id, opt);
+ },
+
+ getDefaultCellLayer() {
+ return this.cellLayersController.getDefaultCellLayer();
+ },
+
+ getCellLayer(layerId) {
+ return this.cellLayersController.getCellLayer(layerId);
+ },
+
+ hasCellLayer(layerId) {
+ return this.cellLayersController.hasCellLayer(layerId);
+ },
+
+ getCellLayers() {
+ return this.cellLayersController.getCellLayers();
+ },
+
+ getCellLayerCells(layerId) {
+ return this.cellLayersController.getCellLayerCells(layerId);
+ },
+
// Get a cell by `id`.
getCell: function(id) {
- return this.get('cells').get(id);
+ return this.cellCollection.get(id);
},
getCells: function() {
-
- return this.get('cells').toArray();
+ // Preserve old order without layers
+ return this.cellLayersController.getCells();
},
getElements: function() {
- return this.get('cells').toArray().filter(cell => cell.isElement());
+ return this.getCells().filter(cell => cell.isElement());
},
getLinks: function() {
- return this.get('cells').toArray().filter(cell => cell.isLink());
+ return this.getCells().filter(cell => cell.isLink());
},
- getFirstCell: function() {
+ getFirstCell: function(layerId) {
+ let cells;
+ if (!layerId) {
+ cells = this.getCells();
+ } else {
+ cells = this.cellLayersController.getCellLayer(layerId).cells;
+ }
- return this.get('cells').first();
+ return cells[0];
},
- getLastCell: function() {
+ getLastCell: function(layerId) {
+ let cells;
+ if (!layerId) {
+ cells = this.getCells();
+ } else {
+ cells = this.cellLayersController.getCellLayer(layerId).cells;
+ }
- return this.get('cells').last();
+ return cells[cells.length - 1];
},
// Get all inbound and outbound links connected to the cell `model`.
diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs
index 52d4bee93b..593d5474f2 100644
--- a/packages/joint-core/src/dia/Paper.mjs
+++ b/packages/joint-core/src/dia/Paper.mjs
@@ -12,7 +12,6 @@ import {
isFunction,
isPlainObject,
getByPath,
- sortElements,
isString,
guid,
normalizeEvent,
@@ -38,16 +37,26 @@ import { ElementView } from './ElementView.mjs';
import { LinkView } from './LinkView.mjs';
import { Cell } from './Cell.mjs';
import { Graph } from './Graph.mjs';
-import { LayersNames, PaperLayer } from './PaperLayer.mjs';
+import { LayerView } from './layers/LayerView.mjs';
+import { CellLayerView } from './layers/CellLayerView.mjs';
import * as highlighters from '../highlighters/index.mjs';
import * as linkAnchors from '../linkAnchors/index.mjs';
import * as connectionPoints from '../connectionPoints/index.mjs';
import * as anchors from '../anchors/index.mjs';
import $ from '../mvc/Dom/index.mjs';
-import { GridLayer } from './layers/GridLayer.mjs';
+import { GridLayerView } from './layers/GridLayerView.mjs';
+import { EmbeddingLayersController } from './controllers/EmbeddingLayersController.mjs';
+
+export const LAYERS = {
+ GRID: 'grid',
+ BACK: 'back',
+ FRONT: 'front',
+ TOOLS: 'tools',
+ LABELS: 'labels'
+};
-const sortingTypes = {
+export const sortingTypes = {
NONE: 'sorting-none',
APPROX: 'sorting-approximate',
EXACT: 'sorting-exact'
@@ -82,20 +91,6 @@ const defaultHighlighting = {
}
};
-const defaultLayers = [{
- name: LayersNames.GRID,
-}, {
- name: LayersNames.BACK,
-}, {
- name: LayersNames.CELLS,
-}, {
- name: LayersNames.LABELS,
-}, {
- name: LayersNames.FRONT
-}, {
- name: LayersNames.TOOLS
-}];
-
export const Paper = View.extend({
className: 'paper',
@@ -173,7 +168,7 @@ export const Paper = View.extend({
// }
defaultLink: function() {
// Do not create hard dependency on the joint.shapes.standard namespace (by importing the standard.Link model directly)
- const { cellNamespace } = this.model.get('cells');
+ const { cellNamespace } = this.model.cellCollection;
const ctor = getByPath(cellNamespace, ['standard', 'Link']);
if (!ctor) throw new Error('dia.Paper: no default link model found. Use `options.defaultLink` to specify a default link model.');
return new ctor();
@@ -293,6 +288,11 @@ export const Paper = View.extend({
cellViewNamespace: null,
+ layerViewNamespace: {
+ 'GridLayerView': GridLayerView,
+ 'CellLayerView': CellLayerView,
+ },
+
routerNamespace: null,
connectorNamespace: null,
@@ -305,7 +305,7 @@ export const Paper = View.extend({
connectionPointNamespace: connectionPoints,
- overflow: false
+ overflow: false,
},
events: {
@@ -364,7 +364,6 @@ export const Paper = View.extend({
// Paper Layers
_layers: null,
- SORT_DELAYING_BATCHES: ['add', 'to-front', 'to-back'],
UPDATE_DELAYING_BATCHES: ['translate'],
// If you interact with these elements,
// the default interaction such as `element move` is prevented.
@@ -387,9 +386,6 @@ export const Paper = View.extend({
// to mitigate the differences between the model and view geometry.
DEFAULT_FIND_BUFFER: 200,
- // Default layer to insert the cell views into.
- DEFAULT_CELL_LAYER: LayersNames.CELLS,
-
init: function() {
const { options } = this;
@@ -401,18 +397,38 @@ export const Paper = View.extend({
const model = this.model = options.model || new Graph;
+ // Paper layers except the cell layers.
+ this._layersSettings = [{
+ id: LAYERS.GRID,
+ type: 'GridLayerView',
+ patterns: this.constructor.gridPatterns
+ }, {
+ id: LAYERS.BACK,
+ }, {
+ id: LAYERS.LABELS,
+ }, {
+ id: LAYERS.FRONT
+ }, {
+ id: LAYERS.TOOLS
+ }];
+
// Layers (SVGGroups)
this._layers = {
viewsMap: {},
- namesMap: {},
order: [],
};
+ // current cell layers model attributes from the Graph
+ this._cellLayers = [];
this.cloneOptions();
this.render();
this._setDimensions();
this.startListening();
+ if (options.useLayersForEmbedding) {
+ this.embeddingLayersController = new EmbeddingLayersController({ graph: model, paper: this });
+ }
+
// Hash of all cell views.
this._views = {};
@@ -423,7 +439,7 @@ export const Paper = View.extend({
};
// Render existing cells in the graph
- this.resetViews(model.attributes.cells.models);
+ this.resetViews(model.cellCollection.models);
// Start the Rendering Loop
if (!this.isFrozen() && this.isAsync()) this.updateViewsAsync();
},
@@ -451,10 +467,10 @@ export const Paper = View.extend({
var model = this.model;
this.listenTo(model, 'add', this.onCellAdded)
.listenTo(model, 'remove', this.onCellRemoved)
- .listenTo(model, 'change', this.onCellChange)
.listenTo(model, 'reset', this.onGraphReset)
- .listenTo(model, 'sort', this.onGraphSort)
- .listenTo(model, 'batch:stop', this.onGraphBatchStop);
+ .listenTo(model, 'batch:stop', this.onGraphBatchStop)
+ .listenTo(model, 'layers:update', this.updateCellLayers);
+
this.on('cell:highlight', this.onCellHighlight)
.on('cell:unhighlight', this.onCellUnhighlight)
.on('transform', this.update);
@@ -476,25 +492,9 @@ export const Paper = View.extend({
if (view) this.requestViewUpdate(view, view.FLAG_REMOVE, view.UPDATE_PRIORITY, opt);
},
- onCellChange: function(cell, opt) {
- if (cell === this.model.attributes.cells) return;
- if (
- cell.hasChanged('layer') ||
- (cell.hasChanged('z') && this.options.sorting === sortingTypes.APPROX)
- ) {
- const view = this.findViewByModel(cell);
- if (view) this.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
- }
- },
-
- onGraphReset: function(collection, opt) {
- this.resetLayers();
- this.resetViews(collection.models, opt);
- },
-
- onGraphSort: function() {
- if (this.model.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return;
- this.sortViews();
+ onGraphReset: function(_collection, opt) {
+ this.resetLayerViews();
+ this.resetViews(this.model.getCells(), opt);
},
onGraphBatchStop: function(data) {
@@ -507,10 +507,28 @@ export const Paper = View.extend({
this.updateViews(data);
}
}
- var sortDelayingBatches = this.SORT_DELAYING_BATCHES;
- if (sortDelayingBatches.includes(name) && !graph.hasActiveBatch(sortDelayingBatches)) {
- this.sortViews();
- }
+ },
+
+ updateCellLayers: function(cellLayers) {
+ const removedCellLayerViewIds = this._cellLayers.filter(cellLayerView => !cellLayers.some(l => l.id === cellLayerView.id)).map(cellLayerView => cellLayerView.id);
+ removedCellLayerViewIds.forEach(cellLayerViewId => this.requestLayerViewRemove(cellLayerViewId));
+
+ // reverse cellLayers array to render it in order
+ [...cellLayers].reverse().forEach(cellLayer => {
+ if (!this.hasLayerView(cellLayer.id)) {
+ const cellLayerModel = this.model.getCellLayer(cellLayer.id);
+
+ this.renderLayerView({
+ id: cellLayer.id,
+ model: cellLayerModel
+ });
+ }
+
+ const layerView = this.getLayerView(cellLayer.id);
+ this.insertLayerView(layerView, LAYERS.FRONT);
+ });
+
+ this._cellLayers = cellLayers;
},
cloneOptions: function() {
@@ -597,121 +615,136 @@ export const Paper = View.extend({
}];
},
- hasLayerView(layerName) {
- return (layerName in this._layers.viewsMap);
+ hasLayerView(layerId) {
+ return (layerId in this._layers.viewsMap);
},
- getLayerView(layerName) {
+ getLayerView(layerId) {
const { _layers: { viewsMap }} = this;
- if (layerName in viewsMap) return viewsMap[layerName];
- throw new Error(`dia.Paper: Unknown layer "${layerName}".`);
+ if (layerId in viewsMap) {
+ return viewsMap[layerId];
+ }
+ throw new Error(`dia.Paper: Unknown layer view "${layerId}".`);
},
- getLayerNode(layerName) {
- return this.getLayerView(layerName).el;
+ getLayerViewNode(layerId) {
+ return this.getLayerView(layerId).el;
},
- _removeLayer(layerView) {
- this._unregisterLayer(layerView);
+ _removeLayerView(layerView) {
+ this._unregisterLayerView(layerView);
layerView.remove();
},
- _unregisterLayer(layerView) {
- const { _layers: { viewsMap, namesMap, order }} = this;
- const layerName = this._getLayerName(layerView);
- order.splice(order.indexOf(layerName), 1);
- delete namesMap[layerView.cid];
- delete viewsMap[layerName];
- },
+ _unregisterLayerView(layerView) {
+ const { _layers: { viewsMap, order }} = this;
+ const layerId = layerView.id;
- _registerLayer(layerName, layerView, beforeLayerView) {
- const { _layers: { viewsMap, namesMap, order }} = this;
- if (beforeLayerView) {
- const beforeLayerName = this._getLayerName(beforeLayerView);
- order.splice(order.indexOf(beforeLayerName), 0, layerName);
- } else {
- order.push(layerName);
+ if (order.indexOf(layerId) !== -1) {
+ order.splice(order.indexOf(layerId), 1);
}
- viewsMap[layerName] = layerView;
- namesMap[layerView.cid] = layerName;
+
+ delete viewsMap[layerId];
},
- _getLayerView(layer) {
- const { _layers: { namesMap, viewsMap }} = this;
- if (layer instanceof PaperLayer) {
- if (layer.cid in namesMap) return layer;
- return null;
+ _registerLayerView(layerView) {
+ if (!(layerView instanceof LayerView)) {
+ throw new Error('dia.Paper: The layer view must be an instance of dia.LayerView.');
}
- if (layer in viewsMap) return viewsMap[layer];
- return null;
- },
- _getLayerName(layerView) {
- const { _layers: { namesMap }} = this;
- return namesMap[layerView.cid];
+ if (this.hasLayerView(layerView.id)) {
+ throw new Error(`dia.Paper: The layer view "${layerView.id}" already exists.`);
+ }
+
+ const { _layers: { viewsMap }} = this;
+ const layerId = layerView.id;
+
+ viewsMap[layerId] = layerView;
},
- _requireLayerView(layer) {
- const layerView = this._getLayerView(layer);
- if (!layerView) {
- if (layer instanceof PaperLayer) {
- throw new Error('dia.Paper: The layer is not registered.');
- } else {
- throw new Error(`dia.Paper: Unknown layer "${layer}".`);
- }
+ _requireLayerView(layerView) {
+ let layerId;
+ if (isString(layerView)) {
+ layerId = layerView;
+ } else if (layerView instanceof LayerView) {
+ layerId = layerView.id;
+ } else {
+ throw new Error('dia.Paper: The layer view must be provided.');
}
- return layerView;
- },
- hasLayer(layer) {
- return this._getLayerView(layer) !== null;
+ if (!this.hasLayerView(layerId)) {
+ throw new Error(`dia.Paper: Unknown layer view "${layerId}".`);
+ }
+ return this.getLayerView(layerId);
},
- removeLayer(layer) {
- const layerView = this._requireLayerView(layer);
+ removeLayerView(layerView) {
+ layerView = this._requireLayerView(layerView);
+
if (!layerView.isEmpty()) {
- throw new Error('dia.Paper: The layer is not empty.');
+ throw new Error('dia.Paper: The layer view is not empty.');
}
- this._removeLayer(layerView);
+ this._removeLayerView(layerView);
},
- addLayer(layerName, layerView, options = {}) {
- if (!layerName || typeof layerName !== 'string') {
- throw new Error('dia.Paper: The layer name must be provided.');
- }
- if (this._getLayerView(layerName)) {
- throw new Error(`dia.Paper: The layer "${layerName}" already exists.`);
+ requestLayerViewRemove(layerView) {
+ layerView = this._requireLayerView(layerView);
+
+ // TODO: change when after view management merge
+ const { FLAG_REMOVE, UPDATE_PRIORITY } = layerView;
+
+ this.requestViewUpdate(layerView, FLAG_REMOVE, UPDATE_PRIORITY);
+ },
+
+ insertLayerView(layerView, insertBefore) {
+ if (!(layerView instanceof LayerView)) {
+ throw new Error('dia.Paper: The layer view must be an instance of dia.LayerView.');
}
- if (!(layerView instanceof PaperLayer)) {
- throw new Error('dia.Paper: The layer view is not an instance of dia.PaperLayer.');
+
+ const layerId = layerView.id;
+
+ if (!this.hasLayerView(layerId)) {
+ throw new Error(`dia.Paper: Unknown layer view "${layerId}".`);
}
- const { insertBefore } = options;
+
+ const { _layers: { order }} = this;
+
if (!insertBefore) {
- this._registerLayer(layerName, layerView, null);
+ // remove from order in case of a move command
+ if (order.indexOf(layerId) !== -1) {
+ order.splice(order.indexOf(layerId), 1);
+ }
+ order.push(layerId);
this.layers.appendChild(layerView.el);
} else {
const beforeLayerView = this._requireLayerView(insertBefore);
- this._registerLayer(layerName, layerView, beforeLayerView);
+ const beforeLayerViewId = beforeLayerView.id;
+ if (layerId === beforeLayerViewId) {
+ // The layer view is already in the right place.
+ return;
+ }
+
+ let beforeLayerPosition = order.indexOf(beforeLayerViewId);
+ // remove from order in case of a move command
+ if (order.indexOf(layerId) !== -1) {
+ if (order.indexOf(layerId) < beforeLayerPosition) {
+ beforeLayerPosition -= 1;
+ }
+ order.splice(order.indexOf(layerId), 1);
+ }
+ order.splice(beforeLayerPosition, 0, layerId);
this.layers.insertBefore(layerView.el, beforeLayerView.el);
}
},
- moveLayer(layer, insertBefore) {
- const layerView = this._requireLayerView(layer);
- if (layerView === this._getLayerView(insertBefore)) return;
- const layerName = this._getLayerName(layerView);
- this._unregisterLayer(layerView);
- this.addLayer(layerName, layerView, { insertBefore });
- },
-
- getLayerNames() {
- // Returns a sorted array of layer names.
+ getLayerViewOrder() {
+ // Returns a sorted array of layer view ids.
return this._layers.order.slice();
},
- getLayers() {
- // Returns a sorted array of layer views.
- return this.getLayerNames().map(name => this.getLayerView(name));
+ getOrderedLayerViews() {
+ // Returns a sorted array of ordered layer views.
+ return this.getLayerViewOrder().map(id => this.getLayerView(id));
},
render: function() {
@@ -727,7 +760,7 @@ export const Paper = View.extend({
this.defs = defs;
this.layers = layers;
- this.renderLayers();
+ this.renderLayerViews();
V.ensureId(svg);
@@ -749,28 +782,53 @@ export const Paper = View.extend({
V(this.svg).prepend(V.createSVGStyle(css));
},
- createLayer(name) {
- switch (name) {
- case LayersNames.GRID:
- return new GridLayer({ name, paper: this, patterns: this.constructor.gridPatterns });
- default:
- return new PaperLayer({ name });
+ createLayerView(options) {
+ if (options.id == null) {
+ throw new Error('dia.Paper: Layer view id is required.');
}
+
+ options.paper = this;
+
+ let type = options.type;
+
+ if (options.model) {
+ const modelType = options.model.get('type') || options.model.constructor.name;
+ type = modelType + 'View';
+ }
+
+ const viewConstructor = this.options.layerViewNamespace[type] || LayerView;
+
+ return new viewConstructor(options);
},
- renderLayer: function(name) {
- const layerView = this.createLayer(name);
- this.addLayer(name, layerView);
+ renderLayerView: function(options) {
+ if (options == null) {
+ throw new Error('dia.Paper: Layer view options are required.');
+ }
+
+ const layerView = this.createLayerView(options);
+ this.addLayerView(layerView);
+ return layerView;
+ },
+
+ addLayerView: function(layerView) {
+ this._registerLayerView(layerView);
return layerView;
},
- renderLayers: function(layers = defaultLayers) {
- this.removeLayers();
- layers.forEach(({ name }) => this.renderLayer(name));
+ renderLayerViews: function() {
+ this.removeLayerViews();
+ // Render the paper layers.
+ this._layersSettings.forEach(options => {
+ const layerView = this.renderLayerView(options);
+ this.insertLayerView(layerView);
+ });
+ // Render the cell layers.
+ this.updateCellLayers(this.model.get('cellLayers'));
// Throws an exception if doesn't exist
- const cellsLayerView = this.getLayerView(LayersNames.CELLS);
- const toolsLayerView = this.getLayerView(LayersNames.TOOLS);
- const labelsLayerView = this.getLayerView(LayersNames.LABELS);
+ const cellsLayerView = this.getLayerView(this.model.getDefaultCellLayer().id);
+ const toolsLayerView = this.getLayerView(LAYERS.TOOLS);
+ const labelsLayerView = this.getLayerView(LAYERS.LABELS);
// backwards compatibility
this.tools = toolsLayerView.el;
this.cells = this.viewport = cellsLayerView.el;
@@ -783,12 +841,12 @@ export const Paper = View.extend({
labelsLayerView.el.style.userSelect = 'none';
},
- removeLayers: function() {
+ removeLayerViews: function() {
const { _layers: { viewsMap }} = this;
- Object.values(viewsMap).forEach(layerView => this._removeLayer(layerView));
+ Object.values(viewsMap).forEach(layerView => this._removeLayerView(layerView));
},
- resetLayers: function() {
+ resetLayerViews: function() {
const { _layers: { viewsMap }} = this;
Object.values(viewsMap).forEach(layerView => layerView.removePivots());
},
@@ -1041,6 +1099,12 @@ export const Paper = View.extend({
updateView: function(view, flag, opt) {
if (!view) return 0;
const { FLAG_REMOVE, FLAG_INSERT, FLAG_INIT, model } = view;
+ if (view instanceof CellLayerView) {
+ if (flag & FLAG_REMOVE) {
+ this.removeLayerView(view);
+ return 0;
+ }
+ }
if (view instanceof CellView) {
if (flag & FLAG_REMOVE) {
this.removeView(model);
@@ -1449,7 +1513,7 @@ export const Paper = View.extend({
}
this.options.frozen = updates.keyFrozen = false;
if (updates.sort) {
- this.sortViews();
+ this.sortLayers();
updates.sort = false;
}
},
@@ -1471,8 +1535,12 @@ export const Paper = View.extend({
this.freeze();
this._updates.disabled = true;
//clean up all DOM elements/views to prevent memory leaks
- this.removeLayers();
+ this.removeLayerViews();
this.removeViews();
+
+ if (this.embeddingLayersController) {
+ this.embeddingLayersController.stopListening();
+ }
},
getComputedSize: function() {
@@ -1796,7 +1864,7 @@ export const Paper = View.extend({
return new ViewClass({
model: cell,
interactive: options.interactive,
- labelsLayer: options.labelsLayer === true ? LayersNames.LABELS : options.labelsLayer
+ labelsLayer: options.labelsLayer === true ? LAYERS.LABELS : options.labelsLayer
});
},
@@ -1863,7 +1931,7 @@ export const Paper = View.extend({
this.renderView(cells[i], opt);
}
this.unfreeze({ key });
- this.sortViews();
+ this.sortLayers();
},
removeViews: function() {
@@ -1873,8 +1941,7 @@ export const Paper = View.extend({
this._views = {};
},
- sortViews: function() {
-
+ sortLayers: function() {
if (!this.isExactSorting()) {
// noop
return;
@@ -1884,41 +1951,25 @@ export const Paper = View.extend({
this._updates.sort = true;
return;
}
- this.sortViewsExact();
+ this.sortLayersExact();
},
- sortViewsExact: function() {
-
- // Run insertion sort algorithm in order to efficiently sort DOM elements according to their
- // associated model `z` attribute.
-
- var cellNodes = Array.from(this.cells.childNodes).filter(node => node.getAttribute('model-id'));
- var cells = this.model.get('cells');
+ sortLayersExact: function() {
+ const { _layers: { viewsMap }} = this;
- sortElements(cellNodes, function(a, b) {
- var cellA = cells.get(a.getAttribute('model-id'));
- var cellB = cells.get(b.getAttribute('model-id'));
- var zA = cellA.attributes.z || 0;
- var zB = cellB.attributes.z || 0;
- return (zA === zB) ? 0 : (zA < zB) ? -1 : 1;
- });
+ Object.values(viewsMap).filter(view => view instanceof CellLayerView).forEach(view => view.sortExact());
},
insertView: function(view, isInitialInsert) {
- const { el, model } = view;
+ const { model } = view;
- const layerName = model.get('layer') || this.DEFAULT_CELL_LAYER;
+ const layerName = model.layer();
const layerView = this.getLayerView(layerName);
- switch (this.options.sorting) {
- case sortingTypes.APPROX:
- layerView.insertSortedNode(el, model.get('z'));
- break;
- case sortingTypes.EXACT:
- default:
- layerView.insertNode(el);
- break;
- }
+ layerView.insertCellView(view);
+
+ this.trigger('cell:inserted', view, isInitialInsert);
+
view.onMount(isInitialInsert);
},
@@ -2813,13 +2864,13 @@ export const Paper = View.extend({
options.gridSize = gridSize;
if (options.drawGrid && !options.drawGridSize) {
// Do not redraw the grid if the `drawGridSize` is set.
- this.getLayerView(LayersNames.GRID).renderGrid();
+ this.getLayerView(LAYERS.GRID).renderGrid();
}
return this;
},
setGrid: function(drawGrid) {
- this.getLayerView(LayersNames.GRID).setGrid(drawGrid);
+ this.getLayerView(LAYERS.GRID).setGrid(drawGrid);
return this;
},
@@ -3168,7 +3219,7 @@ export const Paper = View.extend({
sorting: sortingTypes,
- Layers: LayersNames,
+ Layers: LAYERS,
backgroundPatterns: {
@@ -3276,7 +3327,6 @@ export const Paper = View.extend({
return canvas;
}
},
-
gridPatterns: {
dot: [{
color: '#AAAAAA',
diff --git a/packages/joint-core/src/dia/ToolsView.mjs b/packages/joint-core/src/dia/ToolsView.mjs
index fcce15b1dd..8a60d9e576 100644
--- a/packages/joint-core/src/dia/ToolsView.mjs
+++ b/packages/joint-core/src/dia/ToolsView.mjs
@@ -1,7 +1,7 @@
import * as mvc from '../mvc/index.mjs';
import * as util from '../util/index.mjs';
import { CellView } from './CellView.mjs';
-import { LayersNames } from './PaperLayer.mjs';
+import { LAYERS } from './Paper.mjs';
import { ToolView } from './ToolView.mjs';
export const ToolsView = mvc.View.extend({
@@ -14,7 +14,7 @@ export const ToolsView = mvc.View.extend({
tools: null,
relatedView: null,
name: null,
- // layer?: LayersNames.TOOLS
+ // layer?: LAYERS.TOOLS
// z?: number
},
@@ -140,7 +140,7 @@ export const ToolsView = mvc.View.extend({
mount: function() {
const { options, el } = this;
- const { relatedView, layer = LayersNames.TOOLS, z } = options;
+ const { relatedView, layer = LAYERS.TOOLS, z } = options;
if (relatedView) {
if (layer) {
relatedView.paper.getLayerView(layer).insertSortedNode(el, z);
diff --git a/packages/joint-core/src/dia/controllers/CellLayersController.mjs b/packages/joint-core/src/dia/controllers/CellLayersController.mjs
new file mode 100644
index 0000000000..957b3216d8
--- /dev/null
+++ b/packages/joint-core/src/dia/controllers/CellLayersController.mjs
@@ -0,0 +1,252 @@
+import { Listener } from '../../mvc/Listener.mjs';
+import { CellLayer } from '../groups/CellLayer.mjs';
+
+export class CellLayersController extends Listener {
+
+ constructor(context) {
+ super(context);
+
+ this.graph = context.graph;
+
+ this.defaultCellLayerId = 'cells';
+
+ this.cellLayersMap = {};
+ this.cellLayerAttributes = this.processGraphCellLayersAttribute(this.graph.get('cellLayers'));
+
+ this.startListening();
+ }
+
+ startListening() {
+ const { graph } = this;
+
+ this.listenTo(graph, 'add', (_context, cell) => {
+ this.onAdd(cell);
+ });
+
+ this.listenTo(graph, 'remove', (_context, cell) => {
+ this.onRemove(cell);
+ });
+
+ this.graph.listenTo(graph, 'change:cellLayers', (_context, cellLayers, opt) => {
+ if (opt.controller) {
+ return; // do not process changes triggered by this controller
+ }
+
+ this.cellLayersAttributes = this.processGraphCellLayersAttribute(cellLayers);
+ });
+
+ this.listenTo(graph, 'reset', (_context, { models: cells }) => {
+ const { cellLayersMap } = this;
+
+ for (let layerId in cellLayersMap) {
+ cellLayersMap[layerId].reset();
+ }
+
+ cells.forEach(cell => {
+ this.onAdd(cell, true);
+ });
+ });
+
+ this.listenTo(graph, 'change:layer', (_context, cell, layerId, opt) => {
+ if (!layerId) {
+ layerId = this.defaultCellLayerId;
+ }
+ const previousLayerId = cell.previous('layer') || this.defaultCellLayerId;
+
+ if (previousLayerId === layerId) {
+ return; // no change
+ }
+
+ const previousLayer = this.getCellLayer(previousLayerId);
+ previousLayer.remove(cell, opt);
+
+ const layer = this.getCellLayer(layerId);
+ layer.add(cell, opt);
+ });
+ }
+
+ processGraphCellLayersAttribute(cellLayers = []) {
+ const cellLayerAttributes = cellLayers;
+
+ const defaultLayers = cellLayerAttributes.filter(attrs => attrs.default === true);
+
+ if (defaultLayers.length > 1) {
+ throw new Error('dia.Graph: Only one default cell layer can be defined.');
+ }
+ // if no default layer is defined, create one
+ if (defaultLayers.length === 0) {
+ cellLayerAttributes.push({
+ id: this.defaultCellLayerId,
+ default: true
+ });
+ }
+ if (defaultLayers.length === 1) {
+ this.defaultCellLayerId = defaultLayers[0].id;
+ }
+
+ cellLayerAttributes.forEach(attributes => {
+ const cellLayer = this.createCellLayer(attributes);
+ this.cellLayersMap[attributes.id] = cellLayer;
+ });
+
+ this.graph.set('cellLayers', cellLayerAttributes, { controller: this });
+ this.graph.trigger('layers:update', cellLayerAttributes);
+ return cellLayerAttributes;
+ }
+
+ createCellLayer(attributes) {
+ const cellLayer = new CellLayer(attributes);
+
+ return cellLayer;
+ }
+
+ onAdd(cell, reset = false) {
+ const layerId = cell.layer() || this.defaultCellLayerId;
+ const layer = this.getCellLayer(layerId);
+
+ // compatibility
+ // in the version before groups, z-index was not set on reset
+ if (!reset) {
+ if (!cell.has('z')) {
+ cell.set('z', layer.maxZIndex() + 1);
+ }
+ }
+
+ // add to the layer without triggering rendering update
+ // when the cell is just added to the graph, it will be rendered normally by the paper
+ layer.add(cell, { initial: true });
+ }
+
+ onRemove(cell) {
+ const layerId = cell.layer() || this.defaultCellLayerId;
+
+ if (this.hasCellLayer(layerId)) {
+ const layer = this.getCellLayer(layerId);
+ layer.remove(cell);
+ }
+ }
+
+ getDefaultCellLayer() {
+ return this.cellLayersMap[this.defaultCellLayerId];
+ }
+
+ addCellLayer(cellLayer, _opt) {
+ const { cellLayersMap } = this;
+
+ if (this.hasCellLayer(cellLayer.id)) {
+ throw new Error(`dia.Graph: Layer with id '${cellLayer.id}' already exists.`);
+ }
+
+ cellLayersMap[cellLayer.id] = cellLayer;
+ }
+
+ insertCellLayer(cellLayer, insertAt) {
+ if (!this.hasCellLayer(cellLayer.id)) {
+ throw new Error(`dia.Graph: Layer with id '${cellLayer.id}' does not exist.`);
+ }
+
+ const id = cellLayer.id;
+
+ const currentIndex = this.cellLayerAttributes.findIndex(attrs => attrs.id === id);
+ let attributes;
+ if (currentIndex !== -1) {
+ attributes = this.cellLayerAttributes[currentIndex];
+ this.cellLayerAttributes.splice(currentIndex, 1); // remove existing layer attributes
+ } else {
+ attributes = {
+ id
+ };
+ }
+
+ if (insertAt == null) {
+ insertAt = this.cellLayerAttributes.length;
+ }
+
+ this.cellLayerAttributes.splice(insertAt, 0, attributes);
+
+ this.graph.set('cellLayers', this.cellLayerAttributes, { controller: this });
+ this.graph.trigger('layers:update', this.cellLayerAttributes);
+ }
+
+ removeCellLayer(layerId, _opt) {
+ const { cellLayersMap, defaultCellLayerId } = this;
+
+ if (layerId === defaultCellLayerId) {
+ throw new Error('dia.Graph: default layer cannot be removed.');
+ }
+
+ if (!this.hasCellLayer(layerId)) {
+ throw new Error(`dia.Graph: Layer with id '${layerId}' does not exist.`);
+ }
+
+ const layer = this.getCellLayer(layerId);
+ // reset the layer to remove all cells from it
+ layer.reset();
+
+ delete cellLayersMap[layerId];
+
+ // remove from the layers array
+ if (this.cellLayerAttributes.some(attrs => attrs.id === layerId)) {
+ this.cellLayerAttributes = this.cellLayerAttributes.filter(l => l.id !== layerId);
+
+ this.graph.set('cellLayers', this.cellLayerAttributes, { controller: this });
+ this.graph.trigger('layers:update', this.cellLayerAttributes);
+ }
+ }
+
+ minZIndex(layerId) {
+ const { defaultCellLayerId } = this;
+
+ layerId = layerId || defaultCellLayerId;
+
+ const layer = this.getCellLayer(layerId);
+
+ return layer.minZIndex();
+ }
+
+ maxZIndex(layerId) {
+ const { defaultCellLayerId } = this;
+
+ layerId = layerId || defaultCellLayerId;
+
+ const layer = this.getCellLayer(layerId);
+
+ return layer.maxZIndex();
+ }
+
+ hasCellLayer(layerId) {
+ return !!this.cellLayersMap[layerId];
+ }
+
+ getCellLayer(layerId) {
+ if (!this.cellLayersMap[layerId]) {
+ throw new Error(`dia.Graph: Cell layer with id '${layerId}' does not exist.`);
+ }
+
+ return this.cellLayersMap[layerId];
+ }
+
+ getCellLayers() {
+ const cellLayers = [];
+
+ for (let layerId in this.cellLayersMap) {
+ cellLayers.push(this.cellLayersMap[layerId]);
+ }
+
+ return cellLayers;
+ }
+
+ getCellLayerCells(layerId) {
+ return this.cellLayersMap[layerId].cells.toArray();
+ }
+
+ getCells() {
+ const cells = [];
+
+ for (let layerId in this.cellLayersMap) {
+ cells.push(...this.cellLayersMap[layerId].cells);
+ }
+
+ return cells;
+ }
+}
diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs
new file mode 100644
index 0000000000..7bc147420a
--- /dev/null
+++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs
@@ -0,0 +1,87 @@
+import { Listener } from '../../mvc/Listener.mjs';
+import { CellLayer } from '../groups/CellLayer.mjs';
+
+export class EmbeddingLayersController extends Listener {
+
+ constructor(context) {
+ super(context);
+
+ this.graph = context.graph;
+ this.paper = context.paper;
+
+ this.startListening();
+ }
+
+ startListening() {
+ const { graph, paper } = this;
+
+ this.listenTo(graph, 'remove', (_context, cell) => {
+ if (graph.hasCellLayer(cell.id)) {
+ graph.removeCellLayer(cell.id);
+ paper.requestLayerViewRemove(cell.id);
+ }
+ });
+
+ this.listenTo(graph, 'add', (_context, cell) => {
+ const parentId = cell.get('parent');
+
+ if (parentId) {
+ this.onParentChange(cell, parentId);
+ }
+ });
+
+ this.listenTo(graph, 'reset', (_context, { models: cells }) => {
+ cells.forEach(cell => {
+ const parentId = cell.get('parent');
+
+ if (parentId) {
+ this.onParentChange(cell, cell.get('parent'));
+ }
+ });
+ });
+
+ this.listenTo(graph, 'change:parent', (_context, cell, parentId) => {
+ this.onParentChange(cell, parentId);
+ });
+
+ this.listenTo(paper, 'cell:inserted', (_context, cellView) => {
+ const cellId = cellView.model.id;
+ if (paper.hasLayerView(cellId)) {
+ this.insertEmbeddedLayer(cellView);
+ }
+ });
+ }
+
+ onParentChange(cell, parentId) {
+ const { paper, graph } = this;
+
+ if (parentId) {
+ // Create new layer if it's not exist
+ if (!graph.hasCellLayer(parentId)) {
+ const cellLayer = new CellLayer({
+ id: parentId
+ });
+ graph.addCellLayer(cellLayer);
+ paper.renderLayerView({
+ id: parentId,
+ model: cellLayer,
+ });
+
+ const cellView = paper.findViewByModel(parentId);
+ if (cellView.isMounted()) {
+ this.insertEmbeddedLayer(cellView);
+ }
+ }
+
+ cell.layer(parentId); // Set the layer for the cell
+ } else {
+ cell.layer(null); // Move to the default layer
+ }
+ }
+
+ insertEmbeddedLayer(cellView) {
+ const cellId = cellView.model.id;
+ const layerView = this.paper.getLayerView(cellId);
+ cellView.el.after(layerView.el);
+ }
+}
diff --git a/packages/joint-core/src/dia/groups/CellGroup.mjs b/packages/joint-core/src/dia/groups/CellGroup.mjs
new file mode 100644
index 0000000000..3f7f7a083a
--- /dev/null
+++ b/packages/joint-core/src/dia/groups/CellGroup.mjs
@@ -0,0 +1,61 @@
+import { Model, Collection } from '../../mvc/index.mjs';
+
+export class CellGroupCollection extends Collection {
+
+ _prepareModel(attrs, _options) {
+ if (this._isModel(attrs)) {
+ // do not change collection attribute of a cell model
+ return attrs;
+ } else {
+ throw new Error('CellGroupCollection only accepts Cell instances.');
+ }
+ }
+
+ _onModelEvent(event, model, collection, options) {
+ if (model) {
+ if (event === 'changeId') {
+ var prevId = this.modelId(model.previousAttributes(), model.idAttribute);
+ var id = this.modelId(model.attributes, model.idAttribute);
+ if (prevId != null) delete this._byId[prevId];
+ if (id != null) this._byId[id] = model;
+ }
+ }
+ arguments[0] = 'cell:' + event;
+ //retrigger model events with the `cell:` prefix
+ this.trigger.apply(this, arguments);
+ }
+}
+
+export class CellGroup extends Model {
+
+ defaults() {
+ return {
+ type: 'CellGroup',
+ collectionConstructor: CellGroupCollection,
+ };
+ }
+
+ initialize(attrs) {
+ super.initialize(attrs);
+
+ this.cells = new this.attributes.collectionConstructor();
+
+ // Make all the events fired in the `cells` collection available.
+ // to the outside world.
+ this.cells.on('all', this.trigger, this);
+ }
+
+ add(cell, opt) {
+ this.cells.add(cell, opt);
+ }
+
+ remove(cell, opt) {
+ this.cells.remove(cell, opt);
+ }
+
+ reset() {
+ this.cells.toArray().forEach(cell => {
+ this.remove(cell);
+ });
+ }
+}
diff --git a/packages/joint-core/src/dia/groups/CellLayer.mjs b/packages/joint-core/src/dia/groups/CellLayer.mjs
new file mode 100644
index 0000000000..1f274dc901
--- /dev/null
+++ b/packages/joint-core/src/dia/groups/CellLayer.mjs
@@ -0,0 +1,37 @@
+import { CellGroupCollection, CellGroup } from './CellGroup.mjs';
+
+export class CellLayerCollection extends CellGroupCollection {
+
+ // `comparator` makes it easy to sort cells based on their `z` index.
+ comparator(model) {
+ return model.get('z') || 0;
+ }
+}
+
+export class CellLayer extends CellGroup {
+
+ defaults() {
+ return {
+ type: 'CellLayer',
+ collectionConstructor: CellLayerCollection,
+ };
+ }
+
+ initialize(attrs) {
+ super.initialize(attrs);
+
+ this.cells.on('cell:change:z', () => {
+ this.cells.sort();
+ });
+ }
+
+ minZIndex() {
+ const firstCell = this.cells.first();
+ return firstCell ? (firstCell.get('z') || 0) : 0;
+ }
+
+ maxZIndex() {
+ const lastCell = this.cells.last();
+ return lastCell ? (lastCell.get('z') || 0) : 0;
+ }
+}
diff --git a/packages/joint-core/src/dia/index.mjs b/packages/joint-core/src/dia/index.mjs
index 9a25dc2fb6..b7431c973f 100644
--- a/packages/joint-core/src/dia/index.mjs
+++ b/packages/joint-core/src/dia/index.mjs
@@ -1,6 +1,5 @@
export * from './Graph.mjs';
export * from './attributes/index.mjs';
-export * from './PaperLayer.mjs';
export * from './Cell.mjs';
export * from './CellView.mjs';
export * from './Element.mjs';
@@ -11,3 +10,8 @@ export * from './Paper.mjs';
export * from './ToolView.mjs';
export * from './ToolsView.mjs';
export * from './HighlighterView.mjs';
+export * from './layers/CellLayerView.mjs';
+export * from './layers/LayerView.mjs';
+export * from './layers/GridLayerView.mjs';
+export * from './groups/CellGroup.mjs';
+export * from './groups/CellLayer.mjs';
diff --git a/packages/joint-core/src/dia/layers/CellLayerView.mjs b/packages/joint-core/src/dia/layers/CellLayerView.mjs
new file mode 100644
index 0000000000..0fe708b4fa
--- /dev/null
+++ b/packages/joint-core/src/dia/layers/CellLayerView.mjs
@@ -0,0 +1,98 @@
+import { LayerView } from './LayerView.mjs';
+import { sortElements } from '../../util/index.mjs';
+import { sortingTypes } from '../Paper.mjs';
+
+export const CellLayerView = LayerView.extend({
+
+ SORT_DELAYING_BATCHES: ['add', 'reset', 'to-front', 'to-back'],
+
+ init() {
+ LayerView.prototype.init.apply(this, arguments);
+
+ this.startListening();
+ },
+
+ startListening() {
+ const { model, options: { paper } } = this;
+ const graph = paper.model;
+
+ this.listenTo(model, 'sort', () => {
+ if (graph.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return;
+ this.sort();
+ });
+
+ this.listenTo(graph, 'batch:stop', (data) => {
+ const name = data && data.batchName;
+ const sortDelayingBatches = this.SORT_DELAYING_BATCHES;
+
+ if (sortDelayingBatches.includes(name) && !graph.hasActiveBatch(sortDelayingBatches)) {
+ this.sort();
+ }
+ });
+
+ this.listenTo(model, 'cell:add', (cell, _collection, opt) => {
+ if (!opt.initial) {
+ const view = paper.findViewByModel(cell);
+ if (view) {
+ paper.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
+ }
+ }
+ });
+
+ this.listenTo(model, 'cell:change:z', (cell, _value, opt) => {
+ if (paper.options.sorting === sortingTypes.APPROX) {
+ const view = paper.findViewByModel(cell);
+ if (view) {
+ paper.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt);
+ }
+ }
+ });
+ },
+
+ sort() {
+ const { options: { paper } } = this;
+ if (!paper)
+ return;
+
+ if (!paper.isExactSorting()) {
+ // noop
+ return;
+ }
+ if (paper.isFrozen()) {
+ // sort views once unfrozen
+ paper._updates.sort = true;
+ return;
+ }
+ this.sortExact();
+ },
+
+ sortExact() {
+ // Run insertion sort algorithm in order to efficiently sort DOM elements according to their
+ // associated model `z` attribute.
+ const cellNodes = Array.from(this.el.children).filter(node => node.getAttribute('model-id'));
+ const cellCollection = this.model.cells;
+
+ sortElements(cellNodes, function(a, b) {
+ const cellA = cellCollection.get(a.getAttribute('model-id'));
+ const cellB = cellCollection.get(b.getAttribute('model-id'));
+ const zA = cellA.attributes.z || 0;
+ const zB = cellB.attributes.z || 0;
+ return (zA === zB) ? 0 : (zA < zB) ? -1 : 1;
+ });
+ },
+
+ insertCellView(cellView) {
+ const { el, model } = cellView;
+ const { options: { paper } } = this;
+
+ switch (paper.options.sorting) {
+ case sortingTypes.APPROX:
+ this.insertSortedNode(el, model.get('z'));
+ break;
+ case sortingTypes.EXACT:
+ default:
+ this.insertNode(el);
+ break;
+ }
+ }
+});
diff --git a/packages/joint-core/src/dia/layers/GridLayer.mjs b/packages/joint-core/src/dia/layers/GridLayerView.mjs
similarity index 90%
rename from packages/joint-core/src/dia/layers/GridLayer.mjs
rename to packages/joint-core/src/dia/layers/GridLayerView.mjs
index 38f7a2c17b..e147107804 100644
--- a/packages/joint-core/src/dia/layers/GridLayer.mjs
+++ b/packages/joint-core/src/dia/layers/GridLayerView.mjs
@@ -1,4 +1,4 @@
-import { PaperLayer } from '../PaperLayer.mjs';
+import { LayerView } from './LayerView.mjs';
import {
isFunction,
isString,
@@ -9,7 +9,7 @@ import {
} from '../../util/index.mjs';
import V from '../../V/index.mjs';
-export const GridLayer = PaperLayer.extend({
+export const GridLayerView = LayerView.extend({
style: {
'pointer-events': 'none'
@@ -19,7 +19,7 @@ export const GridLayer = PaperLayer.extend({
_gridSettings: null,
init() {
- PaperLayer.prototype.init.apply(this, arguments);
+ LayerView.prototype.init.apply(this, arguments);
const { options: { paper }} = this;
this._gridCache = null;
this._gridSettings = [];
@@ -143,30 +143,30 @@ export const GridLayer = PaperLayer.extend({
_resolveDrawGridOption(opt) {
- var namespace = this.options.patterns;
+ const namespace = this.options.patterns;
if (isString(opt) && Array.isArray(namespace[opt])) {
return namespace[opt].map(function(item) {
return assign({}, item);
});
}
- var options = opt || { args: [{}] };
- var isArray = Array.isArray(options);
- var name = options.name;
+ const options = opt || { args: [{}] };
+ const isArray = Array.isArray(options);
+ let name = options.name;
if (!isArray && !name && !options.markup) {
name = 'dot';
}
if (name && Array.isArray(namespace[name])) {
- var pattern = namespace[name].map(function(item) {
+ const pattern = namespace[name].map(function(item) {
return assign({}, item);
});
- var args = Array.isArray(options.args) ? options.args : [options.args || {}];
+ const args = Array.isArray(options.args) ? options.args : [options.args || {}];
defaults(args[0], omit(opt, 'args'));
- for (var i = 0; i < args.length; i++) {
+ for (let i = 0; i < args.length; i++) {
if (pattern[i]) {
assign(pattern[i], args[i]);
}
diff --git a/packages/joint-core/src/dia/PaperLayer.mjs b/packages/joint-core/src/dia/layers/LayerView.mjs
similarity index 79%
rename from packages/joint-core/src/dia/PaperLayer.mjs
rename to packages/joint-core/src/dia/layers/LayerView.mjs
index 4b600f57ff..3ce03474ec 100644
--- a/packages/joint-core/src/dia/PaperLayer.mjs
+++ b/packages/joint-core/src/dia/layers/LayerView.mjs
@@ -1,34 +1,27 @@
-import { View } from '../mvc/index.mjs';
-import { addClassNamePrefix } from '../util/util.mjs';
+import { View } from '../../mvc/index.mjs';
+import { addClassNamePrefix } from '../../util/util.mjs';
-export const LayersNames = {
- GRID: 'grid',
- CELLS: 'cells',
- BACK: 'back',
- FRONT: 'front',
- TOOLS: 'tools',
- LABELS: 'labels'
-};
-
-export const PaperLayer = View.extend({
+export const LayerView = View.extend({
tagName: 'g',
svgElement: true,
pivotNodes: null,
defaultTheme: null,
- options: {
- name: ''
- },
+ UPDATE_PRIORITY: 10,
- className: function() {
- const { name } = this.options;
- if (!name) return null;
- return addClassNamePrefix(`${name}-layer`);
+ options: {
+ id: ''
},
init: function() {
this.pivotNodes = {};
+ this.id = this.options.id || this.cid;
+ },
+
+ className: function() {
+ const { id } = this.options;
+ return addClassNamePrefix(`${id}-layer`);
},
insertSortedNode: function(node, z) {
@@ -78,5 +71,4 @@ export const PaperLayer = View.extend({
// Check if the layer has any child elements (pivot comments are not counted).
return this.el.children.length === 0;
},
-
});
diff --git a/packages/joint-core/test/jointjs/basic.js b/packages/joint-core/test/jointjs/basic.js
index 0ce785bb35..0a882e9d92 100644
--- a/packages/joint-core/test/jointjs/basic.js
+++ b/packages/joint-core/test/jointjs/basic.js
@@ -808,7 +808,7 @@ QUnit.module('basic', function(hooks) {
this.graph.addCell(r1);
this.graph.addCell(r2);
- var spy = sinon.spy(this.paper, 'sortViews');
+ var spy = sinon.spy(this.paper.getLayerView('cells'), 'sort');
var r1View = this.paper.findViewByModel(r1);
var r2View = this.paper.findViewByModel(r2);
diff --git a/packages/joint-core/test/jointjs/dia/HighlighterView.js b/packages/joint-core/test/jointjs/dia/HighlighterView.js
index fb7c58e1cb..bba41da848 100644
--- a/packages/joint-core/test/jointjs/dia/HighlighterView.js
+++ b/packages/joint-core/test/jointjs/dia/HighlighterView.js
@@ -255,7 +255,7 @@ QUnit.module('HighlighterView', function(hooks) {
// Layer = Back/Front
['back', 'front'].forEach(function(layer) {
- var vLayer = V(paper.getLayerNode(layer));
+ var vLayer = V(paper.getLayerViewNode(layer));
var layerChildrenCount = vLayer.children().length;
highlighter = joint.dia.HighlighterView.add(elementView, 'body', id, {
layer: layer
@@ -284,7 +284,7 @@ QUnit.module('HighlighterView', function(hooks) {
var h1 = joint.dia.HighlighterView.add(elementView, 'body', 'highlighter-id-1', { layer: layer, z: 2 });
var h2 = joint.dia.HighlighterView.add(elementView, 'body', 'highlighter-id-2', { layer: layer, z: 3 });
var h3 = joint.dia.HighlighterView.add(elementView, 'body', 'highlighter-id-3', { layer: layer, z: 1 });
- var frontLayerNode = paper.getLayerNode(layer);
+ var frontLayerNode = paper.getLayerViewNode(layer);
assert.equal(frontLayerNode.children.length, 3);
assert.equal(frontLayerNode.children[0], h3.el.parentNode);
assert.equal(frontLayerNode.children[1], h1.el.parentNode);
diff --git a/packages/joint-core/test/jointjs/dia/PaperLayer.js b/packages/joint-core/test/jointjs/dia/LayerView.js
similarity index 69%
rename from packages/joint-core/test/jointjs/dia/PaperLayer.js
rename to packages/joint-core/test/jointjs/dia/LayerView.js
index f39ac573e2..95e61d667e 100644
--- a/packages/joint-core/test/jointjs/dia/PaperLayer.js
+++ b/packages/joint-core/test/jointjs/dia/LayerView.js
@@ -1,12 +1,12 @@
-QUnit.module('joint.dia.PaperLayer', function(hooks) {
+QUnit.module('joint.dia.LayerView', function(hooks) {
- QUnit.test('options: name', function(assert) {
- const layer = new joint.dia.PaperLayer({ name: 'test' });
+ QUnit.test('options: id', function(assert) {
+ const layer = new joint.dia.LayerView({ id: 'test' });
assert.ok(layer.el.classList.contains('joint-test-layer'));
});
QUnit.test('isEmpty() returns true when there are no nodes in the layer', function(assert) {
- const layer = new joint.dia.PaperLayer();
+ const layer = new joint.dia.LayerView();
assert.ok(layer.isEmpty());
const node = document.createElement('div');
layer.insertNode(node);
diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js
index af7f5f1bb5..94b7e2c383 100644
--- a/packages/joint-core/test/jointjs/dia/Paper.js
+++ b/packages/joint-core/test/jointjs/dia/Paper.js
@@ -951,7 +951,7 @@ QUnit.module('joint.dia.Paper', function(hooks) {
var rect1 = new joint.shapes.standard.Rectangle({ z: 0 });
var rect2 = new joint.shapes.standard.Rectangle({ z: 2 });
var rect3 = new joint.shapes.standard.Rectangle({ z: 1 });
- var sortViewsExactSpy = sinon.spy(paper, 'sortViewsExact');
+ var sortLayersExactSpy = sinon.spy(paper, 'sortLayersExact');
// RESET CELLS
graph.resetCells([rect1, rect2, rect3]);
var rect1View = rect1.findView(paper);
@@ -960,10 +960,10 @@ QUnit.module('joint.dia.Paper', function(hooks) {
assert.equal(rect1View.el.previousElementSibling, null);
assert.equal(rect2View.el.previousElementSibling, rect3View.el);
assert.equal(rect3View.el.previousElementSibling, rect1View.el);
- assert.equal(sortViewsExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0);
+ assert.equal(sortLayersExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0);
// CHANGE Z
rect3.toFront();
- assert.equal(sortViewsExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 2 : 0);
+ assert.equal(sortLayersExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0);
if (paper.options.sorting === Paper.sorting.NONE) {
assert.equal(rect1View.el.previousElementSibling, null);
assert.equal(rect2View.el.previousElementSibling, rect3View.el);
@@ -973,13 +973,14 @@ QUnit.module('joint.dia.Paper', function(hooks) {
assert.equal(rect2View.el.previousElementSibling, rect1View.el);
assert.equal(rect3View.el.previousElementSibling, rect2View.el);
}
- sortViewsExactSpy.resetHistory();
+ sortLayersExactSpy.resetHistory();
rect3.translate(10, 10);
- assert.ok(sortViewsExactSpy.notCalled);
+ assert.ok(sortLayersExactSpy.notCalled);
// ADD CELLS
+ var sortLayerExactSpy = sinon.spy(paper.getLayerView('cells'), 'sortExact');
graph.clear();
graph.addCells([rect1, rect2, rect3]);
- assert.equal(sortViewsExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0);
+ assert.equal(sortLayerExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0);
if (paper.options.sorting !== Paper.sorting.NONE) {
rect1View = rect1.findView(paper);
rect2View = rect2.findView(paper);
@@ -995,25 +996,25 @@ QUnit.module('joint.dia.Paper', function(hooks) {
var rect1 = new joint.shapes.standard.Rectangle({ z: 0 });
var rect2 = new joint.shapes.standard.Rectangle({ z: 2 });
var rect3 = new joint.shapes.standard.Rectangle({ z: 1 });
- var sortViewsExactSpy = sinon.spy(paper, 'sortViewsExact');
+ var sortLayersExactSpy = sinon.spy(paper, 'sortLayersExact');
graph.resetCells([rect1, rect2, rect3]);
var rect1View = rect1.findView(paper);
var rect2View = rect2.findView(paper);
var rect3View = rect3.findView(paper);
rect3.toFront();
- assert.ok(sortViewsExactSpy.notCalled);
+ assert.ok(sortLayersExactSpy.notCalled);
paper.unfreeze();
- assert.equal(sortViewsExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0);
+ assert.equal(sortLayersExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0);
if (paper.options.sorting !== Paper.sorting.NONE) {
assert.equal(rect1View.el.previousElementSibling, null);
assert.equal(rect2View.el.previousElementSibling, rect1View.el);
assert.equal(rect3View.el.previousElementSibling, rect2View.el);
}
- sortViewsExactSpy.resetHistory();
+ sortLayersExactSpy.resetHistory();
paper.freeze();
rect3.translate(10, 10);
paper.unfreeze();
- assert.ok(sortViewsExactSpy.notCalled);
+ assert.ok(sortLayersExactSpy.notCalled);
});
});
@@ -1595,8 +1596,8 @@ QUnit.module('joint.dia.Paper', function(hooks) {
hooks.beforeEach(function() {
paper.options.labelsLayer = true;
paper.options.sorting = joint.dia.Paper.sorting.APPROX;
- labelsLayer = paper.getLayerNode(joint.dia.Paper.Layers.LABELS);
- cellsLayer = paper.getLayerNode(joint.dia.Paper.Layers.CELLS);
+ labelsLayer = paper.getLayerViewNode(joint.dia.Paper.Layers.LABELS);
+ cellsLayer = paper.getLayerViewNode(paper.model.getDefaultCellLayer().id);
});
QUnit.test('sanity', function(assert) {
@@ -2096,7 +2097,7 @@ QUnit.module('joint.dia.Paper', function(hooks) {
});
});
- QUnit.module('layers', function(hooks) {
+ QUnit.module('layer views', function(hooks) {
hooks.beforeEach(function() {
paper = new Paper({
@@ -2109,79 +2110,107 @@ QUnit.module('joint.dia.Paper', function(hooks) {
});
QUnit.test('sanity', function(assert) {
- assert.ok(paper.getLayerNode(joint.dia.Paper.Layers.BACK));
- assert.ok(paper.getLayerNode(joint.dia.Paper.Layers.GRID));
- assert.ok(paper.getLayerNode(joint.dia.Paper.Layers.CELLS));
- assert.ok(paper.getLayerNode(joint.dia.Paper.Layers.FRONT));
- assert.ok(paper.getLayerNode(joint.dia.Paper.Layers.TOOLS));
- assert.ok(paper.getLayerNode(joint.dia.Paper.Layers.LABELS));
+ assert.ok(paper.getLayerViewNode(joint.dia.Paper.Layers.BACK));
+ assert.ok(paper.getLayerViewNode(joint.dia.Paper.Layers.GRID));
+ assert.ok(paper.getLayerViewNode(paper.model.getDefaultCellLayer().id));
+ assert.ok(paper.getLayerViewNode(joint.dia.Paper.Layers.FRONT));
+ assert.ok(paper.getLayerViewNode(joint.dia.Paper.Layers.TOOLS));
+ assert.ok(paper.getLayerViewNode(joint.dia.Paper.Layers.LABELS));
});
QUnit.module('hasLayer()', function(assert) {
QUnit.test('returns true when layer exists', function(assert) {
- assert.ok(paper.hasLayer(joint.dia.Paper.Layers.BACK));
- assert.ok(paper.hasLayer(joint.dia.Paper.Layers.GRID));
- assert.ok(paper.hasLayer(joint.dia.Paper.Layers.CELLS));
- assert.ok(paper.hasLayer(joint.dia.Paper.Layers.FRONT));
- assert.ok(paper.hasLayer(joint.dia.Paper.Layers.TOOLS));
- assert.ok(paper.hasLayer(joint.dia.Paper.Layers.LABELS));
+ assert.ok(paper.hasLayerView(joint.dia.Paper.Layers.BACK));
+ assert.ok(paper.hasLayerView(joint.dia.Paper.Layers.GRID));
+ assert.ok(paper.hasLayerView(paper.model.getDefaultCellLayer().id));
+ assert.ok(paper.hasLayerView(joint.dia.Paper.Layers.FRONT));
+ assert.ok(paper.hasLayerView(joint.dia.Paper.Layers.TOOLS));
+ assert.ok(paper.hasLayerView(joint.dia.Paper.Layers.LABELS));
});
QUnit.test('returns false when layer does not exist', function(assert) {
- assert.notOk(paper.hasLayer('test'));
+ assert.notOk(paper.hasLayerView('test'));
});
});
- QUnit.module('addLayer()', function() {
+ QUnit.module('addLayerView()', function() {
QUnit.test('throws error when invalid parameters are provided', function(assert) {
assert.throws(
function() {
- paper.addLayer();
+ paper.addLayerView();
},
- /dia.Paper: The layer name must be provided./,
- 'Layer name must be provided.'
+ /dia.Paper: The layer view must be an instance of dia.LayerView./,
+ 'Layer view must be provided.'
);
assert.throws(
function() {
- paper.addLayer('test');
+ paper.addLayerView({ id: 'test' });
},
- /dia.Paper: The layer view is not an instance of dia.PaperLayer./,
- 'Layer view must be an instance of joint.dia.PaperLayer.'
+ /dia.Paper: The layer view must be an instance of dia.LayerView/,
+ 'Layer view must be provided.'
);
});
- QUnit.test('throws error when layer with the same name already exists', function(assert) {
+ QUnit.test('throws error when layer with the same id already exists', function(assert) {
assert.throws(
function() {
- paper.addLayer(joint.dia.Paper.Layers.BACK);
+ paper.addLayerView(new joint.dia.LayerView({ id: joint.dia.Paper.Layers.BACK }));
},
- /dia.Paper: The layer "back" already exists./,
+ /dia.Paper: The layer view "back" already exists./,
'Layer with the name "back" already exists.'
);
});
- QUnit.test('adds a new layer at the end of the layers list', function(assert) {
- const testLayer = new joint.dia.PaperLayer();
- assert.equal(paper.getLayerNames().indexOf('test1'), -1);
- assert.notOk(paper.hasLayer('test1'));
- paper.addLayer('test1', testLayer);
- assert.ok(paper.hasLayer('test1'));
- assert.equal(paper.getLayers().at(-1), testLayer);
+ QUnit.test('adds a new layer view to the Paper', function(assert) {
+ const testLayer = new joint.dia.LayerView({
+ id: 'test1'
+ });
+ assert.notOk(paper.hasLayerView('test1'));
+ paper.addLayerView(testLayer);
+ assert.ok(paper.hasLayerView('test1'));
});
- QUnit.test('adds a new layer before the specified layer', function(assert) {
- const testLayer = new joint.dia.PaperLayer();
- assert.equal(paper.getLayerNames().indexOf('test2'), -1);
- assert.notOk(paper.hasLayer('test1'));
- paper.addLayer('test2', testLayer, { insertBefore: 'cells' });
- assert.ok(paper.hasLayer('test2'));
- const layerNames = paper.getLayerNames();
- assert.equal(layerNames.indexOf('test2'), layerNames.indexOf('cells') - 1);
+ });
+
+ QUnit.module('renderLayerView()', function() {
+
+ QUnit.test('throws error when invalid parameters are provided', function(assert) {
+ assert.throws(
+ function() {
+ paper.renderLayerView();
+ },
+ /dia.Paper: Layer view options are required./,
+ 'Layer view options must be provided.'
+ );
+ assert.throws(
+ function() {
+ paper.renderLayerView({});
+ },
+ /dia.Paper: Layer view id is required./,
+ 'Layer view options must contain id.'
+ );
});
+ QUnit.test('throws error when layer with the same name already exists', function(assert) {
+ assert.throws(
+ function() {
+ paper.renderLayerView(new joint.dia.LayerView({ id: joint.dia.Paper.Layers.BACK }));
+ },
+ /dia.Paper: The layer view "back" already exists./,
+ 'Layer with the name "back" already exists.'
+ );
+ });
+
+ QUnit.test('render a new layer view in the paper', function(assert) {
+ assert.notOk(paper.hasLayerView('test1'));
+ paper.renderLayerView({
+ id: 'test1'
+ });
+ assert.ok(paper.hasLayerView('test1'));
+ });
});
QUnit.module('removeLayer()', function() {
@@ -2189,81 +2218,71 @@ QUnit.module('joint.dia.Paper', function(hooks) {
QUnit.test('throws error when invalid parameters are provided', function(assert) {
assert.throws(
function() {
- paper.removeLayer();
+ paper.removeLayerView();
},
- /dia.Paper: Unknown layer "undefined"./,
- 'Layer name must be provided.'
+ /dia.Paper: The layer view must be provided./,
+ 'Layer view must be provided.'
);
});
QUnit.test('throws error when layer does not exist', function(assert) {
- assert.notOk(paper.hasLayer('test'));
+ assert.notOk(paper.hasLayerView('test'));
assert.throws(
function() {
- paper.removeLayer('test');
+ paper.removeLayerView(new joint.dia.LayerView({ id: 'test' }));
},
- /dia.Paper: Unknown layer "test"./,
+ /dia.Paper: Unknown layer view "test"./,
'Layer with the name "test" does not exist.'
);
});
QUnit.test('removes the specified layer', function(assert) {
- assert.ok(paper.hasLayer(joint.dia.Paper.Layers.BACK));
- paper.removeLayer(joint.dia.Paper.Layers.BACK);
- assert.notOk(paper.hasLayer(joint.dia.Paper.Layers.BACK));
+ assert.ok(paper.hasLayerView(joint.dia.Paper.Layers.BACK));
+ paper.removeLayerView(joint.dia.Paper.Layers.BACK);
+ assert.notOk(paper.hasLayerView(joint.dia.Paper.Layers.BACK));
});
QUnit.test('throws error when trying to remove a layer with content', function(assert) {
graph.addCells([new joint.shapes.standard.Rectangle()], { async: false });
assert.throws(
function() {
- paper.removeLayer(joint.dia.Paper.Layers.CELLS);
+ paper.removeLayerView(graph.getDefaultCellLayer().id);
},
- /dia.Paper: The layer is not empty./,
+ /dia.Paper: The layer view is not empty./,
'Layer "cells" cannot be removed because it contains views.'
);
});
QUnit.test('removes the layer if passed as an object', function(assert) {
- const testLayer = new joint.dia.PaperLayer();
- paper.addLayer('test', testLayer);
- assert.ok(paper.hasLayer('test'));
- paper.removeLayer(testLayer);
- assert.notOk(paper.hasLayer('test'));
- });
-
- QUnit.test('throws error when trying to remove a layer which is not added to the paper', function(assert) {
- const testLayer = new joint.dia.PaperLayer();
- assert.throws(
- function() {
- paper.removeLayer(testLayer);
- },
- /dia.Paper: The layer is not registered./,
- 'Layer with the name "test" does not exist.'
- );
+ const testLayer = new joint.dia.LayerView({
+ id: 'test'
+ });
+ paper.renderLayerView(testLayer);
+ assert.ok(paper.hasLayerView('test'));
+ paper.removeLayerView(testLayer);
+ assert.notOk(paper.hasLayerView('test'));
});
-
});
- QUnit.module('moveLayer()', function() {
+ QUnit.module('insertLayerView()', function() {
QUnit.test('throws error when invalid parameters are provided', function(assert) {
assert.throws(
function() {
- paper.moveLayer();
+ paper.insertLayerView();
},
- /dia.Paper: Unknown layer "undefined"./,
+ /dia.Paper: The layer view must be an instance of dia.LayerView./,
'Layer name must be provided.'
);
});
QUnit.test('throws error when layer does not exist', function(assert) {
- assert.notOk(paper.hasLayer('test'));
+ assert.notOk(paper.hasLayerView('test'));
assert.throws(
function() {
- paper.moveLayer('test');
+ paper.insertLayerView(new joint.dia.LayerView({ id: 'test' }));
},
- /dia.Paper: Unknown layer "test"./,
+ /dia.Paper: Unknown layer view "test"./,
'Layer with the name "test" does not exist.'
);
});
@@ -2271,45 +2290,72 @@ QUnit.module('joint.dia.Paper', function(hooks) {
QUnit.test('throws error when invalid position is provided', function(assert) {
assert.throws(
function() {
- paper.moveLayer(joint.dia.Paper.Layers.BACK, 'test');
+ paper.insertLayerView(paper.getLayerView(joint.dia.Paper.Layers.BACK), 'test');
},
- /dia.Paper: Unknown layer "test"./,
+ /dia.Paper: Unknown layer view "test"./,
'Invalid position "test".'
);
});
+ QUnit.test('inserts a new layer at the end of the order', function(assert) {
+ const testLayer = new joint.dia.LayerView({
+ id: 'test'
+ });
+ assert.equal(paper.getLayerViewOrder().indexOf('test'), -1);
+ assert.notOk(paper.hasLayerView('test'));
+ paper.renderLayerView(testLayer);
+ assert.ok(paper.hasLayerView('test'));``
+ paper.insertLayerView(testLayer);
+ const order = paper.getLayerViewOrder();
+ assert.equal(order.indexOf('test'), order.length - 1);
+ });
+
+
+ QUnit.test('inserts a new layer before the specified layer', function(assert) {
+ const testLayer = new joint.dia.LayerView({
+ id: 'test'
+ });
+ assert.equal(paper.getLayerViewOrder().indexOf('test'), -1);
+ assert.notOk(paper.hasLayerView('test'));
+ paper.renderLayerView(testLayer);
+ assert.ok(paper.hasLayerView('test'));
+ paper.insertLayerView(testLayer, 'cells');
+ const order = paper.getLayerViewOrder();
+ assert.equal(order.indexOf('test'), order.indexOf('cells') - 1);
+ });
+
QUnit.test('moves the specified layer to the specified position', function(assert) {
- const layerNames = paper.getLayerNames();
- const [firstLayer, secondLayer] = layerNames;
- paper.moveLayer(secondLayer, firstLayer);
- const [newFirstLayer, newSecondLayer] = paper.getLayerNames();
+ const order = paper.getLayerViewOrder();
+ const [firstLayer, secondLayer] = order;
+ paper.insertLayerView(paper.getLayerView(secondLayer), firstLayer);
+ const [newFirstLayer, newSecondLayer] = paper.getLayerViewOrder();
assert.equal(newFirstLayer, secondLayer);
assert.equal(newSecondLayer, firstLayer);
});
QUnit.test('moves the specified layer to the end of the layers list', function(assert) {
- const layerNames = paper.getLayerNames();
- const [firstLayer, secondLayer] = layerNames;
- paper.moveLayer(firstLayer);
- const newLayerNames = paper.getLayerNames();
+ const order = paper.getLayerViewOrder();
+ const [firstLayer, secondLayer] = order;
+ paper.insertLayerView(paper.getLayerView(firstLayer));
+ const newLayerNames = paper.getLayerViewOrder();
assert.equal(newLayerNames.at(0), secondLayer);
assert.equal(newLayerNames.at(-1), firstLayer);
});
QUnit.test('it\'s possible to move the layer to the same position', function(assert) {
- const layerNames = paper.getLayerNames();
- const [firstLayer, secondLayer] = layerNames;
- paper.moveLayer(firstLayer, secondLayer);
- const newLayerNames = paper.getLayerNames();
+ const order = paper.getLayerViewOrder();
+ const [firstLayer, secondLayer] = order;
+ paper.insertLayerView(paper.getLayerView(firstLayer), secondLayer);
+ const newLayerNames = paper.getLayerViewOrder();
assert.equal(newLayerNames.at(0), firstLayer);
assert.equal(newLayerNames.at(1), secondLayer);
});
QUnit.test('it\'s ok to move layer before itself', function(assert) {
- const layerNames = paper.getLayerNames();
- const [, secondLayer] = layerNames;
- paper.moveLayer(secondLayer, secondLayer);
- const newLayerNames = paper.getLayerNames();
+ const order = paper.getLayerViewOrder();
+ const [, secondLayer] = order;
+ paper.insertLayerView(paper.getLayerView(secondLayer), secondLayer);
+ const newLayerNames = paper.getLayerViewOrder();
assert.equal(newLayerNames.at(1), secondLayer);
});
@@ -2321,40 +2367,44 @@ QUnit.module('joint.dia.Paper', function(hooks) {
const r1 = new joint.shapes.standard.Rectangle();
graph.addCell(r1, { async: false });
- assert.notOk(r1.get('layer'));
- assert.ok(paper.getLayerNode('cells').contains(r1.findView(paper).el));
-
- const test1Layer = new joint.dia.PaperLayer();
- paper.addLayer('test1', test1Layer);
-
+ assert.ok(paper.getLayerViewNode('cells').contains(r1.findView(paper).el), 'cell view is in the "cells" layer');
- const r2 = new joint.shapes.standard.Rectangle({ layer: 'test1' });
- graph.addCell(r2, { async: false });
- assert.ok(paper.getLayerNode('test1').contains(r2.findView(paper).el));
-
- const r3 = new joint.shapes.standard.Rectangle({ layer: 'test2' });
+ const r2 = new joint.shapes.standard.Rectangle({ layer: 'test' });
assert.throws(
() => {
- graph.addCell(r3, { async: false });
+ graph.addCell(r2, { async: false });
},
- /dia.Paper: Unknown layer "test2"./,
- 'Layer "test2" does not exist.'
+ /dia.Graph: Cell layer with id 'test' does not exist./,
+ 'Cell layer "test" does not exist in Graph.'
);
+
+ graph.removeCells([r2], { async: false });
+
+ const testLayer = new joint.dia.CellLayer({ id: 'test' });
+ graph.addCellLayer(testLayer);
+ graph.insertCellLayer(testLayer);
+
+ assert.ok(paper.hasLayerView('test'), 'Layer view "test" is created in Paper.');
+
+ graph.addCell(r2, { async: false });
+ assert.ok(paper.getLayerViewNode('test').contains(r2.findView(paper).el), 'cell view is added to the "test" layer');
});
QUnit.test('cell view is moved to correct layer', function(assert) {
const r1 = new joint.shapes.standard.Rectangle();
graph.addCell(r1, { async: false });
- assert.ok(paper.getLayerNode('cells').contains(r1.findView(paper).el));
+ assert.ok(paper.getLayerViewNode('cells').contains(r1.findView(paper).el), 'cell view is in the "cells" layer');
- const test1Layer = new joint.dia.PaperLayer();
- paper.addLayer('test1', test1Layer);
+ const testLayer = new joint.dia.CellLayer({ id: 'test' });
+ graph.addCellLayer(testLayer);
+ graph.insertCellLayer(testLayer);
- r1.set('layer', 'test1', { async: false });
- assert.ok(paper.getLayerNode('test1').contains(r1.findView(paper).el));
- });
+ assert.ok(paper.hasLayerView('test'), 'Layer view "test" is created in Paper.');
+ r1.set('layer', 'test', { async: false });
+ assert.ok(paper.getLayerViewNode('test').contains(r1.findView(paper).el), 'cell view is moved to the "test" layer');
+ });
});
});
});
diff --git a/packages/joint-core/test/jointjs/dia/linkTools.js b/packages/joint-core/test/jointjs/dia/linkTools.js
index 8b647a5160..478d3cf03c 100644
--- a/packages/joint-core/test/jointjs/dia/linkTools.js
+++ b/packages/joint-core/test/jointjs/dia/linkTools.js
@@ -457,7 +457,7 @@ QUnit.module('linkTools', function(hooks) {
var link3 = new joint.shapes.standard.Link();
graph.addCells(link2, link3);
- var toolsLayerNode = paper.getLayerNode(layer);
+ var toolsLayerNode = paper.getLayerViewNode(layer);
var t1 = new joint.dia.ToolsView({ z: 2, tools: [], layer: layer });
var t2 = new joint.dia.ToolsView({ z: 3, tools: [], layer: layer });
var t3 = new joint.dia.ToolsView({ z: 1, tools: [], layer: layer });
diff --git a/packages/joint-core/test/jointjs/index.html b/packages/joint-core/test/jointjs/index.html
index 6cbc38a894..3668ec27c3 100644
--- a/packages/joint-core/test/jointjs/index.html
+++ b/packages/joint-core/test/jointjs/index.html
@@ -13,6 +13,7 @@
+
@@ -23,7 +24,7 @@
-
+
@@ -43,6 +44,7 @@
-
+
+