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 @@ - + + diff --git a/packages/joint-core/test/jointjs/layers/basic.js b/packages/joint-core/test/jointjs/layers/basic.js new file mode 100644 index 0000000000..ee4ed47a0b --- /dev/null +++ b/packages/joint-core/test/jointjs/layers/basic.js @@ -0,0 +1,112 @@ +QUnit.module('layers-basic', function(hooks) { + + hooks.beforeEach(() => { + + const fixtureEl = fixtures.getElement(); + const paperEl = document.createElement('div'); + fixtureEl.appendChild(paperEl); + this.graph = new joint.dia.Graph({}, { cellNamespace: joint.shapes }); + this.paper = new joint.dia.Paper({ + el: paperEl, + model: this.graph, + cellViewNamespace: joint.shapes, + }); + }); + + hooks.afterEach(() => { + + this.paper.remove(); + this.graph = null; + this.paper = null; + }); + + QUnit.test('Default layers setup', (assert) => { + assert.ok(this.graph.cellLayersController, 'Cell layers controller is created'); + + const cellLayers = this.graph.get('cellLayers'); + + assert.ok(Array.isArray(cellLayers), 'Graph has cellLayers attribute'); + + assert.strictEqual(cellLayers.length, 1, 'Graph has one default cell layer'); + + assert.strictEqual(cellLayers[0].id, 'cells', 'Graph has default cell layer with id "cells"'); + + assert.ok(this.paper.getLayerView('cells'), 'Paper has default layer view for "cells" layer'); + + const cellsLayerView = this.paper.getLayerView('cells'); + const graphDefaultCellLayer = this.graph.getDefaultCellLayer(); + + assert.equal(cellsLayerView.model, graphDefaultCellLayer, 'Default layer view is linked to the default layer model'); + }); + + QUnit.test('default fromJSON() cells', (assert) => { + this.graph.fromJSON({ + cells: [ + { + type: 'standard.Rectangle', + id: 'rect1', + position: { x: 100, y: 100 }, + size: { width: 200, height: 100 }, + }, + { + type: 'standard.Ellipse', + id: 'ellipse1', + position: { x: 150, y: 150 }, + size: { width: 20, height: 20 }, + } + ] + }); + + const defaultCellLayer = this.graph.getDefaultCellLayer(); + + assert.ok(defaultCellLayer.cells.has('rect1'), 'Default cell layer has rectangle cell'); + assert.ok(defaultCellLayer.cells.has('ellipse1'), 'Default cell layer has ellipse cell'); + + const layerViewNode = this.paper.getLayerViewNode(defaultCellLayer.id); + + assert.ok(layerViewNode.querySelector(`[model-id="rect1"]`), 'Layer view has rectangle cell view node'); + assert.ok(layerViewNode.querySelector(`[model-id="ellipse1"]`), 'Layer view has ellipse cell view node'); + }); + + QUnit.test('default fromJSON() cellLayers', (assert) => { + this.graph.fromJSON({ + cellLayers: [ + { id: 'layer1' }, + { id: 'layer2' } + ], + cells: [ + { + type: 'standard.Rectangle', + id: 'rect1', + position: { x: 100, y: 100 }, + size: { width: 200, height: 100 }, + layer: 'layer1' + }, + { + type: 'standard.Ellipse', + id: 'ellipse1', + position: { x: 150, y: 150 }, + size: { width: 20, height: 20 }, + layer: 'layer2' + } + ] + }); + + assert.ok(this.graph.hasCellLayer('layer1'), 'Graph has layer "layer1"'); + assert.ok(this.graph.hasCellLayer('layer2'), 'Graph has layer "layer2"'); + + const layer1 = this.graph.getCellLayer('layer1'); + const layer2 = this.graph.getCellLayer('layer2'); + + assert.ok(layer1.cells.has('rect1'), 'Layer "layer1" has rectangle cell'); + assert.ok(layer2.cells.has('ellipse1'), 'Layer "layer2" has ellipse cell'); + + const layerViewNode = this.paper.getLayerViewNode('layer1'); + + assert.ok(layerViewNode.querySelector(`[model-id="rect1"]`), 'Layer view for "layer1" has rectangle cell view node'); + + const layerViewNode2 = this.paper.getLayerViewNode('layer2'); + + assert.ok(layerViewNode2.querySelector(`[model-id="ellipse1"]`), 'Layer view for "layer2" has ellipse cell view node'); + }); +}); diff --git a/packages/joint-core/test/jointjs/layers/embedding.js b/packages/joint-core/test/jointjs/layers/embedding.js new file mode 100644 index 0000000000..d9f2f71133 --- /dev/null +++ b/packages/joint-core/test/jointjs/layers/embedding.js @@ -0,0 +1,88 @@ +QUnit.module('embedding-layers', function(hooks) { + + hooks.beforeEach(() => { + + const fixtureEl = fixtures.getElement(); + const paperEl = document.createElement('div'); + fixtureEl.appendChild(paperEl); + this.graph = new joint.dia.Graph({}, { cellNamespace: joint.shapes }); + this.paper = new joint.dia.Paper({ + el: paperEl, + width: 800, + height: 600, + model: this.graph, + cellViewNamespace: joint.shapes, + useLayersForEmbedding: true + }); + }); + + hooks.afterEach(() => { + + this.paper.remove(); + this.graph = null; + this.paper = null; + }); + + QUnit.test('Embedding layers setup', (assert) => { + assert.ok(this.paper.embeddingLayersController, 1, 'Controller is created'); + }); + + QUnit.test('from JSON without cellLayers', (assert) => { + this.graph.fromJSON({ + cells: [ + { + type: 'standard.Rectangle', + id: 'rect1', + position: { x: 100, y: 100 }, + size: { width: 200, height: 100 }, + embeds: ['ellipse1'] + }, + { + type: 'standard.Ellipse', + id: 'ellipse1', + position: { x: 150, y: 150 }, + size: { width: 20, height: 20 }, + parent: 'rect1' + } + ] + }); + + assert.ok(this.paper.hasLayerView('rect1'), 'Paper has layer for parent cell'); + assert.ok(this.graph.hasCellLayer('rect1'), 'Graph has layer for parent cell'); + + const layer = this.graph.getCellLayer('rect1'); + + assert.ok(layer.cells.has('ellipse1'), 'Cell Layer has cell'); + + const layerView = this.paper.getLayerView('rect1'); + + assert.ok(layerView.el.querySelector(`[model-id="ellipse1"]`), 'Layer view has cell view node for embedded cell'); + }); + + + QUnit.test('to/from JSON', (assert) => { + this.graph.fromJSON({ + cells: [ + { + type: 'standard.Rectangle', + id: 'rect1', + position: { x: 100, y: 100 }, + size: { width: 200, height: 100 }, + embeds: ['ellipse1'] + }, + { + type: 'standard.Ellipse', + id: 'ellipse1', + position: { x: 150, y: 150 }, + size: { width: 20, height: 20 }, + parent: 'rect1' + } + ] + }); + + const toJSONstring = JSON.stringify(this.graph.toJSON()); + + + assert.equal(toJSONstring, '{"cellLayers":[{"id":"cells","default":true}],"cells":[{"type":"standard.Rectangle","attrs":{},"position":{"x":100,"y":100},"size":{"width":200,"height":100},"angle":0,"id":"rect1","embeds":["ellipse1"]},{"type":"standard.Ellipse","attrs":{},"position":{"x":150,"y":150},"size":{"width":20,"height":20},"angle":0,"id":"ellipse1","parent":"rect1","layer":"rect1"}]}', 'toJSON returns correct JSON structure'); + }); +}); diff --git a/packages/joint-core/test/jointjs/paper.js b/packages/joint-core/test/jointjs/paper.js index 4f8a5df081..cf6c4b917d 100644 --- a/packages/joint-core/test/jointjs/paper.js +++ b/packages/joint-core/test/jointjs/paper.js @@ -97,9 +97,9 @@ QUnit.module('paper', function(hooks) { }); - QUnit.test('paper.addCell() number of sortViews()', function(assert) { + QUnit.test('paper.addCell() number of sort()', function(assert) { - var spy = sinon.spy(this.paper, 'sortViews'); + var spy = sinon.spy(this.paper.getLayerView('cells'), 'sort'); var r1 = new joint.shapes.standard.Rectangle; var r2 = new joint.shapes.standard.Rectangle; @@ -119,9 +119,9 @@ QUnit.module('paper', function(hooks) { }); - QUnit.test('paper.addCells() number of sortViews()', function(assert) { + QUnit.test('paper.addCells() number of sort()', function(assert) { - var spy = sinon.spy(this.paper, 'sortViews'); + var spy = sinon.spy(this.paper.getLayerView('cells'), 'sort'); var r1 = new joint.shapes.standard.Rectangle; var r2 = new joint.shapes.standard.Rectangle; @@ -1786,7 +1786,7 @@ QUnit.module('paper', function(hooks) { }; const getGridVel = function(paper) { - return V(paper.getLayerNode(joint.dia.Paper.Layers.GRID).firstChild); + return V(paper.getLayerViewNode(joint.dia.Paper.Layers.GRID).firstChild); }; var preparePaper = function(drawGrid, paperSettings) { diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index fc5dfcc49f..25b9800124 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -185,15 +185,25 @@ export namespace dia { cellNamespace: any; } + interface CellLayerAttributes { + id: string; + default?: boolean; + order?: number; + } + interface Attributes { cells?: Cells; + cellLayers?: CellLayerAttributes[]; [key: string]: any; } } class Graph extends mvc.Model { - constructor(attributes?: Graph.Attributes, opt?: { cellNamespace?: any, cellModel?: typeof Cell }); + constructor(attributes?: Graph.Attributes, opt?: { + cellNamespace?: any, + cellModel?: typeof Cell, + }); addCell(cell: Cell.JSON | Cell, opt?: CollectionAddOptions): this; addCell(cell: Array, opt?: CollectionAddOptions): this; @@ -202,6 +212,18 @@ export namespace dia { resetCells(cells: Array, opt?: Graph.Options): this; + addCellLayer(layer: CellLayer): void; + + removeCellLayer(layer: CellLayer): void; + + getDefaultCellLayer(): CellLayer; + + getCellLayer(id: string): CellLayer; + + hasCellLayer(id: string): boolean; + + getCellLayers(): CellLayer[]; + getCell(id: Cell.ID | Cell): Cell; getElements(): Element[]; @@ -305,9 +327,9 @@ export namespace dia { hasActiveBatch(name?: string | string[]): boolean; - maxZIndex(): number; + maxZIndex(layerName?: string): number; - minZIndex(): number; + minZIndex(layerName?: string): number; removeCells(cells: Cell[], opt?: Cell.DisconnectableOptions): this; @@ -518,6 +540,9 @@ export namespace dia { z(): number; + layer(): string; + layer(id: string | null, opt?: Graph.Options): this; + angle(): number; getBBox(): g.Rect; @@ -1319,7 +1344,6 @@ export namespace dia { } enum Layers { - CELLS = 'cells', LABELS = 'labels', BACK = 'back', FRONT = 'front', @@ -1402,6 +1426,7 @@ export namespace dia { elementView?: typeof ElementView | ((element: Element) => typeof ElementView); linkView?: typeof LinkView | ((link: Link) => typeof LinkView); // embedding + useLayersForEmbedding?: boolean; embeddingMode?: boolean; frontParentOnly?: boolean; findParentBy?: FindParentByType | FindParentByCallback; @@ -1750,29 +1775,33 @@ export namespace dia { // layers - getLayerNode(layerName: Paper.Layers | string): SVGGElement; + getLayerViewNode(id: Paper.Layers | string): SVGGElement; - getLayerView(layerName: Paper.Layers | string): PaperLayer; + getLayerView(id: Paper.Layers | string): LayerView; - hasLayerView(layerName: Paper.Layers | string): boolean; + hasLayerView(id: Paper.Layers | string): boolean; - renderLayers(layers: Array<{ name: string }>): void; + protected removeLayerViews(): void; - protected removeLayers(): void; + protected resetLayerViews(): void; - protected resetLayers(): void; + renderLayerView(options: Omit): LayerView; - addLayer(layerName: string, layerView: PaperLayer, options?: { insertBefore?: string }): void; + createLayerView(options: Omit): LayerView; - removeLayer(layer: string | PaperLayer): void; + addLayerView(layerView: LayerView): void; - moveLayer(layer: string | PaperLayer, insertBefore: string | PaperLayer | null): void; + insertLayerView(layerView: LayerView, insertBefore?: string | LayerView): void; - hasLayer(layer: string | PaperLayer): boolean; + removeLayerView(LayerView: LayerView): void; - getLayerNames(): string[]; + requestLayerViewRemove(layerView: string | LayerView): void; - getLayers(): Array; + getLayerViewOrder(): string[]; + + getOrderedLayerViews(): Array; + + protected updateCellLayers(graph: Graph, cellLayers: Graph.CellLayerAttributes[]): void; // rendering @@ -1965,17 +1994,20 @@ export namespace dia { scaleContentToFit(opt?: Paper.ScaleContentOptions): void; } - namespace PaperLayer { + namespace LayerView { - interface Options extends mvc.ViewOptions { - name: string; + interface Options extends mvc.ViewOptions { + id: string; + paper: Paper; + type?: string; } } - class PaperLayer extends mvc.View { - constructor(opt?: PaperLayer.Options); + class LayerView extends mvc.View { - options: PaperLayer.Options; + constructor(opt?: LayerView.Options); + + options: LayerView.Options; pivotNodes: { [z: number]: Comment }; @@ -1988,6 +2020,52 @@ export namespace dia { removePivots(): void; } + namespace CellGroup { + + class CellGroupCollection extends mvc.Collection { + } + + interface Attributes extends mvc.ObjectHash { + type: string; + collectionConstructor: typeof CellGroupCollection; + } + } + + class CellGroup extends mvc.Model { + + cells: C; + + add(cell: Cell, opt: Graph.Options): void; + + remove(cell: Cell): void; + + reset(): void; + } + + namespace CellLayer { + + class CellLayerCollection extends CellGroup.CellGroupCollection { + } + } + + class CellLayer extends CellGroup implements CellGroup { + + minZIndex(): number; + + maxZIndex(): number; + } + + class CellLayerView extends LayerView { + + protected startListening(): void; + + protected sort(): void; + + protected sortExact(): void; + + protected insertCellView(cellView: CellView): void; + } + namespace ToolsView { interface Options extends mvc.ViewOptions {