From 5ddbc5e28085e2bfac47a50e620ecf5753b10f80 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 19 Feb 2025 17:35:46 +0100 Subject: [PATCH 01/54] wip --- packages/joint-core/src/dia/Graph.mjs | 10 +++ packages/joint-core/src/dia/Layer.mjs | 35 ++++++++++ .../src/dia/{PaperLayer.mjs => LayerView.mjs} | 63 ++++++++---------- packages/joint-core/src/dia/Paper.mjs | 64 +++++++++++-------- .../joint-core/src/dia/layers/GridLayer.mjs | 57 +++++++++-------- 5 files changed, 141 insertions(+), 88 deletions(-) create mode 100644 packages/joint-core/src/dia/Layer.mjs rename packages/joint-core/src/dia/{PaperLayer.mjs => LayerView.mjs} (62%) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 41615e5942..f55185f4ee 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -1,6 +1,7 @@ import * as util from '../util/index.mjs'; import * as g from '../g/index.mjs'; +import { LayersNames, Layer } from './Layer.mjs'; import { Model } from '../mvc/Model.mjs'; import { Collection } from '../mvc/Collection.mjs'; import { wrappers, wrapWith } from '../util/wrappers.mjs'; @@ -70,6 +71,15 @@ export const Graph = Model.extend({ opt = opt || {}; + const defaultLayer = new Layer({ + name: LayersNames.CELLS, + displayName: 'Default' + }); + + this.set('layers', [ + defaultLayer + ]); + // 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. diff --git a/packages/joint-core/src/dia/Layer.mjs b/packages/joint-core/src/dia/Layer.mjs new file mode 100644 index 0000000000..c00c097c3d --- /dev/null +++ b/packages/joint-core/src/dia/Layer.mjs @@ -0,0 +1,35 @@ +import { Model } from '../mvc/index.mjs'; + +export const LayersNames = { + GRID: 'grid', + CELLS: 'cells', + BACK: 'back', + FRONT: 'front', + TOOLS: 'tools', + LABELS: 'labels' +}; + +export class Layer extends Model { + + defaults() { + return { + name: '', + displayName: '', + }; + } + + initialize() { + this.models = []; + } + + addModel(item) { + this.models.push(item); + } + + removeModel(item) { + const index = this.models.indexOf(item); + if (index !== -1) { + this.models.splice(index, 1); + } + } +} diff --git a/packages/joint-core/src/dia/PaperLayer.mjs b/packages/joint-core/src/dia/LayerView.mjs similarity index 62% rename from packages/joint-core/src/dia/PaperLayer.mjs rename to packages/joint-core/src/dia/LayerView.mjs index 4b600f57ff..b67d2b23e9 100644 --- a/packages/joint-core/src/dia/PaperLayer.mjs +++ b/packages/joint-core/src/dia/LayerView.mjs @@ -1,48 +1,41 @@ 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 class LayerView extends View { -export const PaperLayer = View.extend({ + pivotNodes = null; + defaultTheme = null; - tagName: 'g', - svgElement: true, - pivotNodes: null, - defaultTheme: null, + preinitialize() { + this.tagName = 'g'; + this.svgElement = true; - options: { - name: '' - }, + this.options = { + name: '' + } + } - className: function() { + className() { const { name } = this.options; if (!name) return null; return addClassNamePrefix(`${name}-layer`); - }, + } - init: function() { + init() { this.pivotNodes = {}; - }, + } - insertSortedNode: function(node, z) { + insertSortedNode(node, z) { this.el.insertBefore(node, this.insertPivot(z)); - }, + } - insertNode: function(node) { + insertNode(node) { const { el } = this; if (node.parentNode !== el) { el.appendChild(node); } - }, + } - insertPivot: function(z) { + insertPivot(z) { const { el, pivotNodes } = this; z = +z; z || (z = 0); @@ -66,17 +59,17 @@ export const PaperLayer = View.extend({ el.insertBefore(pivotNode, el.firstChild); } return pivotNode; - }, + } - removePivots: function() { + removePivotNodes() { const { el, pivotNodes } = this; - for (let z in pivotNodes) el.removeChild(pivotNodes[z]); + for (let z in pivotNodes) { + el.removeChild(pivotNodes[z]); + } this.pivotNodes = {}; - }, + } - isEmpty: function() { - // Check if the layer has any child elements (pivot comments are not counted). + isEmpty() { return this.el.children.length === 0; - }, - -}); + } +} diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 52d4bee93b..add1f01e32 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -38,7 +38,8 @@ 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 { LayersNames, Layer } from './Layer.mjs'; +import { LayerView } from './LayerView.mjs'; import * as highlighters from '../highlighters/index.mjs'; import * as linkAnchors from '../linkAnchors/index.mjs'; import * as connectionPoints from '../connectionPoints/index.mjs'; @@ -82,20 +83,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', @@ -401,6 +388,29 @@ export const Paper = View.extend({ const model = this.model = options.model || new Graph; + const graphLayers = model.get('layers'); + + this._layersSettings = [{ + name: LayersNames.GRID, + }, { + name: LayersNames.BACK, + }]; + + graphLayers.forEach(layer => { + this._layersSettings.push({ + name: layer.get('name'), + model: layer + }); + }); + + this._layersSettings = this._layersSettings.concat([{ + name: LayersNames.LABELS, + }, { + name: LayersNames.FRONT + }, { + name: LayersNames.TOOLS + }]); + // Layers (SVGGroups) this._layers = { viewsMap: {}, @@ -638,7 +648,7 @@ export const Paper = View.extend({ _getLayerView(layer) { const { _layers: { namesMap, viewsMap }} = this; - if (layer instanceof PaperLayer) { + if (layer instanceof LayerView) { if (layer.cid in namesMap) return layer; return null; } @@ -654,7 +664,7 @@ export const Paper = View.extend({ _requireLayerView(layer) { const layerView = this._getLayerView(layer); if (!layerView) { - if (layer instanceof PaperLayer) { + if (layer instanceof LayerView) { throw new Error('dia.Paper: The layer is not registered.'); } else { throw new Error(`dia.Paper: Unknown layer "${layer}".`); @@ -682,8 +692,8 @@ export const Paper = View.extend({ if (this._getLayerView(layerName)) { throw new Error(`dia.Paper: The layer "${layerName}" already exists.`); } - if (!(layerView instanceof PaperLayer)) { - throw new Error('dia.Paper: The layer view is not an instance of dia.PaperLayer.'); + if (!(layerView instanceof LayerView)) { + throw new Error('dia.Paper: The layer view is not an instance of dia.LayerView.'); } const { insertBefore } = options; if (!insertBefore) { @@ -749,24 +759,24 @@ export const Paper = View.extend({ V(this.svg).prepend(V.createSVGStyle(css)); }, - createLayer(name) { - switch (name) { + createLayer(attributes) { + switch (attributes.name) { case LayersNames.GRID: - return new GridLayer({ name, paper: this, patterns: this.constructor.gridPatterns }); + return new GridLayer({ ...attributes, paper: this, patterns: this.constructor.gridPatterns }); default: - return new PaperLayer({ name }); + return new LayerView(attributes); } }, - renderLayer: function(name) { - const layerView = this.createLayer(name); - this.addLayer(name, layerView); + renderLayer: function(attributes) { + const layerView = this.createLayer(attributes); + this.addLayer(attributes.name, layerView); return layerView; }, renderLayers: function(layers = defaultLayers) { this.removeLayers(); - layers.forEach(({ name }) => this.renderLayer(name)); + layers.forEach(attributes => this.renderLayer(attributes)); // Throws an exception if doesn't exist const cellsLayerView = this.getLayerView(LayersNames.CELLS); const toolsLayerView = this.getLayerView(LayersNames.TOOLS); diff --git a/packages/joint-core/src/dia/layers/GridLayer.mjs b/packages/joint-core/src/dia/layers/GridLayer.mjs index 38f7a2c17b..eb4946b9c1 100644 --- a/packages/joint-core/src/dia/layers/GridLayer.mjs +++ b/packages/joint-core/src/dia/layers/GridLayer.mjs @@ -1,4 +1,4 @@ -import { PaperLayer } from '../PaperLayer.mjs'; +import { LayerView } from '../LayerView.mjs'; import { isFunction, isString, @@ -9,27 +9,32 @@ import { } from '../../util/index.mjs'; import V from '../../V/index.mjs'; -export const GridLayer = PaperLayer.extend({ +export class GridLayer extends LayerView { - style: { - 'pointer-events': 'none' - }, + _gridCache = null; + _gridSettings = null; - _gridCache: null, - _gridSettings: null, + preinitialize() { + super.preinitialize(); - init() { - PaperLayer.prototype.init.apply(this, arguments); + this.style = { + 'pointer-events': 'none' + } + + } + + init(...args) { + super.init(...args); const { options: { paper }} = this; this._gridCache = null; this._gridSettings = []; this.listenTo(paper, 'transform resize', this.updateGrid); - }, + } setGrid(drawGrid) { this._gridSettings = this.getGridSettings(drawGrid); this.renderGrid(); - }, + } getGridSettings(drawGrid) { const gridSettings = []; @@ -40,14 +45,14 @@ export const GridLayer = PaperLayer.extend({ }); } return gridSettings; - }, + } removeGrid() { const { _gridCache: grid } = this; if (!grid) return; grid.root.remove(); this._gridCache = null; - }, + } renderGrid() { @@ -92,7 +97,7 @@ export const GridLayer = PaperLayer.extend({ refs.root.appendTo(this.el); this.updateGrid(); - }, + } updateGrid() { @@ -111,11 +116,11 @@ export const GridLayer = PaperLayer.extend({ options.update(vPattern.node.firstChild, options, paper); } }); - }, + } _getPatternId(index) { return `pattern_${this.options.paper.cid}_${index}`; - }, + } _getGridRefs() { let { _gridCache: grid } = this; @@ -139,34 +144,34 @@ export const GridLayer = PaperLayer.extend({ } }; return grid; - }, + } _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); + const 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]); } @@ -175,11 +180,11 @@ export const GridLayer = PaperLayer.extend({ } return isArray ? options : [options]; - }, + } isEmpty() { const { _gridCache: grid } = this; return this.el.children.length === (grid ? 1 : 0); } -}); +} From 724b1a83d865ffea80e7fef83f8b5e386b692abb Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 21 Feb 2025 18:10:55 +0100 Subject: [PATCH 02/54] wip --- packages/joint-core/src/dia/Cell.mjs | 4 ++ packages/joint-core/src/dia/Graph.mjs | 77 ++++++++++++----------- packages/joint-core/src/dia/Layer.mjs | 47 ++++++++++---- packages/joint-core/src/dia/LayerView.mjs | 8 +-- packages/joint-core/src/dia/Paper.mjs | 8 ++- packages/joint-core/src/dia/ToolsView.mjs | 2 +- packages/joint-core/src/dia/index.mjs | 3 +- packages/joint-core/types/joint.d.ts | 6 +- 8 files changed, 96 insertions(+), 59 deletions(-) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index e0cebc9cba..49667a6153 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -942,6 +942,10 @@ export const Cell = Model.extend({ .getPointRotatedAroundCenter(this.angle(), x, y) // Transform the absolute position into relative .difference(this.position()); + }, + + layer() { + return this.get('layer') || null; } }, { diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index f55185f4ee..8f652beebe 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -20,7 +20,6 @@ const GraphCells = Collection.extend({ /* eslint-enable no-undef */ } - this.graph = opt.graph; }, @@ -71,14 +70,18 @@ export const Graph = Model.extend({ opt = opt || {}; + this.defaultLayerName = LayersNames.CELLS; + const defaultLayer = new Layer({ - name: LayersNames.CELLS, + name: this.defaultLayerName, displayName: 'Default' }); - this.set('layers', [ - defaultLayer - ]); + this.set('layers', { + [defaultLayer.name]: defaultLayer + }); + + this.useLayersForEmbedding = opt.useLayersForEmbedding || false; // Passing `cellModel` function in the options object to graph allows for // setting models based on attribute objects. This is especially handy @@ -94,10 +97,6 @@ export const Graph = Model.extend({ // 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); - // `joint.dia.Graph` keeps an internal data structure (an adjacency list) // for fast graph queries. All changes that affect the structure of the graph // must be reflected in the `al` object. This object provides fast answers to @@ -130,11 +129,6 @@ export const Graph = Model.extend({ cells.on('remove', this._removeCell, this); }, - _sortOnChangeZ: function() { - - this.get('cells').sort(); - }, - _restructureOnAdd: function(cell) { if (cell.isLink()) { @@ -149,6 +143,15 @@ export const Graph = Model.extend({ } else { this._nodes[cell.id] = true; } + + const layerName = cell.layer() || this.defaultLayerName; + const layer = this.get('layers')[layerName]; + + if (!cell.has('z')) { + cell.set('z', layer.maxZIndex() + 1); + } + + layer.add(cell); }, _restructureOnRemove: function(cell) { @@ -165,18 +168,28 @@ export const Graph = Model.extend({ } else { delete this._nodes[cell.id]; } + + const layerName = cell.layer() || this.defaultLayerName; + const layer = this.get('layers')[layerName]; + + layer.remove(cell); }, - _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 = {}; this._nodes = {}; this._edges = {}; + const layers = this.get('layers'); + for (let layerName in layers) { + layers[layerName].clear(); + } + cells.forEach(this._restructureOnAdd, this); }, @@ -305,16 +318,18 @@ export const Graph = Model.extend({ return cell; }, - minZIndex: function() { + minZIndex: function(layer) { + const layers = this.get('layers'); + layer = layer || this.defaultLayerName; - var firstCell = this.get('cells').first(); - return firstCell ? (firstCell.get('z') || 0) : 0; + return layers[layer].minZIndex(); }, - maxZIndex: function() { + maxZIndex: function(layer) { + const layers = this.get('layers'); + layer = layer || this.defaultLayerName; - var lastCell = this.get('cells').last(); - return lastCell ? (lastCell.get('z') || 0) : 0; + return layers[layer].maxZIndex(); }, addCell: function(cell, opt) { @@ -324,17 +339,6 @@ 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 || {}); return this; @@ -348,10 +352,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; @@ -365,6 +369,7 @@ export const Graph = Model.extend({ var preparedCells = util.toArray(cells).map(function(cell) { return this._prepareCell(cell, opt); }, this); + this.get('cells').reset(preparedCells, opt); return this; diff --git a/packages/joint-core/src/dia/Layer.mjs b/packages/joint-core/src/dia/Layer.mjs index c00c097c3d..121e5ff5e7 100644 --- a/packages/joint-core/src/dia/Layer.mjs +++ b/packages/joint-core/src/dia/Layer.mjs @@ -1,4 +1,4 @@ -import { Model } from '../mvc/index.mjs'; +import { Model, Collection } from '../mvc/index.mjs'; export const LayersNames = { GRID: 'grid', @@ -9,27 +9,52 @@ export const LayersNames = { LABELS: 'labels' }; +class LayerCells extends Collection { + + // `comparator` makes it easy to sort cells based on their `z` index. + comparator(model) { + return model.get('z') || 0; + } +} + export class Layer extends Model { defaults() { return { - name: '', displayName: '', }; } - initialize() { - this.models = []; + initialize(attrs) { + this.name = attrs.name; + + const cells = new LayerCells(); + this.set('cells', cells); + + cells.on('change:z', () => { + cells.sort(); + }); + } + + add(cell) { + this.get('cells').add(cell); + } + + remove(cell) { + this.get('cells').remove(cell); + } + + clear() { + this.get('cells').reset(); } - addModel(item) { - this.models.push(item); + minZIndex() { + const firstCell = this.get('cells').first(); + return firstCell ? (firstCell.get('z') || 0) : 0; } - removeModel(item) { - const index = this.models.indexOf(item); - if (index !== -1) { - this.models.splice(index, 1); - } + maxZIndex() { + const lastCell = this.get('cells').last(); + return lastCell ? (lastCell.get('z') || 0) : 0; } } diff --git a/packages/joint-core/src/dia/LayerView.mjs b/packages/joint-core/src/dia/LayerView.mjs index b67d2b23e9..749a9184b3 100644 --- a/packages/joint-core/src/dia/LayerView.mjs +++ b/packages/joint-core/src/dia/LayerView.mjs @@ -2,12 +2,12 @@ import { View } from '../mvc/index.mjs'; export class LayerView extends View { - pivotNodes = null; defaultTheme = null; preinitialize() { this.tagName = 'g'; this.svgElement = true; + this.pivotNodes = {}; this.options = { name: '' @@ -20,10 +20,6 @@ export class LayerView extends View { return addClassNamePrefix(`${name}-layer`); } - init() { - this.pivotNodes = {}; - } - insertSortedNode(node, z) { this.el.insertBefore(node, this.insertPivot(z)); } @@ -61,7 +57,7 @@ export class LayerView extends View { return pivotNode; } - removePivotNodes() { + removePivots() { const { el, pivotNodes } = this; for (let z in pivotNodes) { el.removeChild(pivotNodes[z]); diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index add1f01e32..ecdffe3b68 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -396,7 +396,9 @@ export const Paper = View.extend({ name: LayersNames.BACK, }]; - graphLayers.forEach(layer => { + Object.keys(graphLayers).forEach(name => { + const layer = graphLayers[name]; + this._layersSettings.push({ name: layer.get('name'), model: layer @@ -737,7 +739,7 @@ export const Paper = View.extend({ this.defs = defs; this.layers = layers; - this.renderLayers(); + this.renderLayers(this._layersSettings); V.ensureId(svg); @@ -774,7 +776,7 @@ export const Paper = View.extend({ return layerView; }, - renderLayers: function(layers = defaultLayers) { + renderLayers: function(layers) { this.removeLayers(); layers.forEach(attributes => this.renderLayer(attributes)); // Throws an exception if doesn't exist diff --git a/packages/joint-core/src/dia/ToolsView.mjs b/packages/joint-core/src/dia/ToolsView.mjs index fcce15b1dd..054f3b2490 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 { LayersNames } from './Layer.mjs'; import { ToolView } from './ToolView.mjs'; export const ToolsView = mvc.View.extend({ diff --git a/packages/joint-core/src/dia/index.mjs b/packages/joint-core/src/dia/index.mjs index 9a25dc2fb6..132b9d1da7 100644 --- a/packages/joint-core/src/dia/index.mjs +++ b/packages/joint-core/src/dia/index.mjs @@ -1,6 +1,7 @@ export * from './Graph.mjs'; export * from './attributes/index.mjs'; -export * from './PaperLayer.mjs'; +export * from './Layer.mjs'; +export * from './LayerView.mjs'; export * from './Cell.mjs'; export * from './CellView.mjs'; export * from './Element.mjs'; diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index fc5dfcc49f..72ddbe4f52 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -193,7 +193,11 @@ export namespace dia { 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, + useLayersForEmbedding: boolean, + }); addCell(cell: Cell.JSON | Cell, opt?: CollectionAddOptions): this; addCell(cell: Array, opt?: CollectionAddOptions): this; From 044af30f2413accdaadcbdd38fd5ee8b73a6f0a2 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Sun, 23 Feb 2025 20:30:42 +0100 Subject: [PATCH 03/54] layer models --- packages/joint-core/src/dia/Cell.mjs | 8 ++++++ packages/joint-core/src/dia/Graph.mjs | 35 +++++++++++++++++++++++++++ packages/joint-core/src/dia/Layer.mjs | 2 ++ 3 files changed, 45 insertions(+) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index 49667a6153..4c41dcaa97 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -944,6 +944,14 @@ export const Cell = Model.extend({ .difference(this.position()); }, + setLayer(layerName) { + if (layerName) { + this.set('layer', layerName); + } else { + this.unset('layer'); + } + }, + layer() { return this.get('layer') || null; } diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 8f652beebe..f779b705ae 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -83,6 +83,10 @@ export const Graph = Model.extend({ this.useLayersForEmbedding = opt.useLayersForEmbedding || false; + if (this.useLayersForEmbedding) { + this.set('embeddingLayers', {}); + } + // 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. @@ -127,6 +131,8 @@ export const Graph = Model.extend({ cells.on('change:source', this._restructureOnChangeSource, this); cells.on('change:target', this._restructureOnChangeTarget, this); cells.on('remove', this._removeCell, this); + + cells.on('change:parent', this._onCellParentChange, this); }, _restructureOnAdd: function(cell) { @@ -217,6 +223,35 @@ export const Graph = Model.extend({ } }, + _onCellParentChange: function(cell, parentId, opt) { + if (this.useLayersForEmbedding) { + const embeddingLayers = this.get('embeddingLayers'); + const layers = this.get('layers'); + + const currentLayer = cell.layer() || this.defaultLayerName; + + if (layers[currentLayer]) { + layers[currentLayer].remove(cell); + } else if (embeddingLayers[currentLayer]) { + embeddingLayers[currentLayer].remove(cell); + } + + if (parentId && !embeddingLayers[parentId]) { + embeddingLayers[parentId] = new Layer({ + name: parentId, + displayName: parentId + }); + } + + const targetLayer = embeddingLayers[parentId] || layers[this.defaultLayerName]; + + targetLayer.add(cell); + + console.log('embeddingLayers', embeddingLayers); + console.log('layers', layers); + } + }, + // Return all outbound edges for the node. Return value is an object // of the form: [edgeId] -> true getOutboundEdges: function(node) { diff --git a/packages/joint-core/src/dia/Layer.mjs b/packages/joint-core/src/dia/Layer.mjs index 121e5ff5e7..a2e841f82d 100644 --- a/packages/joint-core/src/dia/Layer.mjs +++ b/packages/joint-core/src/dia/Layer.mjs @@ -37,10 +37,12 @@ export class Layer extends Model { } add(cell) { + cell.setLayer(this.name); this.get('cells').add(cell); } remove(cell) { + cell.setLayer(); this.get('cells').remove(cell); } From af043f26a491a9c3eaa92da28ea41690a62f5056 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Sun, 23 Feb 2025 22:36:57 +0100 Subject: [PATCH 04/54] wip --- packages/joint-core/src/dia/Graph.mjs | 17 ++++++++++++++++- packages/joint-core/src/dia/Paper.mjs | 11 +++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index f779b705ae..00eed33812 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -176,9 +176,24 @@ export const Graph = Model.extend({ } const layerName = cell.layer() || this.defaultLayerName; - const layer = this.get('layers')[layerName]; + + const layers = this.get('layers'); + const embeddingLayers = this.get('embeddingLayers'); + + const layer = layers[layerName] || embeddingLayers[layerName]; layer.remove(cell); + + if (embeddingLayers[cell.id]) { + const embeddingLayer = embeddingLayers[cell.id]; + const cells = embeddingLayer.get('cells').models; + cells.forEach((cell) => { + layers[this.defaultLayerName].add(cell); + }); + + this.trigger('embeddingLayers:remove', embeddingLayer, {}); + delete embeddingLayers[cell.id]; + } }, _restructureOnReset: function(collection) { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index ecdffe3b68..060c916890 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -467,6 +467,10 @@ export const Paper = View.extend({ .listenTo(model, 'reset', this.onGraphReset) .listenTo(model, 'sort', this.onGraphSort) .listenTo(model, 'batch:stop', this.onGraphBatchStop); + + this.listenTo(model, 'embeddingLayer:insert', this.onEmbeddingLayerInsert); + this.listenTo(model, 'embeddingLayer:remove', this.onEmbeddingLayerRemove); + this.on('cell:highlight', this.onCellHighlight) .on('cell:unhighlight', this.onCellUnhighlight) .on('transform', this.update); @@ -525,6 +529,13 @@ export const Paper = View.extend({ } }, + onEmbeddingLayerInsert: function(layer, opt) { + }, + + onEmbeddingLayerRemove: function(layer, opt) { + + }, + cloneOptions: function() { const { options } = this; From 60bbcd987a67f788d354e24609153eae6d85c8cf Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 25 Feb 2025 11:51:42 +0100 Subject: [PATCH 05/54] update --- packages/joint-core/src/dia/Graph.mjs | 6 +-- packages/joint-core/src/dia/LayerView.mjs | 4 ++ packages/joint-core/src/dia/Paper.mjs | 48 +++++++++++++++-------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 00eed33812..e7eb71e300 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -191,7 +191,7 @@ export const Graph = Model.extend({ layers[this.defaultLayerName].add(cell); }); - this.trigger('embeddingLayers:remove', embeddingLayer, {}); + this.trigger('embeddingLayer:remove', embeddingLayer, {}); delete embeddingLayers[cell.id]; } }, @@ -256,14 +256,12 @@ export const Graph = Model.extend({ name: parentId, displayName: parentId }); + this.trigger('embeddingLayer:insert', embeddingLayers[parentId], {}); } const targetLayer = embeddingLayers[parentId] || layers[this.defaultLayerName]; targetLayer.add(cell); - - console.log('embeddingLayers', embeddingLayers); - console.log('layers', layers); } }, diff --git a/packages/joint-core/src/dia/LayerView.mjs b/packages/joint-core/src/dia/LayerView.mjs index 749a9184b3..cd85b40306 100644 --- a/packages/joint-core/src/dia/LayerView.mjs +++ b/packages/joint-core/src/dia/LayerView.mjs @@ -68,4 +68,8 @@ export class LayerView extends View { isEmpty() { return this.el.children.length === 0; } + + get name() { + return this.options.name; + } } diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 060c916890..02c239e458 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -416,10 +416,12 @@ export const Paper = View.extend({ // Layers (SVGGroups) this._layers = { viewsMap: {}, - namesMap: {}, order: [], }; + this._embeddingLayers = { + }; + this.cloneOptions(); this.render(); this._setDimensions(); @@ -530,10 +532,26 @@ export const Paper = View.extend({ }, onEmbeddingLayerInsert: function(layer, opt) { + const cellId = layer.get('name'); + const layerView = this.createLayer({ name: cellId, model: layer }); + + const cellView = this._views[cellId]; + if (cellView.isMounted()) { + cellView.el.after(layerView.el); + } + + this._embeddingLayers[cellId] = layerView; + this._layers.viewsMap[cellId] = layerView; }, onEmbeddingLayerRemove: function(layer, opt) { + const cellId = layer.get('name'); + const layerView = this._embeddingLayers.viewsMap[cellId]; + + delete this._embeddingLayers[cellId]; + delete this._layers.viewsMap[cellId]; + layerView.remove(); }, cloneOptions: function() { @@ -640,39 +658,29 @@ export const Paper = View.extend({ }, _unregisterLayer(layerView) { - const { _layers: { viewsMap, namesMap, order }} = this; - const layerName = this._getLayerName(layerView); + const { _layers: { viewsMap, order }} = this; + const layerName = layerView.name; order.splice(order.indexOf(layerName), 1); - delete namesMap[layerView.cid]; delete viewsMap[layerName]; }, _registerLayer(layerName, layerView, beforeLayerView) { - const { _layers: { viewsMap, namesMap, order }} = this; + const { _layers: { viewsMap, order }} = this; if (beforeLayerView) { - const beforeLayerName = this._getLayerName(beforeLayerView); + const beforeLayerName = beforeLayerView.name; order.splice(order.indexOf(beforeLayerName), 0, layerName); } else { order.push(layerName); } viewsMap[layerName] = layerView; - namesMap[layerView.cid] = layerName; }, _getLayerView(layer) { - const { _layers: { namesMap, viewsMap }} = this; - if (layer instanceof LayerView) { - if (layer.cid in namesMap) return layer; - return null; - } + const { _layers: { viewsMap }} = this; if (layer in viewsMap) return viewsMap[layer]; return null; }, - _getLayerName(layerView) { - const { _layers: { namesMap }} = this; - return namesMap[layerView.cid]; - }, _requireLayerView(layer) { const layerView = this._getLayerView(layer); @@ -722,7 +730,7 @@ export const Paper = View.extend({ moveLayer(layer, insertBefore) { const layerView = this._requireLayerView(layer); if (layerView === this._getLayerView(insertBefore)) return; - const layerName = this._getLayerName(layerView); + const layerName = layerView.name; this._unregisterLayer(layerView); this.addLayer(layerName, layerView, { insertBefore }); }, @@ -1942,6 +1950,12 @@ export const Paper = View.extend({ layerView.insertNode(el); break; } + + if (this._embeddingLayers[model.id]) { + const layerView = this._embeddingLayers[model.id]; + el.after(layerView.el); + } + view.onMount(isInitialInsert); }, From 5832a22412b301d28fa8645d413c213a90c83f25 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 25 Feb 2025 12:58:19 +0100 Subject: [PATCH 06/54] fix --- packages/joint-core/src/dia/Paper.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 02c239e458..96fc7294ea 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -546,7 +546,7 @@ export const Paper = View.extend({ onEmbeddingLayerRemove: function(layer, opt) { const cellId = layer.get('name'); - const layerView = this._embeddingLayers.viewsMap[cellId]; + const layerView = this._embeddingLayers[cellId]; delete this._embeddingLayers[cellId]; delete this._layers.viewsMap[cellId]; From bb09418401e5cc5100853fc7ce9d1798f2e6ba7c Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 25 Feb 2025 17:40:25 +0100 Subject: [PATCH 07/54] up --- packages/joint-core/src/dia/Cell.mjs | 4 ++-- packages/joint-core/src/dia/Graph.mjs | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index 4c41dcaa97..33b5e24ce7 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -261,7 +261,7 @@ export const Cell = Model.extend({ const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z()); - const maxZ = graph.maxZIndex(); + const maxZ = graph.maxZIndex(this.layer()); let z = maxZ - cells.length + 1; const collection = graph.get('cells'); @@ -304,7 +304,7 @@ export const Cell = Model.extend({ const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z()); - let z = graph.minZIndex(); + let z = graph.minZIndex(this.layer()); var collection = graph.get('cells'); diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index e7eb71e300..e4a0d29ec5 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -366,18 +366,26 @@ export const Graph = Model.extend({ return cell; }, - minZIndex: function(layer) { + minZIndex: function(layerName) { + layerName = layerName || this.defaultLayerName; + const layers = this.get('layers'); - layer = layer || this.defaultLayerName; + const embeddingLayers = this.get('embeddingLayers'); + + const layer = layers[layerName] || embeddingLayers[layerName]; - return layers[layer].minZIndex(); + return layer.minZIndex(); }, - maxZIndex: function(layer) { + maxZIndex: function(layerName) { + layerName = layerName || this.defaultLayerName; + const layers = this.get('layers'); - layer = layer || this.defaultLayerName; + const embeddingLayers = this.get('embeddingLayers'); + + const layer = layers[layerName] || embeddingLayers[layerName]; - return layers[layer].maxZIndex(); + return layer.maxZIndex(); }, addCell: function(cell, opt) { From 9a9aeac4ad666151a4ec779782cb233f318a31ea Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 26 Feb 2025 16:29:45 +0100 Subject: [PATCH 08/54] up --- packages/joint-core/src/dia/Graph.mjs | 4 +++ packages/joint-core/src/dia/Layer.mjs | 4 +++ packages/joint-core/types/joint.d.ts | 38 ++++++++++++++++++--------- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index e4a0d29ec5..d1e90be53f 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -500,6 +500,10 @@ export const Graph = Model.extend({ this.stopBatch(batchName); }, + getLayers() { + return Object.values(this.get('layers')); + }, + // Get a cell by `id`. getCell: function(id) { diff --git a/packages/joint-core/src/dia/Layer.mjs b/packages/joint-core/src/dia/Layer.mjs index a2e841f82d..ffdcfed12b 100644 --- a/packages/joint-core/src/dia/Layer.mjs +++ b/packages/joint-core/src/dia/Layer.mjs @@ -22,10 +22,14 @@ export class Layer extends Model { defaults() { return { displayName: '', + hidden: false, + locked: false, }; } initialize(attrs) { + super.initialize(attrs); + this.name = attrs.name; const cells = new LayerCells(); diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 72ddbe4f52..455d8e7dc8 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -206,6 +206,8 @@ export namespace dia { resetCells(cells: Array, opt?: Graph.Options): this; + getLayers(): Layer[]; + getCell(id: Cell.ID | Cell): Cell; getElements(): Element[]; @@ -309,9 +311,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; @@ -1756,7 +1758,7 @@ export namespace dia { getLayerNode(layerName: Paper.Layers | string): SVGGElement; - getLayerView(layerName: Paper.Layers | string): PaperLayer; + getLayerView(layerName: Paper.Layers | string): LayerView; hasLayerView(layerName: Paper.Layers | string): boolean; @@ -1766,17 +1768,17 @@ export namespace dia { protected resetLayers(): void; - addLayer(layerName: string, layerView: PaperLayer, options?: { insertBefore?: string }): void; + addLayer(layerName: string, layerView: LayerView, options?: { insertBefore?: string }): void; - removeLayer(layer: string | PaperLayer): void; + removeLayer(layer: string | LayerView): void; - moveLayer(layer: string | PaperLayer, insertBefore: string | PaperLayer | null): void; + moveLayer(layer: string | LayerView, insertBefore: string | LayerView | null): void; - hasLayer(layer: string | PaperLayer): boolean; + hasLayer(layer: string | LayerView): boolean; getLayerNames(): string[]; - getLayers(): Array; + getLayers(): Array; // rendering @@ -1969,17 +1971,17 @@ export namespace dia { scaleContentToFit(opt?: Paper.ScaleContentOptions): void; } - namespace PaperLayer { + namespace LayerView { interface Options extends mvc.ViewOptions { name: string; } } - class PaperLayer extends mvc.View { + class LayerView extends mvc.View { - constructor(opt?: PaperLayer.Options); + constructor(opt?: LayerView.Options); - options: PaperLayer.Options; + options: LayerView.Options; pivotNodes: { [z: number]: Comment }; @@ -1992,6 +1994,18 @@ export namespace dia { removePivots(): void; } + class Layer extends mvc.Model { + add(cell: Cell): void; + + remove(cell: Cell): void; + + //clear(): void; + + minZIndex(): number; + + maxZIndex(): number; + } + namespace ToolsView { interface Options extends mvc.ViewOptions { From 6249685d5639a750339753b0d9f022886f97a75d Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 28 Feb 2025 09:47:08 +0100 Subject: [PATCH 09/54] up --- packages/joint-core/types/joint.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 455d8e7dc8..c081ebccf6 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -196,7 +196,7 @@ export namespace dia { constructor(attributes?: Graph.Attributes, opt?: { cellNamespace?: any, cellModel?: typeof Cell, - useLayersForEmbedding: boolean, + useLayersForEmbedding?: boolean, }); addCell(cell: Cell.JSON | Cell, opt?: CollectionAddOptions): this; From 1a208bc967cb44fe4e8c86b98de0e10517deacc2 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Sun, 2 Mar 2025 14:52:29 +0100 Subject: [PATCH 10/54] up --- packages/joint-core/src/dia/Graph.mjs | 39 ++++++++++++++++++++++++--- packages/joint-core/src/dia/Paper.mjs | 18 ++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index d1e90be53f..7d8264169e 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -71,6 +71,7 @@ export const Graph = Model.extend({ opt = opt || {}; this.defaultLayerName = LayersNames.CELLS; + this.activeLayerName = this.defaultLayerName; const defaultLayer = new Layer({ name: this.defaultLayerName, @@ -150,7 +151,7 @@ export const Graph = Model.extend({ this._nodes[cell.id] = true; } - const layerName = cell.layer() || this.defaultLayerName; + const layerName = cell.layer() || this.activeLayerName; const layer = this.get('layers')[layerName]; if (!cell.has('z')) { @@ -175,6 +176,7 @@ export const Graph = Model.extend({ delete this._nodes[cell.id]; } + // should be defined at any time ideally const layerName = cell.layer() || this.defaultLayerName; const layers = this.get('layers'); @@ -188,7 +190,7 @@ export const Graph = Model.extend({ const embeddingLayer = embeddingLayers[cell.id]; const cells = embeddingLayer.get('cells').models; cells.forEach((cell) => { - layers[this.defaultLayerName].add(cell); + layers[this.activeLayerName].add(cell); }); this.trigger('embeddingLayer:remove', embeddingLayer, {}); @@ -259,7 +261,7 @@ export const Graph = Model.extend({ this.trigger('embeddingLayer:insert', embeddingLayers[parentId], {}); } - const targetLayer = embeddingLayers[parentId] || layers[this.defaultLayerName]; + const targetLayer = embeddingLayers[parentId] || layers[this.activeLayerName]; targetLayer.add(cell); } @@ -500,10 +502,41 @@ export const Graph = Model.extend({ this.stopBatch(batchName); }, + getActiveLayer() { + return this.get('layers')[this.activeLayerName]; + }, + + addLayer(layer, opt) { + const layers = this.get('layers'); + if (layers[layer.name]) { + throw new Exception(`dia.Graph: Layer with name '${layer.name}' already exists.`); + } + + layers[layer.name] = layer; + }, + + removeLayer(layerName, opt) { + const layers = this.get('layers'); + if (!layers[layerName]) { + throw new Exception(`dia.Graph: Layer with name '${layerName}' does not exist.`); + } + + delete layers[layerName]; + }, + getLayers() { return Object.values(this.get('layers')); }, + setActiveLayer(layerName) { + const layers = this.get('layers'); + if (!layers[layerName]) { + throw new Exception(`dia.Graph: Layer with name '${layerName}' does not exist.`); + } + + this.activeLayerName = layerName; + }, + // Get a cell by `id`. getCell: function(id) { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 96fc7294ea..9a4533601e 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -473,6 +473,9 @@ export const Paper = View.extend({ this.listenTo(model, 'embeddingLayer:insert', this.onEmbeddingLayerInsert); this.listenTo(model, 'embeddingLayer:remove', this.onEmbeddingLayerRemove); + this.listenTo(model, 'layer:insert', this.onLayerInsert); + this.listenTo(model, 'layer:remove', this.onLayerRemove); + this.on('cell:highlight', this.onCellHighlight) .on('cell:unhighlight', this.onCellUnhighlight) .on('transform', this.update); @@ -554,6 +557,16 @@ export const Paper = View.extend({ layerView.remove(); }, + onLayerInsert: function(layer, opt) { + const layerView = this.createLayer({ name: layer.get('name'), model: layer }); + this.addLayer(layer.get('name'), layerView, { insertBefore: LayersNames.LABELS }); + }, + + onLayerRemove: function(layerName, opt) { + const { viewsMap } = this._layers; + this.removeLayer(viewsMap[layerName]); + }, + cloneOptions: function() { const { options } = this; @@ -675,13 +688,12 @@ export const Paper = View.extend({ viewsMap[layerName] = layerView; }, - _getLayerView(layer) { + _getLayerView(layerName) { const { _layers: { viewsMap }} = this; - if (layer in viewsMap) return viewsMap[layer]; + if (layerName in viewsMap) return viewsMap[layerName]; return null; }, - _requireLayerView(layer) { const layerView = this._getLayerView(layer); if (!layerView) { From 309edaa9849d44b9a63b22a01e2d668995171317 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 4 Mar 2025 15:38:50 +0100 Subject: [PATCH 11/54] wip --- packages/joint-core/src/dia/Graph.mjs | 93 +++++++++++++++------------ packages/joint-core/src/dia/Paper.mjs | 13 ++-- packages/joint-core/types/joint.d.ts | 13 +++- 3 files changed, 71 insertions(+), 48 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 7d8264169e..97f2db714b 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -78,14 +78,18 @@ export const Graph = Model.extend({ displayName: 'Default' }); - this.set('layers', { + this.set('layers', [ + defaultLayer + ]); + + this.layersMap = { [defaultLayer.name]: defaultLayer - }); + } this.useLayersForEmbedding = opt.useLayersForEmbedding || false; if (this.useLayersForEmbedding) { - this.set('embeddingLayers', {}); + this.embeddingLayers = {}; } // Passing `cellModel` function in the options object to graph allows for @@ -152,7 +156,7 @@ export const Graph = Model.extend({ } const layerName = cell.layer() || this.activeLayerName; - const layer = this.get('layers')[layerName]; + const layer = this.layersMap[layerName]; if (!cell.has('z')) { cell.set('z', layer.maxZIndex() + 1); @@ -176,13 +180,12 @@ export const Graph = Model.extend({ delete this._nodes[cell.id]; } - // should be defined at any time ideally - const layerName = cell.layer() || this.defaultLayerName; + const { layersMap, embeddingLayers, defaultLayerName, activeLayerName } = this; - const layers = this.get('layers'); - const embeddingLayers = this.get('embeddingLayers'); + // should be defined at any time ideally + const layerName = cell.layer() || defaultLayerName; - const layer = layers[layerName] || embeddingLayers[layerName]; + const layer = layersMap[layerName] || embeddingLayers[layerName]; layer.remove(cell); @@ -190,7 +193,7 @@ export const Graph = Model.extend({ const embeddingLayer = embeddingLayers[cell.id]; const cells = embeddingLayer.get('cells').models; cells.forEach((cell) => { - layers[this.activeLayerName].add(cell); + layersMap[activeLayerName].add(cell); }); this.trigger('embeddingLayer:remove', embeddingLayer, {}); @@ -242,13 +245,12 @@ export const Graph = Model.extend({ _onCellParentChange: function(cell, parentId, opt) { if (this.useLayersForEmbedding) { - const embeddingLayers = this.get('embeddingLayers'); - const layers = this.get('layers'); + const { layersMap, embeddingLayers, defaultLayerName, activeLayerName } = this; - const currentLayer = cell.layer() || this.defaultLayerName; + const currentLayer = cell.layer() || defaultLayerName; - if (layers[currentLayer]) { - layers[currentLayer].remove(cell); + if (layersMap[currentLayer]) { + layersMap[currentLayer].remove(cell); } else if (embeddingLayers[currentLayer]) { embeddingLayers[currentLayer].remove(cell); } @@ -261,7 +263,7 @@ export const Graph = Model.extend({ this.trigger('embeddingLayer:insert', embeddingLayers[parentId], {}); } - const targetLayer = embeddingLayers[parentId] || layers[this.activeLayerName]; + const targetLayer = embeddingLayers[parentId] || layersMap[activeLayerName]; targetLayer.add(cell); } @@ -369,23 +371,21 @@ export const Graph = Model.extend({ }, minZIndex: function(layerName) { - layerName = layerName || this.defaultLayerName; + const { layersMap, embeddingLayers, defaultLayerName } = this; - const layers = this.get('layers'); - const embeddingLayers = this.get('embeddingLayers'); + layerName = layerName || defaultLayerName; - const layer = layers[layerName] || embeddingLayers[layerName]; + const layer = layersMap[layerName] || embeddingLayers[layerName]; return layer.minZIndex(); }, maxZIndex: function(layerName) { - layerName = layerName || this.defaultLayerName; + const { layersMap, embeddingLayers, defaultLayerName } = this; - const layers = this.get('layers'); - const embeddingLayers = this.get('embeddingLayers'); + layerName = layerName || defaultLayerName; - const layer = layers[layerName] || embeddingLayers[layerName]; + const layer = layersMap[layerName] || embeddingLayers[layerName]; return layer.maxZIndex(); }, @@ -502,41 +502,50 @@ export const Graph = Model.extend({ this.stopBatch(batchName); }, - getActiveLayer() { - return this.get('layers')[this.activeLayerName]; - }, - addLayer(layer, opt) { - const layers = this.get('layers'); - if (layers[layer.name]) { - throw new Exception(`dia.Graph: Layer with name '${layer.name}' already exists.`); + if (this.layersMap[layer.name]) { + throw new Error(`dia.Graph: Layer with name '${layer.name}' already exists.`); } - layers[layer.name] = layer; + const layers = this.get('layers'); + + layers.push(layer); + + this.layersMap[layer.name] = layer; + this.set('layers', layers); }, removeLayer(layerName, opt) { - const layers = this.get('layers'); - if (!layers[layerName]) { - throw new Exception(`dia.Graph: Layer with name '${layerName}' does not exist.`); + if (layerName === this.defaultLayerName || layerName === this.activeLayerName) { + throw new Error(`dia.Graph: default or active layer cannot be removed.`); } - delete layers[layerName]; - }, + if (!this.layersMap[layerName]) { + throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); + } + + const layers = this.get('layers'); - getLayers() { - return Object.values(this.get('layers')); + delete this.layersMap[layerName]; + this.set('layers', layers.filter(l => l.name !== layerName)); }, setActiveLayer(layerName) { - const layers = this.get('layers'); - if (!layers[layerName]) { - throw new Exception(`dia.Graph: Layer with name '${layerName}' does not exist.`); + if (!this.layersMap[layerName]) { + throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); } this.activeLayerName = layerName; }, + getActiveLayer() { + return this.layersMap[this.activeLayerName]; + }, + + getDefaultLayer() { + return this.layersMap[this.defaultLayerName]; + }, + // Get a cell by `id`. getCell: function(id) { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 9a4533601e..d5e4619ca0 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -390,6 +390,10 @@ export const Paper = View.extend({ const graphLayers = model.get('layers'); + this._graphLayers = { + + }; + this._layersSettings = [{ name: LayersNames.GRID, }, { @@ -473,8 +477,7 @@ export const Paper = View.extend({ this.listenTo(model, 'embeddingLayer:insert', this.onEmbeddingLayerInsert); this.listenTo(model, 'embeddingLayer:remove', this.onEmbeddingLayerRemove); - this.listenTo(model, 'layer:insert', this.onLayerInsert); - this.listenTo(model, 'layer:remove', this.onLayerRemove); + this.listenTo(model, 'change:layers', this.onLayersChange); this.on('cell:highlight', this.onCellHighlight) .on('cell:unhighlight', this.onCellUnhighlight) @@ -557,9 +560,9 @@ export const Paper = View.extend({ layerView.remove(); }, - onLayerInsert: function(layer, opt) { - const layerView = this.createLayer({ name: layer.get('name'), model: layer }); - this.addLayer(layer.get('name'), layerView, { insertBefore: LayersNames.LABELS }); + onLayersChange: function(layer, opt) { + /*const layerView = this.createLayer({ name: layer.get('name'), model: layer }); + this.addLayer(layer.get('name'), layerView, { insertBefore: LayersNames.LABELS });*/ }, onLayerRemove: function(layerName, opt) { diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index c081ebccf6..d720cd8c07 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -206,7 +206,15 @@ export namespace dia { resetCells(cells: Array, opt?: Graph.Options): this; - getLayers(): Layer[]; + addLayer(layer: Layer): void; + + removeLayer(layerName: string): void; + + getActiveLayer(): Layer; + + setActiveLayer(layerName: string): Layer; + + getDefaultLayer(): Layer; getCell(id: Cell.ID | Cell): Cell; @@ -1995,6 +2003,9 @@ export namespace dia { } class Layer extends mvc.Model { + + name: string; + add(cell: Cell): void; remove(cell: Cell): void; From 099cf81bcac71b9271fe7f800c870d89763eec82 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Sun, 9 Mar 2025 15:11:01 +0100 Subject: [PATCH 12/54] up --- packages/joint-core/src/dia/Graph.mjs | 4 +- packages/joint-core/src/dia/LayerView.mjs | 5 +- packages/joint-core/src/dia/Paper.mjs | 56 ++++++++++++----------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 97f2db714b..920881fef3 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -509,10 +509,8 @@ export const Graph = Model.extend({ const layers = this.get('layers'); - layers.push(layer); - this.layersMap[layer.name] = layer; - this.set('layers', layers); + this.set('layers', layers.concat([layer])); }, removeLayer(layerName, opt) { diff --git a/packages/joint-core/src/dia/LayerView.mjs b/packages/joint-core/src/dia/LayerView.mjs index cd85b40306..c8b90c6204 100644 --- a/packages/joint-core/src/dia/LayerView.mjs +++ b/packages/joint-core/src/dia/LayerView.mjs @@ -1,4 +1,5 @@ import { View } from '../mvc/index.mjs'; +import { addClassNamePrefix } from '../util/util.mjs'; export class LayerView extends View { @@ -8,10 +9,6 @@ export class LayerView extends View { this.tagName = 'g'; this.svgElement = true; this.pivotNodes = {}; - - this.options = { - name: '' - } } className() { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index d5e4619ca0..8e3186448f 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -388,34 +388,21 @@ export const Paper = View.extend({ const model = this.model = options.model || new Graph; - const graphLayers = model.get('layers'); - - this._graphLayers = { - - }; - + // Paper layers this._layersSettings = [{ name: LayersNames.GRID, }, { name: LayersNames.BACK, - }]; - - Object.keys(graphLayers).forEach(name => { - const layer = graphLayers[name]; - - this._layersSettings.push({ - name: layer.get('name'), - model: layer - }); - }); - - this._layersSettings = this._layersSettings.concat([{ + }, { name: LayersNames.LABELS, + }, { + name: LayersNames.CELLS, + model: this.model.getDefaultLayer() }, { name: LayersNames.FRONT }, { name: LayersNames.TOOLS - }]); + }]; // Layers (SVGGroups) this._layers = { @@ -423,14 +410,18 @@ export const Paper = View.extend({ order: [], }; - this._embeddingLayers = { - }; + this._embeddingLayers = {}; + + this._graphLayers = model.get('layers'); this.cloneOptions(); this.render(); this._setDimensions(); this.startListening(); + this._graphLayers = []; + this.updateGraphLayers(this.model.get('layers')); + // Hash of all cell views. this._views = {}; @@ -560,14 +551,25 @@ export const Paper = View.extend({ layerView.remove(); }, - onLayersChange: function(layer, opt) { - /*const layerView = this.createLayer({ name: layer.get('name'), model: layer }); - this.addLayer(layer.get('name'), layerView, { insertBefore: LayersNames.LABELS });*/ + onLayersChange: function(model, layers) { + this.updateGraphLayers(layers) }, - onLayerRemove: function(layerName, opt) { - const { viewsMap } = this._layers; - this.removeLayer(viewsMap[layerName]); + updateGraphLayers: function(layers) { + const removedLayerNames = this._graphLayers.filter(layer => !layers.some(l => l.name === layer.name)).map(layer => layer.name); + removedLayerNames.forEach(layerName => this.removeLayer(layerName)); + + this._graphLayers = this.model.get('layers'); + + this._graphLayers.forEach(layer => { + if (!this.hasLayerView(layer.name)) { + this.renderLayer({ + name: layer.name, + model: layer + }); + } + this.moveLayer(layer.name, LayersNames.FRONT); + }); }, cloneOptions: function() { From 2f2c48293eefbb289636a69a46461183f316007d Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 18 Mar 2025 09:55:31 +0100 Subject: [PATCH 13/54] up --- packages/joint-core/src/dia/Graph.mjs | 79 +------------------ packages/joint-core/src/dia/Paper.mjs | 73 ++++------------- .../dia/controllers/CellLayersController.js | 45 +++++++++++ .../controllers/EmbeddingLayersController.js | 79 +++++++++++++++++++ .../dia/controllers/GraphLayersController.js | 72 +++++++++++++++++ packages/joint-core/types/joint.d.ts | 6 +- 6 files changed, 220 insertions(+), 134 deletions(-) create mode 100644 packages/joint-core/src/dia/controllers/CellLayersController.js create mode 100644 packages/joint-core/src/dia/controllers/EmbeddingLayersController.js create mode 100644 packages/joint-core/src/dia/controllers/GraphLayersController.js diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 920881fef3..2115c45151 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -6,6 +6,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 { GraphLayersController } from './controllers/GraphLayersController'; const GraphCells = Collection.extend({ @@ -71,26 +72,16 @@ export const Graph = Model.extend({ opt = opt || {}; this.defaultLayerName = LayersNames.CELLS; - this.activeLayerName = this.defaultLayerName; const defaultLayer = new Layer({ - name: this.defaultLayerName, - displayName: 'Default' + name: this.defaultLayerName }); this.set('layers', [ defaultLayer ]); - this.layersMap = { - [defaultLayer.name]: defaultLayer - } - - this.useLayersForEmbedding = opt.useLayersForEmbedding || false; - - if (this.useLayersForEmbedding) { - this.embeddingLayers = {}; - } + this.layersController = new GraphLayersController({ graph: this }); // Passing `cellModel` function in the options object to graph allows for // setting models based on attribute objects. This is especially handy @@ -136,8 +127,6 @@ export const Graph = Model.extend({ cells.on('change:source', this._restructureOnChangeSource, this); cells.on('change:target', this._restructureOnChangeTarget, this); cells.on('remove', this._removeCell, this); - - cells.on('change:parent', this._onCellParentChange, this); }, _restructureOnAdd: function(cell) { @@ -154,15 +143,6 @@ export const Graph = Model.extend({ } else { this._nodes[cell.id] = true; } - - const layerName = cell.layer() || this.activeLayerName; - const layer = this.layersMap[layerName]; - - if (!cell.has('z')) { - cell.set('z', layer.maxZIndex() + 1); - } - - layer.add(cell); }, _restructureOnRemove: function(cell) { @@ -179,26 +159,6 @@ export const Graph = Model.extend({ } else { delete this._nodes[cell.id]; } - - const { layersMap, embeddingLayers, defaultLayerName, activeLayerName } = this; - - // should be defined at any time ideally - const layerName = cell.layer() || defaultLayerName; - - const layer = layersMap[layerName] || embeddingLayers[layerName]; - - layer.remove(cell); - - if (embeddingLayers[cell.id]) { - const embeddingLayer = embeddingLayers[cell.id]; - const cells = embeddingLayer.get('cells').models; - cells.forEach((cell) => { - layersMap[activeLayerName].add(cell); - }); - - this.trigger('embeddingLayer:remove', embeddingLayer, {}); - delete embeddingLayers[cell.id]; - } }, _restructureOnReset: function(collection) { @@ -211,11 +171,6 @@ export const Graph = Model.extend({ this._nodes = {}; this._edges = {}; - const layers = this.get('layers'); - for (let layerName in layers) { - layers[layerName].clear(); - } - cells.forEach(this._restructureOnAdd, this); }, @@ -243,32 +198,6 @@ export const Graph = Model.extend({ } }, - _onCellParentChange: function(cell, parentId, opt) { - if (this.useLayersForEmbedding) { - const { layersMap, embeddingLayers, defaultLayerName, activeLayerName } = this; - - const currentLayer = cell.layer() || defaultLayerName; - - if (layersMap[currentLayer]) { - layersMap[currentLayer].remove(cell); - } else if (embeddingLayers[currentLayer]) { - embeddingLayers[currentLayer].remove(cell); - } - - if (parentId && !embeddingLayers[parentId]) { - embeddingLayers[parentId] = new Layer({ - name: parentId, - displayName: parentId - }); - this.trigger('embeddingLayer:insert', embeddingLayers[parentId], {}); - } - - const targetLayer = embeddingLayers[parentId] || layersMap[activeLayerName]; - - targetLayer.add(cell); - } - }, - // Return all outbound edges for the node. Return value is an object // of the form: [edgeId] -> true getOutboundEdges: function(node) { @@ -515,7 +444,7 @@ export const Graph = Model.extend({ removeLayer(layerName, opt) { if (layerName === this.defaultLayerName || layerName === this.activeLayerName) { - throw new Error(`dia.Graph: default or active layer cannot be removed.`); + throw new Error('dia.Graph: default or active layer cannot be removed.'); } if (!this.layersMap[layerName]) { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 8e3186448f..f3c722db67 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -38,7 +38,7 @@ import { ElementView } from './ElementView.mjs'; import { LinkView } from './LinkView.mjs'; import { Cell } from './Cell.mjs'; import { Graph } from './Graph.mjs'; -import { LayersNames, Layer } from './Layer.mjs'; +import { LayersNames } from './Layer.mjs'; import { LayerView } from './LayerView.mjs'; import * as highlighters from '../highlighters/index.mjs'; import * as linkAnchors from '../linkAnchors/index.mjs'; @@ -47,6 +47,8 @@ import * as anchors from '../anchors/index.mjs'; import $ from '../mvc/Dom/index.mjs'; import { GridLayer } from './layers/GridLayer.mjs'; +import { EmbeddingLayersController } from './controllers/EmbeddingLayersController'; +import { CellLayersController } from './controllers/CellLayersController'; const sortingTypes = { NONE: 'sorting-none', @@ -292,7 +294,9 @@ export const Paper = View.extend({ connectionPointNamespace: connectionPoints, - overflow: false + overflow: false, + + useLayersForEmbedding: false }, events: { @@ -410,17 +414,18 @@ export const Paper = View.extend({ order: [], }; - this._embeddingLayers = {}; - - this._graphLayers = model.get('layers'); - this.cloneOptions(); this.render(); this._setDimensions(); this.startListening(); - this._graphLayers = []; - this.updateGraphLayers(this.model.get('layers')); + if (options.useLayersForEmbedding) { + this.embeddingLayersController = new EmbeddingLayersController({ graph: model, paper: this }); + } + + if (options.enableCellLayers) { + this.cellLayersController = new CellLayersController({ graph: model, paper: this }); + } // Hash of all cell views. this._views = {}; @@ -465,11 +470,6 @@ export const Paper = View.extend({ .listenTo(model, 'sort', this.onGraphSort) .listenTo(model, 'batch:stop', this.onGraphBatchStop); - this.listenTo(model, 'embeddingLayer:insert', this.onEmbeddingLayerInsert); - this.listenTo(model, 'embeddingLayer:remove', this.onEmbeddingLayerRemove); - - this.listenTo(model, 'change:layers', this.onLayersChange); - this.on('cell:highlight', this.onCellHighlight) .on('cell:unhighlight', this.onCellUnhighlight) .on('transform', this.update); @@ -528,50 +528,6 @@ export const Paper = View.extend({ } }, - onEmbeddingLayerInsert: function(layer, opt) { - const cellId = layer.get('name'); - const layerView = this.createLayer({ name: cellId, model: layer }); - - const cellView = this._views[cellId]; - if (cellView.isMounted()) { - cellView.el.after(layerView.el); - } - - this._embeddingLayers[cellId] = layerView; - this._layers.viewsMap[cellId] = layerView; - }, - - onEmbeddingLayerRemove: function(layer, opt) { - const cellId = layer.get('name'); - const layerView = this._embeddingLayers[cellId]; - - delete this._embeddingLayers[cellId]; - delete this._layers.viewsMap[cellId]; - - layerView.remove(); - }, - - onLayersChange: function(model, layers) { - this.updateGraphLayers(layers) - }, - - updateGraphLayers: function(layers) { - const removedLayerNames = this._graphLayers.filter(layer => !layers.some(l => l.name === layer.name)).map(layer => layer.name); - removedLayerNames.forEach(layerName => this.removeLayer(layerName)); - - this._graphLayers = this.model.get('layers'); - - this._graphLayers.forEach(layer => { - if (!this.hasLayerView(layer.name)) { - this.renderLayer({ - name: layer.name, - model: layer - }); - } - this.moveLayer(layer.name, LayersNames.FRONT); - }); - }, - cloneOptions: function() { const { options } = this; @@ -1521,6 +1477,9 @@ export const Paper = View.extend({ //clean up all DOM elements/views to prevent memory leaks this.removeLayers(); this.removeViews(); + + this.embeddingLayersController.stopListening(); + this.cellLayersController.stopListening(); }, getComputedSize: function() { diff --git a/packages/joint-core/src/dia/controllers/CellLayersController.js b/packages/joint-core/src/dia/controllers/CellLayersController.js new file mode 100644 index 0000000000..f3af7cdd25 --- /dev/null +++ b/packages/joint-core/src/dia/controllers/CellLayersController.js @@ -0,0 +1,45 @@ +import { Listener } from '../mvc/Listener.mjs'; + +export class CellLayersController extends Listener { + + constructor(context) { + super(context); + + this.graph = context.graph; + this.paper = context.paper; + + this.layers = model.get('layers'); + + this.updateGraphLayers(layers); + + this.startListening(); + } + + startListening() { + const { graph, paper } = this; + + this.listenTo(graph, 'change:layers', (_appContext, graph, layers) => { + this.updateGraphLayers(layers) + }); + } + + updateGraphLayers(layers) { + const { graph, paper } = this; + + const removedLayerNames = this.layers.filter(layer => !layers.some(l => l.name === layer.name)).map(layer => layer.name); + removedLayerNames.forEach(layerName => paper.removeLayer(layerName)); + + this.layers = this.model.get('layers'); + + this.layers.forEach(layer => { + if (!paper.hasLayerView(layer.name)) { + paper.renderLayer({ + name: layer.name, + model: layer + }); + } + paper.moveLayer(layer.name, LayersNames.FRONT); + }); + } + +} diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js new file mode 100644 index 0000000000..63e22d64f3 --- /dev/null +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js @@ -0,0 +1,79 @@ +import { Listener } from '../mvc/Listener.mjs'; +import { Layer } from '../Layer.mjs'; + +export class EmbeddingLayersController extends Listener { + + constructor(context) { + super(context); + + this.graph = context.graph; + this.paper = context.paper; + this.layers = {}; + + this.startListening(); + } + + startListening() { + const { graph } = this; + + this.listenTo(graph, 'remove', (_appContext, cell) => { + if (this.layers[cell.id]) { + const layer = this.layers[cell.id]; + const cells = layer.get('cells').models; + cells.forEach((cell) => { + graph.moveToLayer(cell); + }); + + this.removePaperLayer(layer); + delete this.layers[cell.id]; + } + }); + + this.listenTo(graph, 'change:parent', (_appContext, cell, parentId) => { + const { layersMap, embeddingLayers, defaultLayerName, activeLayerName } = this; + + const currentLayer = cell.layer() || defaultLayerName; + + if (layersMap[currentLayer]) { + layersMap[currentLayer].remove(cell); + } else if (embeddingLayers[currentLayer]) { + embeddingLayers[currentLayer].remove(cell); + } + + if (parentId && !embeddingLayers[parentId]) { + embeddingLayers[parentId] = new Layer({ + name: parentId, + displayName: parentId + }); + this.insertPaperLayer(embeddingLayers[parentId]); + } + + const targetLayer = embeddingLayers[parentId] || layersMap[activeLayerName]; + + targetLayer.add(cell); + }); + } + + insertPaperLayer(layer) { + const { paper } = this; + + const cellId = layer.get('name'); + const layerView = paper.createLayer({ name: cellId, model: layer }); + + const cellView = paper.findViewByModel(cellId); + if (cellView.isMounted()) { + cellView.el.after(layerView.el); + } + + this.layers[cellId] = layerView; + } + + removePaperLayer(layer) { + const cellId = layer.get('name'); + const layerView = this.layers[cellId]; + + delete this.layers[cellId]; + + layerView.remove(); + } +} diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.js b/packages/joint-core/src/dia/controllers/GraphLayersController.js new file mode 100644 index 0000000000..727d75c589 --- /dev/null +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.js @@ -0,0 +1,72 @@ +import { Listener } from '../mvc/Listener.mjs'; + +export class GraphLayersController extends Listener { + + constructor(context) { + super(context); + + this.graph = context.graph; + + this.layers = this.graph.get('layers'); + this.layersMap = {}; + + this.layers.forEach(layer => { + this.layersMap[layer.name] = layer; + }); + + this.activeLayerName = this.defaultLayerName; + + //this.updateGraphLayers(layers); + + this.startListening(); + } + + startListening() { + const { graph } = this; + + this.listenTo(graph, 'add', (_appContext, cell) => { + this.onAdd(cell); + }); + + this.listenTo(graph, 'remove', (_appContext, cell) => { + this.onRemove(cell); + }); + + this.listenTo(graph, 'reset', (_appContext, { models: cells }) => { + const layers = this.get('layers'); + for (let layerName in layers) { + layers[layerName].clear(); + } + + cells.forEach(cell => { + this.onAdd(cell); + }); + }); + } + + onAdd(cell) { + const { activeLayerName, layersMap } = this; + + const layerName = cell.layer() || activeLayerName; + const layer = layersMap[layerName]; + + if (!cell.has('z')) { + cell.set('z', layer.maxZIndex() + 1); + } + + layer.add(cell); + } + + onRemove(cell) { + const { layersMap } = this; + + const layerName = cell.layer(); + + const layer = layersMap[layerName]; + + if (layer) { + layer.remove(cell); + } + } + +} diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index d720cd8c07..6bd6093745 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -195,8 +195,7 @@ export namespace dia { constructor(attributes?: Graph.Attributes, opt?: { cellNamespace?: any, - cellModel?: typeof Cell, - useLayersForEmbedding?: boolean, + cellModel?: typeof Cell }); addCell(cell: Cell.JSON | Cell, opt?: CollectionAddOptions): this; @@ -1448,6 +1447,9 @@ export namespace dia { beforeRender?: Paper.BeforeRenderCallback; afterRender?: Paper.AfterRenderCallback; overflow?: boolean; + + useLayersForEmbedding?: boolean; + enableCellLayers?: boolean; } interface TransformToFitContentOptions { From 62ed85159c365e9961482f51e7f58e02fff5a172 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 18 Mar 2025 16:03:56 +0100 Subject: [PATCH 14/54] wip --- packages/joint-core/src/dia/Graph.mjs | 59 +++------- packages/joint-core/src/dia/Paper.mjs | 50 ++++---- .../dia/controllers/CellLayersController.js | 45 -------- .../controllers/EmbeddingLayersController.js | 57 +++++---- .../dia/controllers/GraphLayersController.js | 109 +++++++++++++++++- packages/joint-core/types/joint.d.ts | 7 +- 6 files changed, 183 insertions(+), 144 deletions(-) delete mode 100644 packages/joint-core/src/dia/controllers/CellLayersController.js diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 2115c45151..31fa3d61a0 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -71,6 +71,9 @@ export const Graph = Model.extend({ opt = opt || {}; + this.useEmbeddingLayers = opt.useEmbeddingLayers || false; + this.enableCellLayers = opt.enableCellLayers || false; + this.defaultLayerName = LayersNames.CELLS; const defaultLayer = new Layer({ @@ -300,23 +303,11 @@ export const Graph = Model.extend({ }, minZIndex: function(layerName) { - const { layersMap, embeddingLayers, defaultLayerName } = this; - - layerName = layerName || defaultLayerName; - - const layer = layersMap[layerName] || embeddingLayers[layerName]; - - return layer.minZIndex(); + this.layersController.minZIndex(layerName); }, maxZIndex: function(layerName) { - const { layersMap, embeddingLayers, defaultLayerName } = this; - - layerName = layerName || defaultLayerName; - - const layer = layersMap[layerName] || embeddingLayers[layerName]; - - return layer.maxZIndex(); + this.layersController.maxZIndex(layerName); }, addCell: function(cell, opt) { @@ -432,45 +423,31 @@ export const Graph = Model.extend({ }, addLayer(layer, opt) { - if (this.layersMap[layer.name]) { - throw new Error(`dia.Graph: Layer with name '${layer.name}' already exists.`); - } - - const layers = this.get('layers'); - - this.layersMap[layer.name] = layer; - this.set('layers', layers.concat([layer])); + this.layersController.addLayer(layer, opt); }, removeLayer(layerName, opt) { - if (layerName === this.defaultLayerName || layerName === this.activeLayerName) { - throw new Error('dia.Graph: default or active layer cannot be removed.'); - } - - if (!this.layersMap[layerName]) { - throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); - } + this.layersController.removeLayer(layerName, opt); + }, - const layers = this.get('layers'); + getDefaultLayer() { + return this.layersController.getDefaultLayer(); + }, - delete this.layersMap[layerName]; - this.set('layers', layers.filter(l => l.name !== layerName)); + getActiveLayer() { + return this.layersController.getActiveLayer(); }, setActiveLayer(layerName) { - if (!this.layersMap[layerName]) { - throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); - } - - this.activeLayerName = layerName; + this.layersController.setActiveLayer(layerName); }, - getActiveLayer() { - return this.layersMap[this.activeLayerName]; + moveToLayer(cell, layerName, opt) { + this.layersController.moveToLayer(cell, layerName, opt); }, - getDefaultLayer() { - return this.layersMap[this.defaultLayerName]; + getLayersMap() { + return this.layersController.getLayersMap(); }, // Get a cell by `id`. diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index f3c722db67..632226f868 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -48,7 +48,6 @@ import * as anchors from '../anchors/index.mjs'; import $ from '../mvc/Dom/index.mjs'; import { GridLayer } from './layers/GridLayer.mjs'; import { EmbeddingLayersController } from './controllers/EmbeddingLayersController'; -import { CellLayersController } from './controllers/CellLayersController'; const sortingTypes = { NONE: 'sorting-none', @@ -295,8 +294,6 @@ export const Paper = View.extend({ connectionPointNamespace: connectionPoints, overflow: false, - - useLayersForEmbedding: false }, events: { @@ -419,14 +416,10 @@ export const Paper = View.extend({ this._setDimensions(); this.startListening(); - if (options.useLayersForEmbedding) { + if (model.useLayersForEmbedding) { this.embeddingLayersController = new EmbeddingLayersController({ graph: model, paper: this }); } - if (options.enableCellLayers) { - this.cellLayersController = new CellLayersController({ graph: model, paper: this }); - } - // Hash of all cell views. this._views = {}; @@ -634,17 +627,21 @@ export const Paper = View.extend({ _unregisterLayer(layerView) { const { _layers: { viewsMap, order }} = this; const layerName = layerView.name; - order.splice(order.indexOf(layerName), 1); + if (order.indexOf(layerName) !== -1) { + order.splice(order.indexOf(layerName), 1); + } delete viewsMap[layerName]; }, - _registerLayer(layerName, layerView, beforeLayerView) { + _registerLayer(layerName, layerView, beforeLayerView, ignoreOrder) { const { _layers: { viewsMap, order }} = this; - if (beforeLayerView) { - const beforeLayerName = beforeLayerView.name; - order.splice(order.indexOf(beforeLayerName), 0, layerName); - } else { - order.push(layerName); + if (!ignoreOrder) { + if (beforeLayerView) { + const beforeLayerName = beforeLayerView.name; + order.splice(order.indexOf(beforeLayerName), 0, layerName); + } else { + order.push(layerName); + } } viewsMap[layerName] = layerView; }, @@ -689,14 +686,18 @@ export const Paper = View.extend({ if (!(layerView instanceof LayerView)) { throw new Error('dia.Paper: The layer view is not an instance of dia.LayerView.'); } - const { insertBefore } = options; - if (!insertBefore) { - this._registerLayer(layerName, layerView, null); - this.layers.appendChild(layerView.el); + const { insertBefore, doNotAppend } = options; + if (doNotAppend) { + this._registerLayer(layerName, layerView, null, true); } else { - const beforeLayerView = this._requireLayerView(insertBefore); - this._registerLayer(layerName, layerView, beforeLayerView); - this.layers.insertBefore(layerView.el, beforeLayerView.el); + if (!insertBefore) { + this._registerLayer(layerName, layerView, null); + this.layers.appendChild(layerView.el); + } else { + const beforeLayerView = this._requireLayerView(insertBefore); + this._registerLayer(layerName, layerView, beforeLayerView); + this.layers.insertBefore(layerView.el, beforeLayerView.el); + } } }, @@ -1927,10 +1928,7 @@ export const Paper = View.extend({ break; } - if (this._embeddingLayers[model.id]) { - const layerView = this._embeddingLayers[model.id]; - el.after(layerView.el); - } + this.trigger('cell:inserted', view, isInitialInsert); view.onMount(isInitialInsert); }, diff --git a/packages/joint-core/src/dia/controllers/CellLayersController.js b/packages/joint-core/src/dia/controllers/CellLayersController.js deleted file mode 100644 index f3af7cdd25..0000000000 --- a/packages/joint-core/src/dia/controllers/CellLayersController.js +++ /dev/null @@ -1,45 +0,0 @@ -import { Listener } from '../mvc/Listener.mjs'; - -export class CellLayersController extends Listener { - - constructor(context) { - super(context); - - this.graph = context.graph; - this.paper = context.paper; - - this.layers = model.get('layers'); - - this.updateGraphLayers(layers); - - this.startListening(); - } - - startListening() { - const { graph, paper } = this; - - this.listenTo(graph, 'change:layers', (_appContext, graph, layers) => { - this.updateGraphLayers(layers) - }); - } - - updateGraphLayers(layers) { - const { graph, paper } = this; - - const removedLayerNames = this.layers.filter(layer => !layers.some(l => l.name === layer.name)).map(layer => layer.name); - removedLayerNames.forEach(layerName => paper.removeLayer(layerName)); - - this.layers = this.model.get('layers'); - - this.layers.forEach(layer => { - if (!paper.hasLayerView(layer.name)) { - paper.renderLayer({ - name: layer.name, - model: layer - }); - } - paper.moveLayer(layer.name, LayersNames.FRONT); - }); - } - -} diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js index 63e22d64f3..001ece2847 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js @@ -8,72 +8,81 @@ export class EmbeddingLayersController extends Listener { this.graph = context.graph; this.paper = context.paper; - this.layers = {}; this.startListening(); } startListening() { - const { graph } = this; + const { graph, paper } = this; this.listenTo(graph, 'remove', (_appContext, cell) => { - if (this.layers[cell.id]) { - const layer = this.layers[cell.id]; + const layersMap = graph.getLayersMap(); + + if (layersMap[cell.id]) { + const layer = layersMap[cell.id]; const cells = layer.get('cells').models; cells.forEach((cell) => { graph.moveToLayer(cell); }); + graph.removeLayer(layer); this.removePaperLayer(layer); - delete this.layers[cell.id]; } }); this.listenTo(graph, 'change:parent', (_appContext, cell, parentId) => { - const { layersMap, embeddingLayers, defaultLayerName, activeLayerName } = this; + const layersMap = graph.getLayersMap(); + const activeLayer = graph.getActiveLayer(); - const currentLayer = cell.layer() || defaultLayerName; + const currentLayer = cell.layer(); if (layersMap[currentLayer]) { layersMap[currentLayer].remove(cell); - } else if (embeddingLayers[currentLayer]) { - embeddingLayers[currentLayer].remove(cell); } - if (parentId && !embeddingLayers[parentId]) { - embeddingLayers[parentId] = new Layer({ - name: parentId, - displayName: parentId + let layer; + if (parentId && !layersMap[parentId]) { + layer = new Layer({ + name: parentId }); - this.insertPaperLayer(embeddingLayers[parentId]); + graph.addLayer(layer); + this.insertEmbeddingLayer(layer); } - const targetLayer = embeddingLayers[parentId] || layersMap[activeLayerName]; + const targetLayer = layer || activeLayer; targetLayer.add(cell); }); + + this.listenTo(paper, 'cell:inserted', (_appContext, cellView) => { + const cellId = cellView.model.id; + if (paper.hasLayerView(cellId)) { + const layerView = paper.getLayerView(cellView); + el.after(layerView.el); + } + }); } - insertPaperLayer(layer) { + insertEmbeddingLayer(layer) { const { paper } = this; const cellId = layer.get('name'); const layerView = paper.createLayer({ name: cellId, model: layer }); + paper.addLayer(cellId, layerView, { doNotAppend: true }); const cellView = paper.findViewByModel(cellId); if (cellView.isMounted()) { cellView.el.after(layerView.el); } - - this.layers[cellId] = layerView; } - removePaperLayer(layer) { - const cellId = layer.get('name'); - const layerView = this.layers[cellId]; - - delete this.layers[cellId]; + removeEmbeddingLayer(layer) { + const { paper } = this; - layerView.remove(); + const cellId = layer.get('name'); + const layerView = paper.hasLayerView(cellId); + if (layerView) { + paper.removeLayer(cellId); + } } } diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.js b/packages/joint-core/src/dia/controllers/GraphLayersController.js index 727d75c589..036dfb191a 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.js +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.js @@ -14,10 +14,9 @@ export class GraphLayersController extends Listener { this.layersMap[layer.name] = layer; }); + this.defaultLayerName = this.graph.defaultLayerName; this.activeLayerName = this.defaultLayerName; - //this.updateGraphLayers(layers); - this.startListening(); } @@ -33,8 +32,9 @@ export class GraphLayersController extends Listener { }); this.listenTo(graph, 'reset', (_appContext, { models: cells }) => { - const layers = this.get('layers'); - for (let layerName in layers) { + const { layersMap } = this; + + for (let layerName in layersMap) { layers[layerName].clear(); } @@ -54,6 +54,8 @@ export class GraphLayersController extends Listener { cell.set('z', layer.maxZIndex() + 1); } + // mandatory add to the layer + // so every cell now will have a layer specified layer.add(cell); } @@ -69,4 +71,103 @@ export class GraphLayersController extends Listener { } } + setActiveLayer(layerName) { + const { layersMap } = this; + + if (!layersMap[layerName]) { + throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); + } + + this.activeLayerName = layerName; + } + + getActiveLayer() { + return this.layersMap[this.activeLayerName]; + } + + getDefaultLayer() { + return this.layersMap[this.defaultLayerName]; + } + + addLayer(layer, opt) { + const { layersMap } = this; + + if (layersMap[layer.name]) { + throw new Error(`dia.Graph: Layer with name '${layer.name}' already exists.`); + } + + this.layers = this.layers.concat([layer]); + + layersMap[layer.name] = layer; + this.graph.set('layers', this.layers); + } + + removeLayer(layerName, opt) { + const { layersMap, defaultLayerName, activeLayerName } = this; + + if (layerName === defaultLayerName || layerName === activeLayerName) { + throw new Error('dia.Graph: default or active layer cannot be removed.'); + } + + if (!layersMap[layerName]) { + throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); + } + + this.layers = this.layers.filter(l => l.name !== layerName); + + delete this.layersMap[layerName]; + this.set('layers', this.layers); + } + + moveToLayer(cell, layerName, opt) { + const { layersMap, activeLayerName } = this; + + layerName = layerName || activeLayerName; + + if (!layersMap[layerName]) { + throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); + } + + const layer = layersMap[layerName]; + + if (!cell.has('z')) { + cell.set('z', layer.maxZIndex() + 1); + } + + const currentLayer = cell.layer(); + + if (currentLayer === layerName) { + return; + } + + if (currentLayer) { + layersMap[currentLayer].remove(cell); + } + + layer.add(cell); + } + + minZIndex(layerName) { + const { layersMap, defaultLayerName } = this; + + layerName = layerName || defaultLayerName; + + const layer = layersMap[layerName]; + + return layer.minZIndex(); + } + + maxZIndex(layerName) { + const { layersMap, defaultLayerName } = this; + + layerName = layerName || defaultLayerName; + + const layer = layersMap[layerName]; + + return layer.maxZIndex(); + } + + getLayersMap() { + return this.layersMap; + } } diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 6bd6093745..aee7107576 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -195,7 +195,9 @@ export namespace dia { constructor(attributes?: Graph.Attributes, opt?: { cellNamespace?: any, - cellModel?: typeof Cell + cellModel?: typeof Cell, + // layers + useLayersForEmbedding?: boolean; }); addCell(cell: Cell.JSON | Cell, opt?: CollectionAddOptions): this; @@ -1447,9 +1449,6 @@ export namespace dia { beforeRender?: Paper.BeforeRenderCallback; afterRender?: Paper.AfterRenderCallback; overflow?: boolean; - - useLayersForEmbedding?: boolean; - enableCellLayers?: boolean; } interface TransformToFitContentOptions { From 903e922bf710f2b2cc66b12c0c8bc734e3ee1549 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 18 Mar 2025 17:11:13 +0100 Subject: [PATCH 15/54] wip --- packages/joint-core/src/dia/Graph.mjs | 4 ++-- packages/joint-core/src/dia/Layer.mjs | 4 ++++ packages/joint-core/src/dia/LayerView.mjs | 4 ++++ packages/joint-core/src/dia/Paper.mjs | 5 +++-- ...gLayersController.js => EmbeddingLayersController.mjs} | 8 ++++---- ...GraphLayersController.js => GraphLayersController.mjs} | 4 ++-- 6 files changed, 19 insertions(+), 10 deletions(-) rename packages/joint-core/src/dia/controllers/{EmbeddingLayersController.js => EmbeddingLayersController.mjs} (91%) rename packages/joint-core/src/dia/controllers/{GraphLayersController.js => GraphLayersController.mjs} (97%) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 31fa3d61a0..8b57e05f45 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -6,7 +6,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 { GraphLayersController } from './controllers/GraphLayersController'; +import { GraphLayersController } from './controllers/GraphLayersController.mjs'; const GraphCells = Collection.extend({ @@ -71,7 +71,7 @@ export const Graph = Model.extend({ opt = opt || {}; - this.useEmbeddingLayers = opt.useEmbeddingLayers || false; + this.useLayersForEmbedding = opt.useLayersForEmbedding || false; this.enableCellLayers = opt.enableCellLayers || false; this.defaultLayerName = LayersNames.CELLS; diff --git a/packages/joint-core/src/dia/Layer.mjs b/packages/joint-core/src/dia/Layer.mjs index ffdcfed12b..073e0f8636 100644 --- a/packages/joint-core/src/dia/Layer.mjs +++ b/packages/joint-core/src/dia/Layer.mjs @@ -38,6 +38,10 @@ export class Layer extends Model { cells.on('change:z', () => { cells.sort(); }); + + // Make all the events fired in the `cells` collection available. + // to the outside world. + cells.on('all', this.trigger, this); } add(cell) { diff --git a/packages/joint-core/src/dia/LayerView.mjs b/packages/joint-core/src/dia/LayerView.mjs index c8b90c6204..858880d485 100644 --- a/packages/joint-core/src/dia/LayerView.mjs +++ b/packages/joint-core/src/dia/LayerView.mjs @@ -11,6 +11,10 @@ export class LayerView extends View { this.pivotNodes = {}; } + init(...args) { + super.init(...args); + } + className() { const { name } = this.options; if (!name) return null; diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 632226f868..2e6cf00ce0 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -47,7 +47,7 @@ import * as anchors from '../anchors/index.mjs'; import $ from '../mvc/Dom/index.mjs'; import { GridLayer } from './layers/GridLayer.mjs'; -import { EmbeddingLayersController } from './controllers/EmbeddingLayersController'; +import { EmbeddingLayersController } from './controllers/EmbeddingLayersController.mjs'; const sortingTypes = { NONE: 'sorting-none', @@ -755,9 +755,10 @@ export const Paper = View.extend({ }, createLayer(attributes) { + attributes.paper = this; switch (attributes.name) { case LayersNames.GRID: - return new GridLayer({ ...attributes, paper: this, patterns: this.constructor.gridPatterns }); + return new GridLayer({ ...attributes, patterns: this.constructor.gridPatterns }); default: return new LayerView(attributes); } diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs similarity index 91% rename from packages/joint-core/src/dia/controllers/EmbeddingLayersController.js rename to packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 001ece2847..85bb85b35c 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.js +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -1,4 +1,4 @@ -import { Listener } from '../mvc/Listener.mjs'; +import { Listener } from '../../mvc/Listener.mjs'; import { Layer } from '../Layer.mjs'; export class EmbeddingLayersController extends Listener { @@ -49,7 +49,7 @@ export class EmbeddingLayersController extends Listener { this.insertEmbeddingLayer(layer); } - const targetLayer = layer || activeLayer; + const targetLayer = layersMap[parentId] || activeLayer; targetLayer.add(cell); }); @@ -57,8 +57,8 @@ export class EmbeddingLayersController extends Listener { this.listenTo(paper, 'cell:inserted', (_appContext, cellView) => { const cellId = cellView.model.id; if (paper.hasLayerView(cellId)) { - const layerView = paper.getLayerView(cellView); - el.after(layerView.el); + const layerView = paper.getLayerView(cellId); + cellView.el.after(layerView.el); } }); } diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.js b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs similarity index 97% rename from packages/joint-core/src/dia/controllers/GraphLayersController.js rename to packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 036dfb191a..90dff9df48 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.js +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -1,4 +1,4 @@ -import { Listener } from '../mvc/Listener.mjs'; +import { Listener } from '../../mvc/Listener.mjs'; export class GraphLayersController extends Listener { @@ -35,7 +35,7 @@ export class GraphLayersController extends Listener { const { layersMap } = this; for (let layerName in layersMap) { - layers[layerName].clear(); + layersMap[layerName].clear(); } cells.forEach(cell => { From b491ea88cdae3b45bc09e26aaf8ea52c54182b88 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 18 Mar 2025 20:00:46 +0100 Subject: [PATCH 16/54] fix --- packages/joint-core/src/dia/Graph.mjs | 8 ++---- .../controllers/EmbeddingLayersController.mjs | 3 +- .../dia/controllers/GraphLayersController.mjs | 28 ------------------- 3 files changed, 4 insertions(+), 35 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 8b57e05f45..bfa42bc769 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -303,11 +303,11 @@ export const Graph = Model.extend({ }, minZIndex: function(layerName) { - this.layersController.minZIndex(layerName); + return this.layersController.minZIndex(layerName); }, maxZIndex: function(layerName) { - this.layersController.maxZIndex(layerName); + return this.layersController.maxZIndex(layerName); }, addCell: function(cell, opt) { @@ -442,10 +442,6 @@ export const Graph = Model.extend({ this.layersController.setActiveLayer(layerName); }, - moveToLayer(cell, layerName, opt) { - this.layersController.moveToLayer(cell, layerName, opt); - }, - getLayersMap() { return this.layersController.getLayersMap(); }, diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 85bb85b35c..b79d77b340 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -17,12 +17,13 @@ export class EmbeddingLayersController extends Listener { this.listenTo(graph, 'remove', (_appContext, cell) => { const layersMap = graph.getLayersMap(); + const activeLayer = graph.getActiveLayer(); if (layersMap[cell.id]) { const layer = layersMap[cell.id]; const cells = layer.get('cells').models; cells.forEach((cell) => { - graph.moveToLayer(cell); + activeLayer.add(cell); }); graph.removeLayer(layer); diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 90dff9df48..d3e3bcd1b0 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -119,34 +119,6 @@ export class GraphLayersController extends Listener { this.set('layers', this.layers); } - moveToLayer(cell, layerName, opt) { - const { layersMap, activeLayerName } = this; - - layerName = layerName || activeLayerName; - - if (!layersMap[layerName]) { - throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); - } - - const layer = layersMap[layerName]; - - if (!cell.has('z')) { - cell.set('z', layer.maxZIndex() + 1); - } - - const currentLayer = cell.layer(); - - if (currentLayer === layerName) { - return; - } - - if (currentLayer) { - layersMap[currentLayer].remove(cell); - } - - layer.add(cell); - } - minZIndex(layerName) { const { layersMap, defaultLayerName } = this; From 1533570a5a5be3e8d24cf388ddbca39ffcb7366e Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 19 Mar 2025 10:53:44 +0100 Subject: [PATCH 17/54] up --- packages/joint-core/src/dia/Cell.mjs | 12 ++++++++---- packages/joint-core/src/dia/Graph.mjs | 4 ++++ .../src/dia/controllers/GraphLayersController.mjs | 4 ++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index 33b5e24ce7..077658fa1e 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -261,10 +261,12 @@ export const Cell = Model.extend({ const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z()); - const maxZ = graph.maxZIndex(this.layer()); + const layerName = this.layer(); + + const maxZ = graph.maxZIndex(layerName); let z = maxZ - cells.length + 1; - const collection = graph.get('cells'); + const collection = graph.getLayerCells(layerName); let shouldUpdate = (collection.toArray().indexOf(sortedCells[0]) !== (collection.length - cells.length)); if (!shouldUpdate) { @@ -304,9 +306,11 @@ export const Cell = Model.extend({ const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z()); - let z = graph.minZIndex(this.layer()); + const layerName = this.layer(); + + let z = graph.minZIndex(layerName); - var collection = graph.get('cells'); + var collection = graph.getLayerCells(layerName); let shouldUpdate = (collection.toArray().indexOf(sortedCells[0]) !== 0); if (!shouldUpdate) { diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index bfa42bc769..877d4b381d 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -446,6 +446,10 @@ export const Graph = Model.extend({ return this.layersController.getLayersMap(); }, + getLayerCells(layerName) { + return this.layersController.getLayerCells(layerName); + }, + // Get a cell by `id`. getCell: function(id) { diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index d3e3bcd1b0..6e2f03ae1f 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -142,4 +142,8 @@ export class GraphLayersController extends Listener { getLayersMap() { return this.layersMap; } + + getLayerCells(layerName) { + return this.layersMap[layerName].get('cells'); + } } From 07520763d588665e9775622da084cc12b28d60fc Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 16 May 2025 15:24:35 +0200 Subject: [PATCH 18/54] up --- packages/joint-core/src/dia/LayerView.mjs | 2 -- packages/joint-core/src/dia/Paper.mjs | 9 +++++++-- packages/joint-core/src/dia/layers/GridLayer.mjs | 3 --- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/joint-core/src/dia/LayerView.mjs b/packages/joint-core/src/dia/LayerView.mjs index 858880d485..b68cb2c553 100644 --- a/packages/joint-core/src/dia/LayerView.mjs +++ b/packages/joint-core/src/dia/LayerView.mjs @@ -3,8 +3,6 @@ import { addClassNamePrefix } from '../util/util.mjs'; export class LayerView extends View { - defaultTheme = null; - preinitialize() { this.tagName = 'g'; this.svgElement = true; diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 2e6cf00ce0..d24d01084a 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -1480,8 +1480,13 @@ export const Paper = View.extend({ this.removeLayers(); this.removeViews(); - this.embeddingLayersController.stopListening(); - this.cellLayersController.stopListening(); + if (this.embeddingLayersController) { + this.embeddingLayersController.stopListening(); + } + + if (this.cellLayersController) { + this.cellLayersController.stopListening(); + } }, getComputedSize: function() { diff --git a/packages/joint-core/src/dia/layers/GridLayer.mjs b/packages/joint-core/src/dia/layers/GridLayer.mjs index eb4946b9c1..d232b2d404 100644 --- a/packages/joint-core/src/dia/layers/GridLayer.mjs +++ b/packages/joint-core/src/dia/layers/GridLayer.mjs @@ -11,9 +11,6 @@ import V from '../../V/index.mjs'; export class GridLayer extends LayerView { - _gridCache = null; - _gridSettings = null; - preinitialize() { super.preinitialize(); From 163c22161463be19e1635508b0d380edcb8ace44 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 16 May 2025 15:42:10 +0200 Subject: [PATCH 19/54] wip --- packages/joint-core/src/dia/Paper.mjs | 10 +++++++++- .../joint-core/src/dia/layers/GridLayer.mjs | 2 +- .../dia/{PaperLayer.js => LayerView.js} | 6 +++--- packages/joint-core/test/jointjs/dia/Paper.js | 18 +++++++++--------- packages/joint-core/test/jointjs/index.html | 2 +- 5 files changed, 23 insertions(+), 15 deletions(-) rename packages/joint-core/test/jointjs/dia/{PaperLayer.js => LayerView.js} (76%) diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index d24d01084a..508f6eaab4 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -646,8 +646,16 @@ export const Paper = View.extend({ viewsMap[layerName] = layerView; }, - _getLayerView(layerName) { + _getLayerView(layer) { const { _layers: { viewsMap }} = this; + + let layerName; + if (layer instanceof LayerView) { + layerName = layer.name; + } else { + layerName = layer; + } + if (layerName in viewsMap) return viewsMap[layerName]; return null; }, diff --git a/packages/joint-core/src/dia/layers/GridLayer.mjs b/packages/joint-core/src/dia/layers/GridLayer.mjs index d232b2d404..0e6c53df25 100644 --- a/packages/joint-core/src/dia/layers/GridLayer.mjs +++ b/packages/joint-core/src/dia/layers/GridLayer.mjs @@ -154,7 +154,7 @@ export class GridLayer extends LayerView { const options = opt || { args: [{}] }; const isArray = Array.isArray(options); - const name = options.name; + let name = options.name; if (!isArray && !name && !options.markup) { name = 'dot'; diff --git a/packages/joint-core/test/jointjs/dia/PaperLayer.js b/packages/joint-core/test/jointjs/dia/LayerView.js similarity index 76% rename from packages/joint-core/test/jointjs/dia/PaperLayer.js rename to packages/joint-core/test/jointjs/dia/LayerView.js index f39ac573e2..865167e842 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' }); + const layer = new joint.dia.LayerView({ name: '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..bad0dd04f8 100644 --- a/packages/joint-core/test/jointjs/dia/Paper.js +++ b/packages/joint-core/test/jointjs/dia/Paper.js @@ -2148,8 +2148,8 @@ QUnit.module('joint.dia.Paper', function(hooks) { function() { paper.addLayer('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 is not an instance of dia.LayerView./, + 'Layer view must be an instance of joint.dia.LayerView.' ); }); @@ -2164,7 +2164,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { }); QUnit.test('adds a new layer at the end of the layers list', function(assert) { - const testLayer = new joint.dia.PaperLayer(); + const testLayer = new joint.dia.LayerView(); assert.equal(paper.getLayerNames().indexOf('test1'), -1); assert.notOk(paper.hasLayer('test1')); paper.addLayer('test1', testLayer); @@ -2173,7 +2173,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { }); QUnit.test('adds a new layer before the specified layer', function(assert) { - const testLayer = new joint.dia.PaperLayer(); + const testLayer = new joint.dia.LayerView(); assert.equal(paper.getLayerNames().indexOf('test2'), -1); assert.notOk(paper.hasLayer('test1')); paper.addLayer('test2', testLayer, { insertBefore: 'cells' }); @@ -2225,7 +2225,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { }); QUnit.test('removes the layer if passed as an object', function(assert) { - const testLayer = new joint.dia.PaperLayer(); + const testLayer = new joint.dia.LayerView(); paper.addLayer('test', testLayer); assert.ok(paper.hasLayer('test')); paper.removeLayer(testLayer); @@ -2233,7 +2233,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { }); 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(); + const testLayer = new joint.dia.LayerView(); assert.throws( function() { paper.removeLayer(testLayer); @@ -2321,10 +2321,10 @@ 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.equal(r1.get('layer'), 'cells'); assert.ok(paper.getLayerNode('cells').contains(r1.findView(paper).el)); - const test1Layer = new joint.dia.PaperLayer(); + const test1Layer = new joint.dia.LayerView(); paper.addLayer('test1', test1Layer); @@ -2348,7 +2348,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { graph.addCell(r1, { async: false }); assert.ok(paper.getLayerNode('cells').contains(r1.findView(paper).el)); - const test1Layer = new joint.dia.PaperLayer(); + const test1Layer = new joint.dia.LayerView(); paper.addLayer('test1', test1Layer); r1.set('layer', 'test1', { async: false }); diff --git a/packages/joint-core/test/jointjs/index.html b/packages/joint-core/test/jointjs/index.html index 6cbc38a894..0da4fdbc83 100644 --- a/packages/joint-core/test/jointjs/index.html +++ b/packages/joint-core/test/jointjs/index.html @@ -23,7 +23,7 @@ - + From c66280b3497982777927247685bd820cf6cd468b Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Thu, 29 May 2025 08:22:55 +0200 Subject: [PATCH 20/54] wip --- packages/joint-core/src/dia/Cell.mjs | 20 +++++++++---- packages/joint-core/src/dia/Graph.mjs | 13 ++------- packages/joint-core/src/dia/Paper.mjs | 15 ++++++---- .../dia/controllers/GraphLayersController.mjs | 29 +++++-------------- packages/joint-core/test/jointjs/dia/Paper.js | 28 +++++++++++------- packages/joint-core/types/joint.d.ts | 9 ++---- 6 files changed, 52 insertions(+), 62 deletions(-) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index 077658fa1e..c56256f5ad 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -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 }); @@ -292,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 }); @@ -310,7 +310,7 @@ export const Cell = Model.extend({ let z = graph.minZIndex(layerName); - var collection = graph.getLayerCells(layerName); + const collection = graph.getLayerCells(layerName); let shouldUpdate = (collection.toArray().indexOf(sortedCells[0]) !== 0); if (!shouldUpdate) { @@ -951,13 +951,21 @@ export const Cell = Model.extend({ setLayer(layerName) { if (layerName) { this.set('layer', layerName); - } else { - this.unset('layer'); } }, + unsetLayer() { + this.unset('layer'); + }, + layer() { - return this.get('layer') || null; + let layer = this.get('layer') || null; + if (layer == null && this.graph) { + // If the cell is part of a graph, use the graph's default layer. + layer = this.graph.getDefaultLayer(); + } + + return layer; } }, { diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 877d4b381d..a7d673d941 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -71,7 +71,6 @@ export const Graph = Model.extend({ opt = opt || {}; - this.useLayersForEmbedding = opt.useLayersForEmbedding || false; this.enableCellLayers = opt.enableCellLayers || false; this.defaultLayerName = LayersNames.CELLS; @@ -426,22 +425,14 @@ export const Graph = Model.extend({ this.layersController.addLayer(layer, opt); }, - removeLayer(layerName, opt) { - this.layersController.removeLayer(layerName, opt); + removeLayer(layer, opt) { + this.layersController.removeLayer(layer.name, opt); }, getDefaultLayer() { return this.layersController.getDefaultLayer(); }, - getActiveLayer() { - return this.layersController.getActiveLayer(); - }, - - setActiveLayer(layerName) { - this.layersController.setActiveLayer(layerName); - }, - getLayersMap() { return this.layersController.getLayersMap(); }, diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 508f6eaab4..1c95218e83 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -416,7 +416,7 @@ export const Paper = View.extend({ this._setDimensions(); this.startListening(); - if (model.useLayersForEmbedding) { + if (options.useLayersForEmbedding) { this.embeddingLayersController = new EmbeddingLayersController({ graph: model, paper: this }); } @@ -656,7 +656,9 @@ export const Paper = View.extend({ layerName = layer; } - if (layerName in viewsMap) return viewsMap[layerName]; + if (layerName in viewsMap) { + return viewsMap[layerName]; + } return null; }, @@ -684,7 +686,9 @@ export const Paper = View.extend({ this._removeLayer(layerView); }, - addLayer(layerName, layerView, options = {}) { + addLayer(layerView, options = {}) { + const layerName = layer.name; + if (!layerName || typeof layerName !== 'string') { throw new Error('dia.Paper: The layer name must be provided.'); } @@ -712,9 +716,8 @@ export const Paper = View.extend({ moveLayer(layer, insertBefore) { const layerView = this._requireLayerView(layer); if (layerView === this._getLayerView(insertBefore)) return; - const layerName = layerView.name; this._unregisterLayer(layerView); - this.addLayer(layerName, layerView, { insertBefore }); + this.addLayer(layerView, { insertBefore }); }, getLayerNames() { @@ -774,7 +777,7 @@ export const Paper = View.extend({ renderLayer: function(attributes) { const layerView = this.createLayer(attributes); - this.addLayer(attributes.name, layerView); + this.addLayer(layerView); return layerView; }, diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 6e2f03ae1f..49fe4a8b13 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -15,7 +15,6 @@ export class GraphLayersController extends Listener { }); this.defaultLayerName = this.graph.defaultLayerName; - this.activeLayerName = this.defaultLayerName; this.startListening(); } @@ -45,9 +44,9 @@ export class GraphLayersController extends Listener { } onAdd(cell) { - const { activeLayerName, layersMap } = this; + const { layersMap } = this; - const layerName = cell.layer() || activeLayerName; + const layerName = cell.layer(); const layer = layersMap[layerName]; if (!cell.has('z')) { @@ -71,25 +70,11 @@ export class GraphLayersController extends Listener { } } - setActiveLayer(layerName) { - const { layersMap } = this; - - if (!layersMap[layerName]) { - throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); - } - - this.activeLayerName = layerName; - } - - getActiveLayer() { - return this.layersMap[this.activeLayerName]; - } - getDefaultLayer() { return this.layersMap[this.defaultLayerName]; } - addLayer(layer, opt) { + addLayer(layer, _opt) { const { layersMap } = this; if (layersMap[layer.name]) { @@ -102,11 +87,11 @@ export class GraphLayersController extends Listener { this.graph.set('layers', this.layers); } - removeLayer(layerName, opt) { - const { layersMap, defaultLayerName, activeLayerName } = this; + removeLayer(layerName, _opt) { + const { layersMap, defaultLayerName } = this; - if (layerName === defaultLayerName || layerName === activeLayerName) { - throw new Error('dia.Graph: default or active layer cannot be removed.'); + if (layerName === defaultLayerName) { + throw new Error('dia.Graph: default layer cannot be removed.'); } if (!layersMap[layerName]) { diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js index bad0dd04f8..d2f125a250 100644 --- a/packages/joint-core/test/jointjs/dia/Paper.js +++ b/packages/joint-core/test/jointjs/dia/Paper.js @@ -2146,7 +2146,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { ); assert.throws( function() { - paper.addLayer('test'); + paper.addLayer({ name: 'test' }); }, /dia.Paper: The layer view is not an instance of dia.LayerView./, 'Layer view must be an instance of joint.dia.LayerView.' @@ -2156,7 +2156,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { QUnit.test('throws error when layer with the same name already exists', function(assert) { assert.throws( function() { - paper.addLayer(joint.dia.Paper.Layers.BACK); + paper.addLayer(new LayerView({ name: joint.dia.Paper.Layers.BACK })); }, /dia.Paper: The layer "back" already exists./, 'Layer with the name "back" already exists.' @@ -2164,19 +2164,23 @@ QUnit.module('joint.dia.Paper', function(hooks) { }); QUnit.test('adds a new layer at the end of the layers list', function(assert) { - const testLayer = new joint.dia.LayerView(); + const testLayer = new joint.dia.LayerView({ + name: 'test1' + }); assert.equal(paper.getLayerNames().indexOf('test1'), -1); assert.notOk(paper.hasLayer('test1')); - paper.addLayer('test1', testLayer); + paper.addLayer(testLayer); assert.ok(paper.hasLayer('test1')); assert.equal(paper.getLayers().at(-1), testLayer); }); QUnit.test('adds a new layer before the specified layer', function(assert) { - const testLayer = new joint.dia.LayerView(); + const testLayer = new joint.dia.LayerView({ + name: 'test2' + }); assert.equal(paper.getLayerNames().indexOf('test2'), -1); assert.notOk(paper.hasLayer('test1')); - paper.addLayer('test2', testLayer, { insertBefore: 'cells' }); + paper.addLayer(testLayer, { insertBefore: 'cells' }); assert.ok(paper.hasLayer('test2')); const layerNames = paper.getLayerNames(); assert.equal(layerNames.indexOf('test2'), layerNames.indexOf('cells') - 1); @@ -2225,8 +2229,10 @@ QUnit.module('joint.dia.Paper', function(hooks) { }); QUnit.test('removes the layer if passed as an object', function(assert) { - const testLayer = new joint.dia.LayerView(); - paper.addLayer('test', testLayer); + const testLayer = new joint.dia.LayerView({ + name: 'test' + }); + paper.addLayer(testLayer); assert.ok(paper.hasLayer('test')); paper.removeLayer(testLayer); assert.notOk(paper.hasLayer('test')); @@ -2324,8 +2330,10 @@ QUnit.module('joint.dia.Paper', function(hooks) { assert.equal(r1.get('layer'), 'cells'); assert.ok(paper.getLayerNode('cells').contains(r1.findView(paper).el)); - const test1Layer = new joint.dia.LayerView(); - paper.addLayer('test1', test1Layer); + const test1Layer = new joint.dia.LayerView({ + name: 'test1' + }); + paper.addLayer(test1Layer); const r2 = new joint.shapes.standard.Rectangle({ layer: 'test1' }); diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index aee7107576..8c6d09be47 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -196,8 +196,6 @@ export namespace dia { constructor(attributes?: Graph.Attributes, opt?: { cellNamespace?: any, cellModel?: typeof Cell, - // layers - useLayersForEmbedding?: boolean; }); addCell(cell: Cell.JSON | Cell, opt?: CollectionAddOptions): this; @@ -209,11 +207,7 @@ export namespace dia { addLayer(layer: Layer): void; - removeLayer(layerName: string): void; - - getActiveLayer(): Layer; - - setActiveLayer(layerName: string): Layer; + removeLayer(layer: Layer): void; getDefaultLayer(): Layer; @@ -1417,6 +1411,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; From c2dc6ae213bce4060ecc7dc8b60f0e02ad2b5293 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Thu, 5 Jun 2025 11:11:01 +0200 Subject: [PATCH 21/54] wip --- packages/joint-core/src/dia/Cell.mjs | 2 +- packages/joint-core/src/dia/Graph.mjs | 9 +- .../src/dia/{Layer.mjs => GraphLayer.mjs} | 11 +- .../joint-core/src/dia/HighlighterView.mjs | 2 +- packages/joint-core/src/dia/LayerView.mjs | 6 +- packages/joint-core/src/dia/LinkView.mjs | 2 +- packages/joint-core/src/dia/Paper.mjs | 116 ++++++++++-------- packages/joint-core/src/dia/ToolsView.mjs | 4 +- .../controllers/EmbeddingLayersController.mjs | 22 ++-- .../dia/controllers/GraphLayersController.mjs | 9 ++ packages/joint-core/src/dia/index.mjs | 2 +- .../src/dia/layers/GraphLayerView.mjs | 79 ++++++++++++ packages/joint-core/test/jointjs/dia/Paper.js | 54 +++----- packages/joint-core/test/jointjs/paper.js | 4 +- packages/joint-core/types/joint.d.ts | 20 ++- 15 files changed, 203 insertions(+), 139 deletions(-) rename packages/joint-core/src/dia/{Layer.mjs => GraphLayer.mjs} (87%) create mode 100644 packages/joint-core/src/dia/layers/GraphLayerView.mjs diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index c56256f5ad..7f87eb3287 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -962,7 +962,7 @@ export const Cell = Model.extend({ let layer = this.get('layer') || null; if (layer == null && this.graph) { // If the cell is part of a graph, use the graph's default layer. - layer = this.graph.getDefaultLayer(); + layer = this.graph.getDefaultLayer().name; } return layer; diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index a7d673d941..d34e2e8d77 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -1,7 +1,7 @@ import * as util from '../util/index.mjs'; import * as g from '../g/index.mjs'; -import { LayersNames, Layer } from './Layer.mjs'; +import { GraphLayer } from './GraphLayer.mjs'; import { Model } from '../mvc/Model.mjs'; import { Collection } from '../mvc/Collection.mjs'; import { wrappers, wrapWith } from '../util/wrappers.mjs'; @@ -73,10 +73,11 @@ export const Graph = Model.extend({ this.enableCellLayers = opt.enableCellLayers || false; - this.defaultLayerName = LayersNames.CELLS; + this.defaultLayerName = 'cells'; - const defaultLayer = new Layer({ - name: this.defaultLayerName + const defaultLayer = new GraphLayer({ + name: this.defaultLayerName, + graph: this, }); this.set('layers', [ diff --git a/packages/joint-core/src/dia/Layer.mjs b/packages/joint-core/src/dia/GraphLayer.mjs similarity index 87% rename from packages/joint-core/src/dia/Layer.mjs rename to packages/joint-core/src/dia/GraphLayer.mjs index 073e0f8636..6806966a91 100644 --- a/packages/joint-core/src/dia/Layer.mjs +++ b/packages/joint-core/src/dia/GraphLayer.mjs @@ -1,14 +1,5 @@ import { Model, Collection } from '../mvc/index.mjs'; -export const LayersNames = { - GRID: 'grid', - CELLS: 'cells', - BACK: 'back', - FRONT: 'front', - TOOLS: 'tools', - LABELS: 'labels' -}; - class LayerCells extends Collection { // `comparator` makes it easy to sort cells based on their `z` index. @@ -17,7 +8,7 @@ class LayerCells extends Collection { } } -export class Layer extends Model { +export class GraphLayer extends Model { defaults() { return { diff --git a/packages/joint-core/src/dia/HighlighterView.mjs b/packages/joint-core/src/dia/HighlighterView.mjs index ced89ed005..f7b7df9a49 100644 --- a/packages/joint-core/src/dia/HighlighterView.mjs +++ b/packages/joint-core/src/dia/HighlighterView.mjs @@ -117,7 +117,7 @@ export const HighlighterView = mvc.View.extend({ vGroup = V('g').addClass('highlight-transform').append(el); } this.transformGroup = vGroup; - paper.getLayerView(layerName).insertSortedNode(vGroup.node, options.z); + paper.getLayer(layerName).insertSortedNode(vGroup.node, options.z); } else { // TODO: prepend vs append if (!el.parentNode || el.nextSibling) { diff --git a/packages/joint-core/src/dia/LayerView.mjs b/packages/joint-core/src/dia/LayerView.mjs index b68cb2c553..e88daa45e7 100644 --- a/packages/joint-core/src/dia/LayerView.mjs +++ b/packages/joint-core/src/dia/LayerView.mjs @@ -1,5 +1,5 @@ import { View } from '../mvc/index.mjs'; -import { addClassNamePrefix } from '../util/util.mjs'; +import { addClassNamePrefix, sortElements } from '../util/util.mjs'; export class LayerView extends View { @@ -9,10 +9,6 @@ export class LayerView extends View { this.pivotNodes = {}; } - init(...args) { - super.init(...args); - } - className() { const { name } = this.options; if (!name) return null; diff --git a/packages/joint-core/src/dia/LinkView.mjs b/packages/joint-core/src/dia/LinkView.mjs index c1976bf809..1437dd5194 100644 --- a/packages/joint-core/src/dia/LinkView.mjs +++ b/packages/joint-core/src/dia/LinkView.mjs @@ -391,7 +391,7 @@ export const LinkView = CellView.extend({ if (!vLabels || !model.hasLabels()) return; const { node } = vLabels; if (options.labelsLayer) { - paper.getLayerView(options.labelsLayer).insertSortedNode(node, model.get('z')); + paper.getLayer(options.labelsLayer).insertSortedNode(node, model.get('z')); } else { if (node.parentNode !== el) { el.appendChild(node); diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 1c95218e83..27d2c3c6dd 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -38,7 +38,6 @@ import { ElementView } from './ElementView.mjs'; import { LinkView } from './LinkView.mjs'; import { Cell } from './Cell.mjs'; import { Graph } from './Graph.mjs'; -import { LayersNames } from './Layer.mjs'; import { LayerView } from './LayerView.mjs'; import * as highlighters from '../highlighters/index.mjs'; import * as linkAnchors from '../linkAnchors/index.mjs'; @@ -49,6 +48,15 @@ import $ from '../mvc/Dom/index.mjs'; import { GridLayer } from './layers/GridLayer.mjs'; import { EmbeddingLayersController } from './controllers/EmbeddingLayersController.mjs'; +export const LayersNames = { + GRID: 'grid', + CELLS: 'cells', + BACK: 'back', + FRONT: 'front', + TOOLS: 'tools', + LABELS: 'labels' +}; + const sortingTypes = { NONE: 'sorting-none', APPROX: 'sorting-approximate', @@ -375,9 +383,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; @@ -515,10 +520,6 @@ export const Paper = View.extend({ this.updateViews(data); } } - var sortDelayingBatches = this.SORT_DELAYING_BATCHES; - if (sortDelayingBatches.includes(name) && !graph.hasActiveBatch(sortDelayingBatches)) { - this.sortViews(); - } }, cloneOptions: function() { @@ -605,18 +606,20 @@ export const Paper = View.extend({ }]; }, - hasLayerView(layerName) { + hasLayer(layerName) { return (layerName in this._layers.viewsMap); }, - getLayerView(layerName) { + getLayer(layerName) { const { _layers: { viewsMap }} = this; - if (layerName in viewsMap) return viewsMap[layerName]; + if (layerName in viewsMap) { + return viewsMap[layerName]; + } throw new Error(`dia.Paper: Unknown layer "${layerName}".`); }, getLayerNode(layerName) { - return this.getLayerView(layerName).el; + return this.getLayer(layerName).el; }, _removeLayer(layerView) { @@ -633,8 +636,10 @@ export const Paper = View.extend({ delete viewsMap[layerName]; }, - _registerLayer(layerName, layerView, beforeLayerView, ignoreOrder) { + _registerLayer(layerView, beforeLayerView, ignoreOrder) { const { _layers: { viewsMap, order }} = this; + const layerName = layerView.name; + if (!ignoreOrder) { if (beforeLayerView) { const beforeLayerName = beforeLayerView.name; @@ -646,39 +651,27 @@ export const Paper = View.extend({ viewsMap[layerName] = layerView; }, - _getLayerView(layer) { - const { _layers: { viewsMap }} = this; - + _requireLayerView(layer) { let layerName; - if (layer instanceof LayerView) { + if (typeof layer === 'string') { + layerName = layer; + } else if (layer instanceof LayerView) { layerName = layer.name; } else { - layerName = layer; + throw new Error('dia.Paper: The layer view is not an instance of dia.LayerView.'); } - if (layerName in viewsMap) { - return viewsMap[layerName]; + if (!this.hasLayer(layerName)) { + throw new Error(`dia.Paper: Unknown layer "${layerName}".`); } - return null; + return this.getLayer(layerName); }, - _requireLayerView(layer) { - const layerView = this._getLayerView(layer); - if (!layerView) { - if (layer instanceof LayerView) { - throw new Error('dia.Paper: The layer is not registered.'); - } else { - throw new Error(`dia.Paper: Unknown layer "${layer}".`); - } + removeLayer(layer) { + if (!layer) { + throw new Error('dia.Paper: The layer view must be provided.'); } - return layerView; - }, - hasLayer(layer) { - return this._getLayerView(layer) !== null; - }, - - removeLayer(layer) { const layerView = this._requireLayerView(layer); if (!layerView.isEmpty()) { throw new Error('dia.Paper: The layer is not empty.'); @@ -687,12 +680,16 @@ export const Paper = View.extend({ }, addLayer(layerView, options = {}) { - const layerName = layer.name; + if (!layerView) { + throw new Error('dia.Paper: The layer view must be provided.'); + } + + const layerName = layerView.name; if (!layerName || typeof layerName !== 'string') { - throw new Error('dia.Paper: The layer name must be provided.'); + throw new Error('dia.Paper: The layer should has a name.'); } - if (this._getLayerView(layerName)) { + if (this.hasLayer(layerName)) { throw new Error(`dia.Paper: The layer "${layerName}" already exists.`); } if (!(layerView instanceof LayerView)) { @@ -700,24 +697,35 @@ export const Paper = View.extend({ } const { insertBefore, doNotAppend } = options; if (doNotAppend) { - this._registerLayer(layerName, layerView, null, true); + this._registerLayer(layerView, null, true); } else { if (!insertBefore) { - this._registerLayer(layerName, layerView, null); + this._registerLayer(layerView, null); this.layers.appendChild(layerView.el); } else { const beforeLayerView = this._requireLayerView(insertBefore); - this._registerLayer(layerName, layerView, beforeLayerView); + this._registerLayer(layerView, beforeLayerView); this.layers.insertBefore(layerView.el, beforeLayerView.el); } } }, moveLayer(layer, insertBefore) { - const layerView = this._requireLayerView(layer); - if (layerView === this._getLayerView(insertBefore)) return; - this._unregisterLayer(layerView); - this.addLayer(layerView, { insertBefore }); + if (!layer) { + throw new Error('dia.Paper: The layer view is not an instance of dia.LayerView.'); + } + + if (!this.hasLayer(layer.name)) { + throw new Error(`dia.Paper: Unknown layer "${layer.name}".`); + } + + if (insertBefore) { + const insertBeforeLayer = this._requireLayerView(insertBefore); + if (layer === insertBeforeLayer) + return; + } + this._unregisterLayer(layer); + this.addLayer(layer, { insertBefore }); }, getLayerNames() { @@ -727,7 +735,7 @@ export const Paper = View.extend({ getLayers() { // Returns a sorted array of layer views. - return this.getLayerNames().map(name => this.getLayerView(name)); + return this.getLayerNames().map(name => this.getLayer(name)); }, render: function() { @@ -785,9 +793,9 @@ export const Paper = View.extend({ this.removeLayers(); layers.forEach(attributes => this.renderLayer(attributes)); // 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.getLayer(LayersNames.CELLS); + const toolsLayerView = this.getLayer(LayersNames.TOOLS); + const labelsLayerView = this.getLayer(LayersNames.LABELS); // backwards compatibility this.tools = toolsLayerView.el; this.cells = this.viewport = cellsLayerView.el; @@ -1932,8 +1940,8 @@ export const Paper = View.extend({ insertView: function(view, isInitialInsert) { const { el, model } = view; - const layerName = model.get('layer') || this.DEFAULT_CELL_LAYER; - const layerView = this.getLayerView(layerName); + const layerName = model.layer(); + const layerView = this.getLayer(layerName); switch (this.options.sorting) { case sortingTypes.APPROX: @@ -2841,13 +2849,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.getLayer(LayersNames.GRID).renderGrid(); } return this; }, setGrid: function(drawGrid) { - this.getLayerView(LayersNames.GRID).setGrid(drawGrid); + this.getLayer(LayersNames.GRID).setGrid(drawGrid); return this; }, diff --git a/packages/joint-core/src/dia/ToolsView.mjs b/packages/joint-core/src/dia/ToolsView.mjs index 054f3b2490..7f324861f0 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 './Layer.mjs'; +import { LayersNames } from './Paper.mjs'; import { ToolView } from './ToolView.mjs'; export const ToolsView = mvc.View.extend({ @@ -143,7 +143,7 @@ export const ToolsView = mvc.View.extend({ const { relatedView, layer = LayersNames.TOOLS, z } = options; if (relatedView) { if (layer) { - relatedView.paper.getLayerView(layer).insertSortedNode(el, z); + relatedView.paper.getLayer(layer).insertSortedNode(el, z); } else { relatedView.el.appendChild(el); } diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index b79d77b340..96fc3dde42 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -1,5 +1,5 @@ import { Listener } from '../../mvc/Listener.mjs'; -import { Layer } from '../Layer.mjs'; +import { GraphLayer } from '../GraphLayer.mjs'; export class EmbeddingLayersController extends Listener { @@ -27,7 +27,7 @@ export class EmbeddingLayersController extends Listener { }); graph.removeLayer(layer); - this.removePaperLayer(layer); + this.removeEmbeddingLayer(layer); } }); @@ -43,7 +43,7 @@ export class EmbeddingLayersController extends Listener { let layer; if (parentId && !layersMap[parentId]) { - layer = new Layer({ + layer = new GraphLayer({ name: parentId }); graph.addLayer(layer); @@ -57,8 +57,8 @@ export class EmbeddingLayersController extends Listener { this.listenTo(paper, 'cell:inserted', (_appContext, cellView) => { const cellId = cellView.model.id; - if (paper.hasLayerView(cellId)) { - const layerView = paper.getLayerView(cellId); + if (paper.hasLayer(cellId)) { + const layerView = paper.getLayer(cellId); cellView.el.after(layerView.el); } }); @@ -67,9 +67,9 @@ export class EmbeddingLayersController extends Listener { insertEmbeddingLayer(layer) { const { paper } = this; - const cellId = layer.get('name'); + const cellId = layer.name; const layerView = paper.createLayer({ name: cellId, model: layer }); - paper.addLayer(cellId, layerView, { doNotAppend: true }); + paper.addLayer(layerView, { doNotAppend: true }); const cellView = paper.findViewByModel(cellId); if (cellView.isMounted()) { @@ -80,10 +80,10 @@ export class EmbeddingLayersController extends Listener { removeEmbeddingLayer(layer) { const { paper } = this; - const cellId = layer.get('name'); - const layerView = paper.hasLayerView(cellId); - if (layerView) { - paper.removeLayer(cellId); + const cellId = layer.name; + if (paper.hasLayer(cellId)) { + const layerView = paper.getLayer(cellId); + paper.removeLayer(layerView); } } } diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 49fe4a8b13..fc0478975a 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -49,6 +49,10 @@ export class GraphLayersController extends Listener { const layerName = cell.layer(); const layer = layersMap[layerName]; + if (!layer) { + throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); + } + if (!cell.has('z')) { cell.set('z', layer.maxZIndex() + 1); } @@ -83,7 +87,9 @@ export class GraphLayersController extends Listener { this.layers = this.layers.concat([layer]); + layer.set('graph', this.graph); layersMap[layer.name] = layer; + this.graph.set('layers', this.layers); } @@ -100,6 +106,9 @@ export class GraphLayersController extends Listener { this.layers = this.layers.filter(l => l.name !== layerName); + const layer = layersMap[layerName]; + layer.unset('graph'); + delete this.layersMap[layerName]; this.set('layers', this.layers); } diff --git a/packages/joint-core/src/dia/index.mjs b/packages/joint-core/src/dia/index.mjs index 132b9d1da7..1813173028 100644 --- a/packages/joint-core/src/dia/index.mjs +++ b/packages/joint-core/src/dia/index.mjs @@ -1,6 +1,6 @@ export * from './Graph.mjs'; export * from './attributes/index.mjs'; -export * from './Layer.mjs'; +export * from './GraphLayer.mjs'; export * from './LayerView.mjs'; export * from './Cell.mjs'; export * from './CellView.mjs'; diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs new file mode 100644 index 0000000000..cba889ef1a --- /dev/null +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -0,0 +1,79 @@ +import { LayerView } from '../LayerView.mjs'; + +export class GraphLayerView extends LayerView { + + init(...args) { + super.init(...args); + + const { model } = this; + + this.startListening(); + + this.listenTo(model, 'change:graph', (_, graph) => { + if (graph) + this.startGra + }); + } + + get SORT_DELAYING_BATCHES() { + return ['add', 'to-front', 'to-back']; + } + + startListening() { + const { model } = this; + if (!model) + return; + + this.listenTo(model, 'sort', () => { + if (this.model.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return; + this.sortLayer(); + }); + + const graph = model.get('graph'); + + if (!graph) + return; + + 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.sortLayer(); + } + }); + } + + sortLayer() { + 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.sortLayerExact(); + } + + sortLayerExact() { + // Run insertion sort algorithm in order to efficiently sort DOM elements according to their + // associated model `z` attribute. + const cellNodes = Array.from(this.el.childNodes).filter(node => node.getAttribute('model-id')); + const cells = this.model.get('cells'); + + sortElements(cellNodes, function(a, b) { + const cellA = cells.get(a.getAttribute('model-id')); + const cellB = cells.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; + }); + } + +} diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js index d2f125a250..f87f0b01cc 100644 --- a/packages/joint-core/test/jointjs/dia/Paper.js +++ b/packages/joint-core/test/jointjs/dia/Paper.js @@ -2141,8 +2141,8 @@ QUnit.module('joint.dia.Paper', function(hooks) { function() { paper.addLayer(); }, - /dia.Paper: The layer name must be provided./, - 'Layer name must be provided.' + /dia.Paper: The layer view must be provided./, + 'Layer view must be provided.' ); assert.throws( function() { @@ -2156,7 +2156,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { QUnit.test('throws error when layer with the same name already exists', function(assert) { assert.throws( function() { - paper.addLayer(new LayerView({ name: joint.dia.Paper.Layers.BACK })); + paper.addLayer(new joint.dia.LayerView({ name: joint.dia.Paper.Layers.BACK })); }, /dia.Paper: The layer "back" already exists./, 'Layer with the name "back" already exists.' @@ -2195,8 +2195,8 @@ QUnit.module('joint.dia.Paper', function(hooks) { function() { paper.removeLayer(); }, - /dia.Paper: Unknown layer "undefined"./, - 'Layer name must be provided.' + /dia.Paper: The layer view must be provided./, + 'Layer view must be provided.' ); }); @@ -2204,7 +2204,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { assert.notOk(paper.hasLayer('test')); assert.throws( function() { - paper.removeLayer('test'); + paper.removeLayer(new joint.dia.LayerView({ name: 'test' })); }, /dia.Paper: Unknown layer "test"./, 'Layer with the name "test" does not exist.' @@ -2237,18 +2237,6 @@ QUnit.module('joint.dia.Paper', function(hooks) { 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.LayerView(); - assert.throws( - function() { - paper.removeLayer(testLayer); - }, - /dia.Paper: The layer is not registered./, - 'Layer with the name "test" does not exist.' - ); - }); - }); QUnit.module('moveLayer()', function() { @@ -2258,7 +2246,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { function() { paper.moveLayer(); }, - /dia.Paper: Unknown layer "undefined"./, + /dia.Paper: The layer view is not an instance of dia.LayerView./, 'Layer name must be provided.' ); }); @@ -2267,7 +2255,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { assert.notOk(paper.hasLayer('test')); assert.throws( function() { - paper.moveLayer('test'); + paper.moveLayer(new joint.dia.LayerView({ name: 'test' })); }, /dia.Paper: Unknown layer "test"./, 'Layer with the name "test" does not exist.' @@ -2277,7 +2265,7 @@ 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.moveLayer(paper.getLayer(joint.dia.Paper.Layers.BACK), 'test'); }, /dia.Paper: Unknown layer "test"./, 'Invalid position "test".' @@ -2287,7 +2275,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { QUnit.test('moves the specified layer to the specified position', function(assert) { const layerNames = paper.getLayerNames(); const [firstLayer, secondLayer] = layerNames; - paper.moveLayer(secondLayer, firstLayer); + paper.moveLayer(paper.getLayer(secondLayer), firstLayer); const [newFirstLayer, newSecondLayer] = paper.getLayerNames(); assert.equal(newFirstLayer, secondLayer); assert.equal(newSecondLayer, firstLayer); @@ -2296,7 +2284,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { 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); + paper.moveLayer(paper.getLayer(firstLayer)); const newLayerNames = paper.getLayerNames(); assert.equal(newLayerNames.at(0), secondLayer); assert.equal(newLayerNames.at(-1), firstLayer); @@ -2305,7 +2293,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { 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); + paper.moveLayer(paper.getLayer(firstLayer), secondLayer); const newLayerNames = paper.getLayerNames(); assert.equal(newLayerNames.at(0), firstLayer); assert.equal(newLayerNames.at(1), secondLayer); @@ -2314,7 +2302,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { QUnit.test('it\'s ok to move layer before itself', function(assert) { const layerNames = paper.getLayerNames(); const [, secondLayer] = layerNames; - paper.moveLayer(secondLayer, secondLayer); + paper.moveLayer(paper.getLayer(secondLayer), secondLayer); const newLayerNames = paper.getLayerNames(); assert.equal(newLayerNames.at(1), secondLayer); }); @@ -2334,19 +2322,13 @@ QUnit.module('joint.dia.Paper', function(hooks) { name: 'test1' }); paper.addLayer(test1Layer); - - 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' }); assert.throws( () => { - graph.addCell(r3, { async: false }); + graph.addCell(r2, { async: false }); }, - /dia.Paper: Unknown layer "test2"./, - 'Layer "test2" does not exist.' + /dia.Graph: Layer with name 'test1' does not exist./, + 'Layer "test1" does not exist on Graph Level.' ); }); @@ -2356,8 +2338,8 @@ QUnit.module('joint.dia.Paper', function(hooks) { graph.addCell(r1, { async: false }); assert.ok(paper.getLayerNode('cells').contains(r1.findView(paper).el)); - const test1Layer = new joint.dia.LayerView(); - paper.addLayer('test1', test1Layer); + const test1Layer = new joint.dia.LayerView({ name: 'test1' }); + paper.addLayer(test1Layer); r1.set('layer', 'test1', { async: false }); assert.ok(paper.getLayerNode('test1').contains(r1.findView(paper).el)); diff --git a/packages/joint-core/test/jointjs/paper.js b/packages/joint-core/test/jointjs/paper.js index 4f8a5df081..8a3994876c 100644 --- a/packages/joint-core/test/jointjs/paper.js +++ b/packages/joint-core/test/jointjs/paper.js @@ -1782,7 +1782,7 @@ QUnit.module('paper', function(hooks) { QUnit.module('draw grid options', function(hooks) { const getGridSettings = function(paper) { - return paper.getLayerView(joint.dia.Paper.Layers.GRID)._gridSettings; + return paper.getLayer(joint.dia.Paper.Layers.GRID)._gridSettings; }; const getGridVel = function(paper) { @@ -1857,7 +1857,7 @@ QUnit.module('paper', function(hooks) { gridSize: 1, drawGridSize: 17 }); - const drawGridSpy = sinon.spy(paper.getLayerView(joint.dia.Paper.Layers.GRID), 'renderGrid'); + const drawGridSpy = sinon.spy(paper.getLayer(joint.dia.Paper.Layers.GRID), 'renderGrid'); paper.setGridSize(5); assert.ok(drawGridSpy.notCalled); drawGridSpy.restore(); diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 8c6d09be47..2884a064cd 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -205,11 +205,11 @@ export namespace dia { resetCells(cells: Array, opt?: Graph.Options): this; - addLayer(layer: Layer): void; + addLayer(layer: GraphLayer): void; - removeLayer(layer: Layer): void; + removeLayer(layer: GraphLayer): void; - getDefaultLayer(): Layer; + getDefaultLayer(): GraphLayer; getCell(id: Cell.ID | Cell): Cell; @@ -1762,9 +1762,9 @@ export namespace dia { getLayerNode(layerName: Paper.Layers | string): SVGGElement; - getLayerView(layerName: Paper.Layers | string): LayerView; + getLayer(layerName: Paper.Layers | string): LayerView; - hasLayerView(layerName: Paper.Layers | string): boolean; + hasLayer(layerName: Paper.Layers | string): boolean; renderLayers(layers: Array<{ name: string }>): void; @@ -1772,13 +1772,11 @@ export namespace dia { protected resetLayers(): void; - addLayer(layerName: string, layerView: LayerView, options?: { insertBefore?: string }): void; + addLayer(layerView: LayerView, options?: { insertBefore?: string | LayerView }): void; - removeLayer(layer: string | LayerView): void; + removeLayer(LayerView: LayerView): void; - moveLayer(layer: string | LayerView, insertBefore: string | LayerView | null): void; - - hasLayer(layer: string | LayerView): boolean; + moveLayer(layerView: LayerView, insertBefore: string | LayerView | null): void; getLayerNames(): string[]; @@ -1998,7 +1996,7 @@ export namespace dia { removePivots(): void; } - class Layer extends mvc.Model { + class GraphLayer extends mvc.Model { name: string; From 0a2f047e6054106c42de5a7fa4ca576894253590 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Thu, 5 Jun 2025 16:20:26 +0200 Subject: [PATCH 22/54] up --- packages/joint-core/src/dia/Paper.mjs | 53 +++++++------------ .../src/dia/layers/GraphLayerView.mjs | 24 ++++----- .../{GridLayer.mjs => GridLayerView.mjs} | 6 ++- packages/joint-core/test/jointjs/basic.js | 2 +- packages/joint-core/test/jointjs/dia/Paper.js | 22 ++++---- packages/joint-core/test/jointjs/paper.js | 8 +-- 6 files changed, 52 insertions(+), 63 deletions(-) rename packages/joint-core/src/dia/layers/{GridLayer.mjs => GridLayerView.mjs} (97%) diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 27d2c3c6dd..6a76c3d1ea 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, @@ -39,13 +38,14 @@ import { LinkView } from './LinkView.mjs'; import { Cell } from './Cell.mjs'; import { Graph } from './Graph.mjs'; import { LayerView } from './LayerView.mjs'; +import { GraphLayerView } from './layers/GraphLayerView.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 LayersNames = { @@ -289,6 +289,11 @@ export const Paper = View.extend({ cellViewNamespace: null, + layerViewNamespace: { + 'GridLayerView': GridLayerView, + 'GraphLayerView': GraphLayerView, + }, + routerNamespace: null, connectorNamespace: null, @@ -396,12 +401,14 @@ export const Paper = View.extend({ // Paper layers this._layersSettings = [{ + type: 'GridLayerView', name: LayersNames.GRID, }, { name: LayersNames.BACK, }, { name: LayersNames.LABELS, }, { + type: 'GraphLayerView', name: LayersNames.CELLS, model: this.model.getDefaultLayer() }, { @@ -465,7 +472,6 @@ export const Paper = View.extend({ .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); this.on('cell:highlight', this.onCellHighlight) @@ -505,11 +511,6 @@ export const Paper = View.extend({ this.resetViews(collection.models, opt); }, - onGraphSort: function() { - if (this.model.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return; - this.sortViews(); - }, - onGraphBatchStop: function(data) { if (this.isFrozen()) return; var name = data && data.batchName; @@ -775,12 +776,9 @@ export const Paper = View.extend({ createLayer(attributes) { attributes.paper = this; - switch (attributes.name) { - case LayersNames.GRID: - return new GridLayer({ ...attributes, patterns: this.constructor.gridPatterns }); - default: - return new LayerView(attributes); - } + const viewConstructor = this.options.layerViewNamespace[attributes.type] || LayerView; + + return new viewConstructor(attributes); }, renderLayer: function(attributes) { @@ -1474,7 +1472,7 @@ export const Paper = View.extend({ } this.options.frozen = updates.keyFrozen = false; if (updates.sort) { - this.sortViews(); + this.sortLayers(); updates.sort = false; } }, @@ -1896,7 +1894,7 @@ export const Paper = View.extend({ this.renderView(cells[i], opt); } this.unfreeze({ key }); - this.sortViews(); + this.sortLayers(); }, removeViews: function() { @@ -1906,8 +1904,7 @@ export const Paper = View.extend({ this._views = {}; }, - sortViews: function() { - + sortLayers: function() { if (!this.isExactSorting()) { // noop return; @@ -1917,24 +1914,13 @@ 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 GraphLayerView).forEach(view => view.sortLayerExact()); }, insertView: function(view, isInitialInsert) { @@ -3312,7 +3298,6 @@ export const Paper = View.extend({ return canvas; } }, - gridPatterns: { dot: [{ color: '#AAAAAA', diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index cba889ef1a..3aac0ea4fa 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -1,4 +1,5 @@ import { LayerView } from '../LayerView.mjs'; +import { sortElements } from '../../util/index.mjs'; export class GraphLayerView extends LayerView { @@ -7,11 +8,17 @@ export class GraphLayerView extends LayerView { const { model } = this; - this.startListening(); + const graph = model.get('graph'); + if (graph) { + this.startListening(graph); + } this.listenTo(model, 'change:graph', (_, graph) => { - if (graph) - this.startGra + if (graph) { + this.startListening(graph); + } else { + this.stopListening(); + } }); } @@ -19,21 +26,14 @@ export class GraphLayerView extends LayerView { return ['add', 'to-front', 'to-back']; } - startListening() { + startListening(graph) { const { model } = this; - if (!model) - return; this.listenTo(model, 'sort', () => { - if (this.model.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return; + if (graph.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return; this.sortLayer(); }); - const graph = model.get('graph'); - - if (!graph) - return; - this.listenTo(graph, 'batch:stop', (data) => { const name = data && data.batchName; const sortDelayingBatches = this.SORT_DELAYING_BATCHES; diff --git a/packages/joint-core/src/dia/layers/GridLayer.mjs b/packages/joint-core/src/dia/layers/GridLayerView.mjs similarity index 97% rename from packages/joint-core/src/dia/layers/GridLayer.mjs rename to packages/joint-core/src/dia/layers/GridLayerView.mjs index 0e6c53df25..57070f9ca6 100644 --- a/packages/joint-core/src/dia/layers/GridLayer.mjs +++ b/packages/joint-core/src/dia/layers/GridLayerView.mjs @@ -9,7 +9,7 @@ import { } from '../../util/index.mjs'; import V from '../../V/index.mjs'; -export class GridLayer extends LayerView { +export class GridLayerView extends LayerView { preinitialize() { super.preinitialize(); @@ -22,7 +22,11 @@ export class GridLayer extends LayerView { init(...args) { super.init(...args); + const { options: { paper }} = this; + + this.options.patterns = paper.constructor.gridPatterns; + this._gridCache = null; this._gridSettings = []; this.listenTo(paper, 'transform resize', this.updateGrid); diff --git a/packages/joint-core/test/jointjs/basic.js b/packages/joint-core/test/jointjs/basic.js index 0ce785bb35..8808f85cf2 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, 'sortLayers'); var r1View = this.paper.findViewByModel(r1); var r2View = this.paper.findViewByModel(r2); diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js index f87f0b01cc..29ca790fb6 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,13 @@ 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 graph.clear(); graph.addCells([rect1, rect2, rect3]); - 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) { rect1View = rect1.findView(paper); rect2View = rect2.findView(paper); @@ -995,25 +995,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); }); }); diff --git a/packages/joint-core/test/jointjs/paper.js b/packages/joint-core/test/jointjs/paper.js index 8a3994876c..2ecdfe35d6 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 sortLayers()', function(assert) { - var spy = sinon.spy(this.paper, 'sortViews'); + var spy = sinon.spy(this.paper, 'sortLayers'); 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 sortLayers()', function(assert) { - var spy = sinon.spy(this.paper, 'sortViews'); + var spy = sinon.spy(this.paper, 'sortLayers'); var r1 = new joint.shapes.standard.Rectangle; var r2 = new joint.shapes.standard.Rectangle; From 8a03a898cf5180c219cf41817680b9041e123895 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 6 Jun 2025 12:41:55 +0200 Subject: [PATCH 23/54] up --- packages/joint-core/src/dia/Graph.mjs | 4 ++-- packages/joint-core/src/dia/GraphLayer.mjs | 3 +-- .../dia/controllers/GraphLayersController.mjs | 14 +++++++++++-- packages/joint-core/test/jointjs/basic.js | 2 +- packages/joint-core/test/jointjs/dia/Paper.js | 21 +++++++++++++------ 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index d34e2e8d77..8465ea04d1 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -449,8 +449,8 @@ export const Graph = Model.extend({ }, getCells: function() { - - return this.get('cells').toArray(); + // Preserve old order without layers + return this.layersController.getCells(); }, getElements: function() { diff --git a/packages/joint-core/src/dia/GraphLayer.mjs b/packages/joint-core/src/dia/GraphLayer.mjs index 6806966a91..5ea3db92c9 100644 --- a/packages/joint-core/src/dia/GraphLayer.mjs +++ b/packages/joint-core/src/dia/GraphLayer.mjs @@ -36,12 +36,11 @@ export class GraphLayer extends Model { } add(cell) { - cell.setLayer(this.name); this.get('cells').add(cell); } remove(cell) { - cell.setLayer(); + cell.unsetLayer(); this.get('cells').remove(cell); } diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index fc0478975a..c92f41bcfa 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -46,7 +46,7 @@ export class GraphLayersController extends Listener { onAdd(cell) { const { layersMap } = this; - const layerName = cell.layer(); + const layerName = cell.layer() || this.defaultLayerName; const layer = layersMap[layerName]; if (!layer) { @@ -65,7 +65,7 @@ export class GraphLayersController extends Listener { onRemove(cell) { const { layersMap } = this; - const layerName = cell.layer(); + const layerName = cell.layer() || this.defaultLayerName; const layer = layersMap[layerName]; @@ -140,4 +140,14 @@ export class GraphLayersController extends Listener { getLayerCells(layerName) { return this.layersMap[layerName].get('cells'); } + + getCells() { + const cells = []; + + for (let layerName in this.layersMap) { + cells.push(...this.layersMap[layerName].get('cells')); + } + + return cells; + } } diff --git a/packages/joint-core/test/jointjs/basic.js b/packages/joint-core/test/jointjs/basic.js index 8808f85cf2..b073651685 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, 'sortLayers'); + var spy = sinon.spy(this.paper.getLayer('cells'), 'sortLayer'); var r1View = this.paper.findViewByModel(r1); var r2View = this.paper.findViewByModel(r2); diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js index 29ca790fb6..9fb2f59496 100644 --- a/packages/joint-core/test/jointjs/dia/Paper.js +++ b/packages/joint-core/test/jointjs/dia/Paper.js @@ -977,9 +977,10 @@ QUnit.module('joint.dia.Paper', function(hooks) { rect3.translate(10, 10); assert.ok(sortLayersExactSpy.notCalled); // ADD CELLS + var sortLayerExactSpy = sinon.spy(paper.getLayer('cells'), 'sortLayerExact'); graph.clear(); graph.addCells([rect1, rect2, rect3]); - assert.equal(sortLayersExactSpy.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); @@ -2315,13 +2316,8 @@ QUnit.module('joint.dia.Paper', function(hooks) { const r1 = new joint.shapes.standard.Rectangle(); graph.addCell(r1, { async: false }); - assert.equal(r1.get('layer'), 'cells'); assert.ok(paper.getLayerNode('cells').contains(r1.findView(paper).el)); - const test1Layer = new joint.dia.LayerView({ - name: 'test1' - }); - paper.addLayer(test1Layer); const r2 = new joint.shapes.standard.Rectangle({ layer: 'test1' }); assert.throws( () => { @@ -2330,6 +2326,19 @@ QUnit.module('joint.dia.Paper', function(hooks) { /dia.Graph: Layer with name 'test1' does not exist./, 'Layer "test1" does not exist on Graph Level.' ); + + graph.removeCells([r2], { async: false }); + + const test1Layer = new joint.dia.GraphLayer({ name: 'test1' }); + graph.addLayer(test1Layer); + paper.renderLayer({ + name: 'test1', + type: 'GraphLayerView', + model: test1Layer + }); + + graph.addCell(r2, { async: false }); + assert.ok(paper.getLayerNode('test1').contains(r2.findView(paper).el)); }); QUnit.test('cell view is moved to correct layer', function(assert) { From d058bac09ec3653ada6ed7362867b73712722539 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 6 Jun 2025 13:22:11 +0200 Subject: [PATCH 24/54] reset fix --- packages/joint-core/src/dia/Graph.mjs | 12 ++++++++---- .../joint-core/src/dia/layers/GraphLayerView.mjs | 2 +- packages/joint-core/test/jointjs/paper.js | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 8465ea04d1..4cc1adcc2e 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -344,12 +344,16 @@ 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.stopBatch('reset', opt); + return this; }, @@ -455,22 +459,22 @@ export const Graph = Model.extend({ 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() { - return this.get('cells').first(); + return this.getCells().first(); }, getLastCell: function() { - return this.get('cells').last(); + return this.getCells().last(); }, // Get all inbound and outbound links connected to the cell `model`. diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index 3aac0ea4fa..471d3460d1 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -23,7 +23,7 @@ export class GraphLayerView extends LayerView { } get SORT_DELAYING_BATCHES() { - return ['add', 'to-front', 'to-back']; + return ['add', 'reset', 'to-front', 'to-back']; } startListening(graph) { diff --git a/packages/joint-core/test/jointjs/paper.js b/packages/joint-core/test/jointjs/paper.js index 2ecdfe35d6..51fefda631 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 sortLayers()', function(assert) { + QUnit.test('paper.addCell() number of sortLayer()', function(assert) { - var spy = sinon.spy(this.paper, 'sortLayers'); + var spy = sinon.spy(this.paper.getLayer('cells'), 'sortLayer'); 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 sortLayers()', function(assert) { + QUnit.test('paper.addCells() number of sortLayer()', function(assert) { - var spy = sinon.spy(this.paper, 'sortLayers'); + var spy = sinon.spy(this.paper.getLayer('cells'), 'sortLayer'); var r1 = new joint.shapes.standard.Rectangle; var r2 = new joint.shapes.standard.Rectangle; From 468a6c686f01157117716fa7d63936818185026c Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 9 Jun 2025 13:40:37 +0200 Subject: [PATCH 25/54] wip --- packages/joint-core/src/dia/Graph.mjs | 4 ++++ packages/joint-core/src/dia/GraphLayer.mjs | 1 + packages/joint-core/src/dia/Paper.mjs | 15 ++++++++++++++- .../dia/controllers/EmbeddingLayersController.mjs | 8 ++------ .../src/dia/controllers/GraphLayersController.mjs | 12 +++++++++++- .../joint-core/src/dia/layers/GraphLayerView.mjs | 15 ++++++++++++++- 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 4cc1adcc2e..0310a12a36 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -310,6 +310,10 @@ export const Graph = Model.extend({ return this.layersController.maxZIndex(layerName); }, + addToLayer: function(cell, layer) { + this.layersController.addToLayer(cell, layer); + }, + addCell: function(cell, opt) { if (Array.isArray(cell)) { diff --git a/packages/joint-core/src/dia/GraphLayer.mjs b/packages/joint-core/src/dia/GraphLayer.mjs index 5ea3db92c9..f1162cfada 100644 --- a/packages/joint-core/src/dia/GraphLayer.mjs +++ b/packages/joint-core/src/dia/GraphLayer.mjs @@ -12,6 +12,7 @@ export class GraphLayer extends Model { defaults() { return { + type: 'GraphLayer', displayName: '', hidden: false, locked: false, diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 6a76c3d1ea..ccc8e0907a 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -674,6 +674,11 @@ export const Paper = View.extend({ } const layerView = this._requireLayerView(layer); + + if (layerView instanceof GraphLayerView) { + layerView._prepareRemove(); + } + if (!layerView.isEmpty()) { throw new Error('dia.Paper: The layer is not empty.'); } @@ -776,7 +781,15 @@ export const Paper = View.extend({ createLayer(attributes) { attributes.paper = this; - const viewConstructor = this.options.layerViewNamespace[attributes.type] || LayerView; + + let type = attributes.type; + + if (attributes.model) { + const modelType = attributes.model.get('type') || attributes.model.constructor.name; + type = modelType + 'View'; + } + + const viewConstructor = this.options.layerViewNamespace[type] || LayerView; return new viewConstructor(attributes); }, diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 96fc3dde42..a7a7bf0f24 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -17,13 +17,12 @@ export class EmbeddingLayersController extends Listener { this.listenTo(graph, 'remove', (_appContext, cell) => { const layersMap = graph.getLayersMap(); - const activeLayer = graph.getActiveLayer(); if (layersMap[cell.id]) { const layer = layersMap[cell.id]; const cells = layer.get('cells').models; cells.forEach((cell) => { - activeLayer.add(cell); + graph.addToLayer(cell); }); graph.removeLayer(layer); @@ -33,7 +32,6 @@ export class EmbeddingLayersController extends Listener { this.listenTo(graph, 'change:parent', (_appContext, cell, parentId) => { const layersMap = graph.getLayersMap(); - const activeLayer = graph.getActiveLayer(); const currentLayer = cell.layer(); @@ -50,9 +48,7 @@ export class EmbeddingLayersController extends Listener { this.insertEmbeddingLayer(layer); } - const targetLayer = layersMap[parentId] || activeLayer; - - targetLayer.add(cell); + graph.addToLayer(cell, layersMap[parentId]); }); this.listenTo(paper, 'cell:inserted', (_appContext, cellView) => { diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index c92f41bcfa..beda5a0006 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -78,6 +78,16 @@ export class GraphLayersController extends Listener { return this.layersMap[this.defaultLayerName]; } + addToLayer(cell, layer) { + if (!layer) { + layer = this.getDefaultLayer(); + layer.add(cell); + } else { + layer.add(cell); + cell.setLayer(layer.name); + } + } + addLayer(layer, _opt) { const { layersMap } = this; @@ -110,7 +120,7 @@ export class GraphLayersController extends Listener { layer.unset('graph'); delete this.layersMap[layerName]; - this.set('layers', this.layers); + this.graph.set('layers', this.layers); } minZIndex(layerName) { diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index 471d3460d1..a1a28edc61 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -64,7 +64,7 @@ export class GraphLayerView extends LayerView { sortLayerExact() { // Run insertion sort algorithm in order to efficiently sort DOM elements according to their // associated model `z` attribute. - const cellNodes = Array.from(this.el.childNodes).filter(node => node.getAttribute('model-id')); + const cellNodes = Array.from(this.el.children).filter(node => node.getAttribute('model-id')); const cells = this.model.get('cells'); sortElements(cellNodes, function(a, b) { @@ -76,4 +76,17 @@ export class GraphLayerView extends LayerView { }); } + _prepareRemove() { + const cellNodes = Array.from(this.el.children).filter(node => node.getAttribute('model-id')); + const cells = this.model.get('cells'); + + cellNodes.forEach((node) => { + const cellId = node.getAttribute('model-id'); + + if (!cells.has(cellId)) { + this.el.removeChild(node); + } + }); + } + } From 5083b69ed5bcb5530de9013beebc6aaebd644510 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 9 Jun 2025 15:42:27 +0200 Subject: [PATCH 26/54] up --- packages/joint-core/src/dia/layers/GraphLayerView.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index a1a28edc61..a5dea8a1fb 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -14,10 +14,10 @@ export class GraphLayerView extends LayerView { } this.listenTo(model, 'change:graph', (_, graph) => { + this.stopListening(); + if (graph) { this.startListening(graph); - } else { - this.stopListening(); } }); } From dab0da8c29d765cfb2a41e66a1a99b4864a345e9 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 16 Jun 2025 13:43:50 +0200 Subject: [PATCH 27/54] update --- packages/joint-core/src/dia/Graph.mjs | 2 +- packages/joint-core/src/dia/Paper.mjs | 2 +- .../src/dia/controllers/EmbeddingLayersController.mjs | 2 +- .../src/dia/controllers/GraphLayersController.mjs | 10 ++++++---- packages/joint-core/src/dia/index.mjs | 6 ++++-- .../joint-core/src/dia/{ => layers}/GraphLayer.mjs | 2 +- packages/joint-core/src/dia/layers/GraphLayerView.mjs | 2 +- packages/joint-core/src/dia/layers/GridLayerView.mjs | 3 +-- packages/joint-core/src/dia/{ => layers}/LayerView.mjs | 4 ++-- packages/joint-core/types/joint.d.ts | 1 + 10 files changed, 19 insertions(+), 15 deletions(-) rename packages/joint-core/src/dia/{ => layers}/GraphLayer.mjs (95%) rename packages/joint-core/src/dia/{ => layers}/LayerView.mjs (94%) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 0310a12a36..11e014b825 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -1,7 +1,7 @@ import * as util from '../util/index.mjs'; import * as g from '../g/index.mjs'; -import { GraphLayer } from './GraphLayer.mjs'; +import { GraphLayer } from './layers/GraphLayer.mjs'; import { Model } from '../mvc/Model.mjs'; import { Collection } from '../mvc/Collection.mjs'; import { wrappers, wrapWith } from '../util/wrappers.mjs'; diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index ccc8e0907a..09575407ca 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -37,7 +37,7 @@ import { ElementView } from './ElementView.mjs'; import { LinkView } from './LinkView.mjs'; import { Cell } from './Cell.mjs'; import { Graph } from './Graph.mjs'; -import { LayerView } from './LayerView.mjs'; +import { LayerView } from './layers/LayerView.mjs'; import { GraphLayerView } from './layers/GraphLayerView.mjs'; import * as highlighters from '../highlighters/index.mjs'; import * as linkAnchors from '../linkAnchors/index.mjs'; diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index a7a7bf0f24..8b4f1af7e0 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -1,5 +1,5 @@ import { Listener } from '../../mvc/Listener.mjs'; -import { GraphLayer } from '../GraphLayer.mjs'; +import { GraphLayer } from '../layers/GraphLayer.mjs'; export class EmbeddingLayersController extends Listener { diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index beda5a0006..5a773ff214 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -38,12 +38,12 @@ export class GraphLayersController extends Listener { } cells.forEach(cell => { - this.onAdd(cell); + this.onAdd(cell, true); }); }); } - onAdd(cell) { + onAdd(cell, reset = false) { const { layersMap } = this; const layerName = cell.layer() || this.defaultLayerName; @@ -53,8 +53,10 @@ export class GraphLayersController extends Listener { throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); } - if (!cell.has('z')) { - cell.set('z', layer.maxZIndex() + 1); + if (!reset) { + if (!cell.has('z')) { + cell.set('z', layer.maxZIndex() + 1); + } } // mandatory add to the layer diff --git a/packages/joint-core/src/dia/index.mjs b/packages/joint-core/src/dia/index.mjs index 1813173028..f55b97f59c 100644 --- a/packages/joint-core/src/dia/index.mjs +++ b/packages/joint-core/src/dia/index.mjs @@ -1,7 +1,5 @@ export * from './Graph.mjs'; export * from './attributes/index.mjs'; -export * from './GraphLayer.mjs'; -export * from './LayerView.mjs'; export * from './Cell.mjs'; export * from './CellView.mjs'; export * from './Element.mjs'; @@ -12,3 +10,7 @@ export * from './Paper.mjs'; export * from './ToolView.mjs'; export * from './ToolsView.mjs'; export * from './HighlighterView.mjs'; +export * from './layers/GraphLayer.mjs'; +export * from './layers/LayerView.mjs'; +export * from './layers/GridLayerView.mjs'; +export * from './layers/GraphLayerView.mjs'; diff --git a/packages/joint-core/src/dia/GraphLayer.mjs b/packages/joint-core/src/dia/layers/GraphLayer.mjs similarity index 95% rename from packages/joint-core/src/dia/GraphLayer.mjs rename to packages/joint-core/src/dia/layers/GraphLayer.mjs index f1162cfada..12869b19d1 100644 --- a/packages/joint-core/src/dia/GraphLayer.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayer.mjs @@ -1,4 +1,4 @@ -import { Model, Collection } from '../mvc/index.mjs'; +import { Model, Collection } from '../../mvc/index.mjs'; class LayerCells extends Collection { diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index a5dea8a1fb..3d8b1e021b 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -1,4 +1,4 @@ -import { LayerView } from '../LayerView.mjs'; +import { LayerView } from './LayerView.mjs'; import { sortElements } from '../../util/index.mjs'; export class GraphLayerView extends LayerView { diff --git a/packages/joint-core/src/dia/layers/GridLayerView.mjs b/packages/joint-core/src/dia/layers/GridLayerView.mjs index 57070f9ca6..a703dd40ce 100644 --- a/packages/joint-core/src/dia/layers/GridLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GridLayerView.mjs @@ -1,4 +1,4 @@ -import { LayerView } from '../LayerView.mjs'; +import { LayerView } from './LayerView.mjs'; import { isFunction, isString, @@ -17,7 +17,6 @@ export class GridLayerView extends LayerView { this.style = { 'pointer-events': 'none' } - } init(...args) { diff --git a/packages/joint-core/src/dia/LayerView.mjs b/packages/joint-core/src/dia/layers/LayerView.mjs similarity index 94% rename from packages/joint-core/src/dia/LayerView.mjs rename to packages/joint-core/src/dia/layers/LayerView.mjs index e88daa45e7..57f9b204ff 100644 --- a/packages/joint-core/src/dia/LayerView.mjs +++ b/packages/joint-core/src/dia/layers/LayerView.mjs @@ -1,5 +1,5 @@ -import { View } from '../mvc/index.mjs'; -import { addClassNamePrefix, sortElements } from '../util/util.mjs'; +import { View } from '../../mvc/index.mjs'; +import { addClassNamePrefix } from '../../util/util.mjs'; export class LayerView extends View { diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 2884a064cd..ad5774f731 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -1979,6 +1979,7 @@ export namespace dia { name: string; } } + class LayerView extends mvc.View { constructor(opt?: LayerView.Options); From 58be262088c7ff6023e2623ba6dd2b2a2627a581 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 16 Jun 2025 13:45:11 +0200 Subject: [PATCH 28/54] up --- .../joint-core/src/dia/controllers/GraphLayersController.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 5a773ff214..61b5c06fce 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -53,6 +53,8 @@ export class GraphLayersController extends Listener { throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); } + // compatibility + // in the version before layers, z-index was not set on reset if (!reset) { if (!cell.has('z')) { cell.set('z', layer.maxZIndex() + 1); From 15ec945a50e664779eaed40c001d3790ba6d9d92 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 17 Jun 2025 17:10:09 +0200 Subject: [PATCH 29/54] tests start --- packages/joint-core/test/jointjs/index.html | 3 +- .../joint-core/test/jointjs/layers/basic.js | 27 ++++++++++++++++++ .../test/jointjs/layers/embedding.js | 28 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/joint-core/test/jointjs/layers/basic.js create mode 100644 packages/joint-core/test/jointjs/layers/embedding.js diff --git a/packages/joint-core/test/jointjs/index.html b/packages/joint-core/test/jointjs/index.html index 0da4fdbc83..3b05711a2d 100644 --- a/packages/joint-core/test/jointjs/index.html +++ b/packages/joint-core/test/jointjs/index.html @@ -43,6 +43,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..22edca0876 --- /dev/null +++ b/packages/joint-core/test/jointjs/layers/basic.js @@ -0,0 +1,27 @@ +QUnit.module('layers-basic', function(hooks) { + + hooks.beforeEach(function() { + + 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, + }); + }); + + QUnit.test('Default layers setup', function(assert) { + + assert.ok(this.graph.layersController, 'Graph layers controller is created'); + }); + + hooks.afterEach(function() { + + this.paper.remove(); + this.graph = null; + this.paper = null; + }); +}); 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..398ee84d62 --- /dev/null +++ b/packages/joint-core/test/jointjs/layers/embedding.js @@ -0,0 +1,28 @@ +QUnit.module('embedding-layers', function(hooks) { + + hooks.beforeEach(function() { + + 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, + useLayersForEmbedding: true + }); + }); + + QUnit.test('Embedding layers setup', function(assert) { + + assert.ok(this.paper.embeddingLayersController, 1, 'Controller is created'); + }); + + hooks.afterEach(function() { + + this.paper.remove(); + this.graph = null; + this.paper = null; + }); +}); From e4786f2232c91672d098f4e81c128566be75da71 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 18 Jun 2025 11:43:41 +0200 Subject: [PATCH 30/54] fixes and tests --- packages/joint-core/src/dia/Graph.mjs | 8 ++------ packages/joint-core/src/dia/Paper.mjs | 1 - .../src/dia/controllers/GraphLayersController.mjs | 8 ++++++-- packages/joint-core/src/dia/layers/GraphLayer.mjs | 10 ++++++++++ .../joint-core/src/dia/layers/GraphLayerView.mjs | 14 +++++++------- packages/joint-core/test/jointjs/layers/basic.js | 15 ++++++++++++++- 6 files changed, 39 insertions(+), 17 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 11e014b825..c788f81357 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -76,15 +76,11 @@ export const Graph = Model.extend({ this.defaultLayerName = 'cells'; const defaultLayer = new GraphLayer({ - name: this.defaultLayerName, - graph: this, + name: this.defaultLayerName }); - this.set('layers', [ - defaultLayer - ]); - this.layersController = new GraphLayersController({ graph: this }); + this.layersController.addLayer(defaultLayer); // Passing `cellModel` function in the options object to graph allows for // setting models based on attribute objects. This is especially handy diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 09575407ca..f31a318cb1 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -408,7 +408,6 @@ export const Paper = View.extend({ }, { name: LayersNames.LABELS, }, { - type: 'GraphLayerView', name: LayersNames.CELLS, model: this.model.getDefaultLayer() }, { diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 61b5c06fce..294054c21c 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -7,6 +7,10 @@ export class GraphLayersController extends Listener { this.graph = context.graph; + if (!this.graph.has('layers')) { + this.graph.set('layers', []); + } + this.layers = this.graph.get('layers'); this.layersMap = {}; @@ -101,7 +105,7 @@ export class GraphLayersController extends Listener { this.layers = this.layers.concat([layer]); - layer.set('graph', this.graph); + layer.addToGraph(this.graph); layersMap[layer.name] = layer; this.graph.set('layers', this.layers); @@ -121,7 +125,7 @@ export class GraphLayersController extends Listener { this.layers = this.layers.filter(l => l.name !== layerName); const layer = layersMap[layerName]; - layer.unset('graph'); + layer.removeFromGraph(); delete this.layersMap[layerName]; this.graph.set('layers', this.layers); diff --git a/packages/joint-core/src/dia/layers/GraphLayer.mjs b/packages/joint-core/src/dia/layers/GraphLayer.mjs index 12869b19d1..5fa225ade5 100644 --- a/packages/joint-core/src/dia/layers/GraphLayer.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayer.mjs @@ -58,4 +58,14 @@ export class GraphLayer extends Model { const lastCell = this.get('cells').last(); return lastCell ? (lastCell.get('z') || 0) : 0; } + + addToGraph(graph) { + this.graph = graph; + this.trigger('addToGraph', this.graph); + } + + removeFromGraph() { + this.graph = null; + this.trigger('removeFromGraph'); + } } diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index 3d8b1e021b..b1531848fb 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -8,17 +8,17 @@ export class GraphLayerView extends LayerView { const { model } = this; - const graph = model.get('graph'); - if (graph) { - this.startListening(graph); + if (model.graph) { + this.startListening(model.graph); } - this.listenTo(model, 'change:graph', (_, graph) => { + this.listenTo(model, 'addedToGraph', (graph) => { this.stopListening(); + this.startListening(graph); + }); - if (graph) { - this.startListening(graph); - } + this.listenTo(model, 'removedFromGraph', () => { + this.stopListening(); }); } diff --git a/packages/joint-core/test/jointjs/layers/basic.js b/packages/joint-core/test/jointjs/layers/basic.js index 22edca0876..fbe0295279 100644 --- a/packages/joint-core/test/jointjs/layers/basic.js +++ b/packages/joint-core/test/jointjs/layers/basic.js @@ -14,8 +14,21 @@ QUnit.module('layers-basic', function(hooks) { }); QUnit.test('Default layers setup', function(assert) { - assert.ok(this.graph.layersController, 'Graph layers controller is created'); + + const layers = this.graph.get('layers'); + + assert.ok(Array.isArray(layers), 'Graph has layers attribute'); + + assert.strictEqual(layers.length, 1, 'Graph has one default layer'); + + assert.strictEqual(layers[0].name, 'cells', 'Graph has default layer with name "cells"'); + + assert.ok(this.paper.getLayer('cells'), 'Paper has default layer view for "cells" layer'); + + const cellsLayerView = this.paper.getLayer('cells'); + + assert.equal(cellsLayerView.model, layers[0], 'Default layer view is linked to the default layer model'); }); hooks.afterEach(function() { From 1487770cc593c0eaf42428e8e161a562155c34f0 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Sun, 29 Jun 2025 18:34:12 +0200 Subject: [PATCH 31/54] update --- .../controllers/EmbeddingLayersController.mjs | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 8b4f1af7e0..7325e390ac 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -30,25 +30,26 @@ export class EmbeddingLayersController extends Listener { } }); - this.listenTo(graph, 'change:parent', (_appContext, cell, parentId) => { - const layersMap = graph.getLayersMap(); - - const currentLayer = cell.layer(); + this.listenTo(graph, 'add', (_appContext, cell) => { + const parentId = cell.get('parent'); - if (layersMap[currentLayer]) { - layersMap[currentLayer].remove(cell); + if (parentId) { + this.onParentChange(cell, parentId); } + }); - let layer; - if (parentId && !layersMap[parentId]) { - layer = new GraphLayer({ - name: parentId - }); - graph.addLayer(layer); - this.insertEmbeddingLayer(layer); - } + this.listenTo(graph, 'reset', (_appContext, { models: cells }) => { + cells.forEach(cell => { + const parentId = cell.get('parent'); + + if (parentId) { + this.onParentChange(cell, cell.get('parent')); + } + }); + }); - graph.addToLayer(cell, layersMap[parentId]); + this.listenTo(graph, 'change:parent', (_appContext, cell, parentId) => { + this.onParentChange(cell, parentId); }); this.listenTo(paper, 'cell:inserted', (_appContext, cellView) => { @@ -60,6 +61,28 @@ export class EmbeddingLayersController extends Listener { }); } + onParentChange(cell, parentId) { + const { graph } = this; + const layersMap = graph.getLayersMap(); + + const currentLayer = cell.layer(); + + if (layersMap[currentLayer]) { + layersMap[currentLayer].remove(cell); + } + + let layer; + if (parentId && !layersMap[parentId]) { + layer = new GraphLayer({ + name: parentId + }); + graph.addLayer(layer); + this.insertEmbeddingLayer(layer); + } + + graph.addToLayer(cell, layersMap[parentId]); + } + insertEmbeddingLayer(layer) { const { paper } = this; From bf9a4c8f1901b9050e2dba586dcace2ce7ced3f9 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Thu, 10 Jul 2025 13:11:06 +0200 Subject: [PATCH 32/54] wip --- packages/joint-core/src/dia/Graph.mjs | 12 ++++++++-- .../controllers/EmbeddingLayersController.mjs | 13 +++++------ .../dia/controllers/GraphLayersController.mjs | 22 +++++++++++++++++-- .../src/dia/layers/GraphLayerView.mjs | 1 + .../joint-core/test/jointjs/layers/basic.js | 3 ++- .../test/jointjs/layers/embedding.js | 13 ++++++----- packages/joint-core/types/joint.d.ts | 17 ++++++++++++-- 7 files changed, 61 insertions(+), 20 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index c788f81357..00ba46a288 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -438,8 +438,16 @@ export const Graph = Model.extend({ return this.layersController.getDefaultLayer(); }, - getLayersMap() { - return this.layersController.getLayersMap(); + getLayer(name) { + return this.layersController.getLayer(name); + }, + + hasLayer(name) { + return this.layersController.hasLayer(name); + }, + + getLayers() { + return this.layersController.getLayers(); }, getLayerCells(layerName) { diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 7325e390ac..41e2545db4 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -16,10 +16,8 @@ export class EmbeddingLayersController extends Listener { const { graph, paper } = this; this.listenTo(graph, 'remove', (_appContext, cell) => { - const layersMap = graph.getLayersMap(); - - if (layersMap[cell.id]) { - const layer = layersMap[cell.id]; + if (graph.hasLayer(cell.id)) { + const layer = graph.getLayer(cell.id); const cells = layer.get('cells').models; cells.forEach((cell) => { graph.addToLayer(cell); @@ -63,12 +61,11 @@ export class EmbeddingLayersController extends Listener { onParentChange(cell, parentId) { const { graph } = this; - const layersMap = graph.getLayersMap(); - const currentLayer = cell.layer(); + const currentLayerName = cell.layer(); - if (layersMap[currentLayer]) { - layersMap[currentLayer].remove(cell); + if (graph.hasLayer(currentLayerName)) { + graph.getLayer(currentLayerName).remove(cell); } let layer; diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 294054c21c..540239f997 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -151,8 +151,26 @@ export class GraphLayersController extends Listener { return layer.maxZIndex(); } - getLayersMap() { - return this.layersMap; + hasLayer(layerName) { + return !!this.layersMap[layerName]; + } + + getLayer(layerName) { + if (!this.layersMap[layerName]) { + throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); + } + + return this.layersMap[layerName]; + } + + getLayers() { + const layers = []; + + for (let layerName in this.layersMap) { + layers.push(this.layersMap[layerName]); + } + + return layers; } getLayerCells(layerName) { diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index b1531848fb..1f475b28a9 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -76,6 +76,7 @@ export class GraphLayerView extends LayerView { }); } + // TODO: make it work properly from inside of paper _prepareRemove() { const cellNodes = Array.from(this.el.children).filter(node => node.getAttribute('model-id')); const cells = this.model.get('cells'); diff --git a/packages/joint-core/test/jointjs/layers/basic.js b/packages/joint-core/test/jointjs/layers/basic.js index fbe0295279..5e39c20891 100644 --- a/packages/joint-core/test/jointjs/layers/basic.js +++ b/packages/joint-core/test/jointjs/layers/basic.js @@ -27,8 +27,9 @@ QUnit.module('layers-basic', function(hooks) { assert.ok(this.paper.getLayer('cells'), 'Paper has default layer view for "cells" layer'); const cellsLayerView = this.paper.getLayer('cells'); + const graphDefaultLayer = this.graph.getDefaultLayer(); - assert.equal(cellsLayerView.model, layers[0], 'Default layer view is linked to the default layer model'); + assert.equal(cellsLayerView.model, graphDefaultLayer, 'Default layer view is linked to the default layer model'); }); hooks.afterEach(function() { diff --git a/packages/joint-core/test/jointjs/layers/embedding.js b/packages/joint-core/test/jointjs/layers/embedding.js index 398ee84d62..1b567ec96a 100644 --- a/packages/joint-core/test/jointjs/layers/embedding.js +++ b/packages/joint-core/test/jointjs/layers/embedding.js @@ -14,15 +14,18 @@ QUnit.module('embedding-layers', function(hooks) { }); }); - QUnit.test('Embedding layers setup', function(assert) { - - assert.ok(this.paper.embeddingLayersController, 1, 'Controller is created'); - }); - hooks.afterEach(function() { this.paper.remove(); this.graph = null; this.paper = null; }); + + QUnit.test('Embedding layers setup', function(assert) { + assert.ok(this.paper.embeddingLayersController, 1, 'Controller is created'); + }); + + QUnit.test('Embedding layers setup', function(assert) { + assert.ok(this.paper.embeddingLayersController, 1, 'Controller is created'); + }); }); diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index ad5774f731..b1af1b0218 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -211,6 +211,12 @@ export namespace dia { getDefaultLayer(): GraphLayer; + getLayer(name: string): GraphLayer; + + hasLayer(name: string): boolean; + + getLayers(): GraphLayer[]; + getCell(id: Cell.ID | Cell): Cell; getElements(): Element[]; @@ -1975,12 +1981,12 @@ export namespace dia { namespace LayerView { - interface Options extends mvc.ViewOptions { + interface Options extends mvc.ViewOptions { name: string; } } - class LayerView extends mvc.View { + class LayerView extends mvc.View { constructor(opt?: LayerView.Options); @@ -2012,6 +2018,13 @@ export namespace dia { maxZIndex(): number; } + class GraphLayerView extends LayerView { + + protected sortLayer(): void; + + protected sortLayerExact(): void; + } + namespace ToolsView { interface Options extends mvc.ViewOptions { From 7864d01e9404efcf0bfd65845975107c3998a4bf Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 11 Jul 2025 12:35:04 +0200 Subject: [PATCH 33/54] update --- .../controllers/EmbeddingLayersController.mjs | 30 ++++++++------ .../src/dia/layers/GraphLayerView.mjs | 9 ++++- .../joint-core/test/jointjs/layers/basic.js | 14 +++---- .../test/jointjs/layers/embedding.js | 40 ++++++++++++++++--- 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 41e2545db4..537e8357ae 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -24,7 +24,7 @@ export class EmbeddingLayersController extends Listener { }); graph.removeLayer(layer); - this.removeEmbeddingLayer(layer); + this.removeLayerView(layer); } }); @@ -68,19 +68,25 @@ export class EmbeddingLayersController extends Listener { graph.getLayer(currentLayerName).remove(cell); } - let layer; - if (parentId && !layersMap[parentId]) { - layer = new GraphLayer({ - name: parentId - }); - graph.addLayer(layer); - this.insertEmbeddingLayer(layer); - } + if (parentId) { + let layer; + if (!graph.hasLayer(parentId)) { + layer = new GraphLayer({ + name: parentId + }); + graph.addLayer(layer); + this.insertLayerView(layer); + } else { + layer = graph.getLayer(parentId); + } - graph.addToLayer(cell, layersMap[parentId]); + graph.addToLayer(cell, layer); + } else { + graph.addToLayer(cell); + } } - insertEmbeddingLayer(layer) { + insertLayerView(layer) { const { paper } = this; const cellId = layer.name; @@ -93,7 +99,7 @@ export class EmbeddingLayersController extends Listener { } } - removeEmbeddingLayer(layer) { + removeLayerView(layer) { const { paper } = this; const cellId = layer.name; diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index 1f475b28a9..29205f65ad 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -76,6 +76,14 @@ export class GraphLayerView extends LayerView { }); } + getCellViewNode(cellId) { + const cellNode = this.el.querySelector(`[model-id="${cellId}"]`); + if (!cellNode) { + return null; + } + return cellNode; + } + // TODO: make it work properly from inside of paper _prepareRemove() { const cellNodes = Array.from(this.el.children).filter(node => node.getAttribute('model-id')); @@ -89,5 +97,4 @@ export class GraphLayerView extends LayerView { } }); } - } diff --git a/packages/joint-core/test/jointjs/layers/basic.js b/packages/joint-core/test/jointjs/layers/basic.js index 5e39c20891..92bf443298 100644 --- a/packages/joint-core/test/jointjs/layers/basic.js +++ b/packages/joint-core/test/jointjs/layers/basic.js @@ -13,6 +13,13 @@ QUnit.module('layers-basic', function(hooks) { }); }); + hooks.afterEach(function() { + + this.paper.remove(); + this.graph = null; + this.paper = null; + }); + QUnit.test('Default layers setup', function(assert) { assert.ok(this.graph.layersController, 'Graph layers controller is created'); @@ -31,11 +38,4 @@ QUnit.module('layers-basic', function(hooks) { assert.equal(cellsLayerView.model, graphDefaultLayer, 'Default layer view is linked to the default layer model'); }); - - hooks.afterEach(function() { - - this.paper.remove(); - this.graph = null; - this.paper = null; - }); }); diff --git a/packages/joint-core/test/jointjs/layers/embedding.js b/packages/joint-core/test/jointjs/layers/embedding.js index 1b567ec96a..65a0213024 100644 --- a/packages/joint-core/test/jointjs/layers/embedding.js +++ b/packages/joint-core/test/jointjs/layers/embedding.js @@ -1,6 +1,6 @@ QUnit.module('embedding-layers', function(hooks) { - hooks.beforeEach(function() { + hooks.beforeEach(() => { const fixtureEl = fixtures.getElement(); const paperEl = document.createElement('div'); @@ -8,24 +8,54 @@ QUnit.module('embedding-layers', function(hooks) { 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(function() { + hooks.afterEach(() => { this.paper.remove(); this.graph = null; this.paper = null; }); - QUnit.test('Embedding layers setup', function(assert) { + QUnit.test('Embedding layers setup', (assert) => { assert.ok(this.paper.embeddingLayersController, 1, 'Controller is created'); }); - QUnit.test('Embedding layers setup', function(assert) { - assert.ok(this.paper.embeddingLayersController, 1, 'Controller is created'); + QUnit.test('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' + } + ] + }); + + assert.ok(this.paper.hasLayer('rect1'), 'Paper has layer for parent cell'); + assert.ok(this.graph.hasLayer('rect1'), 'Graph has layer for parent cell'); + + const layer = this.graph.getLayer('rect1'); + + assert.ok(layer.get('cells').has('ellipse1'), 'Graph Layer has cell'); + + const layerView = this.paper.getLayer('rect1'); + + assert.ok(layerView.getCellViewNode('ellipse1'), 'Layer view has cell view node for embedded cell'); }); }); From 4d8ed05f4e6b843b2d398173397b8f5eaa8c8a19 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 11 Jul 2025 17:55:44 +0200 Subject: [PATCH 34/54] up --- packages/joint-core/src/dia/Cell.mjs | 8 ++++---- packages/joint-core/src/dia/Graph.mjs | 16 ++++++++++------ packages/joint-core/src/dia/Paper.mjs | 2 +- .../dia/controllers/GraphLayersController.mjs | 4 ++-- .../joint-core/src/dia/layers/GraphLayer.mjs | 2 +- .../joint-core/src/dia/layers/GraphLayerView.mjs | 10 +++++----- packages/joint-core/test/jointjs/basic.js | 2 +- packages/joint-core/test/jointjs/dia/Paper.js | 2 +- packages/joint-core/test/jointjs/paper.js | 8 ++++---- packages/joint-core/types/joint.d.ts | 10 +++++++--- 10 files changed, 36 insertions(+), 28 deletions(-) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index 7f87eb3287..1471f39e8c 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -266,9 +266,9 @@ export const Cell = Model.extend({ const maxZ = graph.maxZIndex(layerName); let z = maxZ - cells.length + 1; - const collection = graph.getLayerCells(layerName); + const layerCells = graph.getLayerCells(layerName); - 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; @@ -310,9 +310,9 @@ export const Cell = Model.extend({ let z = graph.minZIndex(layerName); - const collection = graph.getLayerCells(layerName); + const layerCells = graph.getLayerCells(layerName); - 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; diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 00ba46a288..d7336cde2b 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -67,14 +67,12 @@ const GraphCells = Collection.extend({ export const Graph = Model.extend({ + defaultLayerName: 'cells', + initialize: function(attrs, opt) { opt = opt || {}; - this.enableCellLayers = opt.enableCellLayers || false; - - this.defaultLayerName = 'cells'; - const defaultLayer = new GraphLayer({ name: this.defaultLayerName }); @@ -475,14 +473,20 @@ export const Graph = Model.extend({ return this.getCells().filter(cell => cell.isLink()); }, + // @deprecated getFirstCell: function() { - return this.getCells().first(); + const cells = this.getCells(); + + return cells[0]; }, + // @deprecated getLastCell: function() { - return this.getCells().last(); + const cells = this.getCells(); + + 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 f31a318cb1..227b795b90 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -1932,7 +1932,7 @@ export const Paper = View.extend({ sortLayersExact: function() { const { _layers: { viewsMap }} = this; - Object.values(viewsMap).filter(view => view instanceof GraphLayerView).forEach(view => view.sortLayerExact()); + Object.values(viewsMap).filter(view => view instanceof GraphLayerView).forEach(view => view.sortExact()); }, insertView: function(view, isInitialInsert) { diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 540239f997..b13b783a15 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -38,7 +38,7 @@ export class GraphLayersController extends Listener { const { layersMap } = this; for (let layerName in layersMap) { - layersMap[layerName].clear(); + layersMap[layerName].reset(); } cells.forEach(cell => { @@ -174,7 +174,7 @@ export class GraphLayersController extends Listener { } getLayerCells(layerName) { - return this.layersMap[layerName].get('cells'); + return this.layersMap[layerName].get('cells').toArray(); } getCells() { diff --git a/packages/joint-core/src/dia/layers/GraphLayer.mjs b/packages/joint-core/src/dia/layers/GraphLayer.mjs index 5fa225ade5..2145fc196d 100644 --- a/packages/joint-core/src/dia/layers/GraphLayer.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayer.mjs @@ -45,7 +45,7 @@ export class GraphLayer extends Model { this.get('cells').remove(cell); } - clear() { + reset() { this.get('cells').reset(); } diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index 29205f65ad..975a4d1200 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -31,7 +31,7 @@ export class GraphLayerView extends LayerView { this.listenTo(model, 'sort', () => { if (graph.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return; - this.sortLayer(); + this.sort(); }); this.listenTo(graph, 'batch:stop', (data) => { @@ -39,12 +39,12 @@ export class GraphLayerView extends LayerView { const sortDelayingBatches = this.SORT_DELAYING_BATCHES; if (sortDelayingBatches.includes(name) && !graph.hasActiveBatch(sortDelayingBatches)) { - this.sortLayer(); + this.sort(); } }); } - sortLayer() { + sort() { const { options: { paper } } = this; if (!paper) return; @@ -58,10 +58,10 @@ export class GraphLayerView extends LayerView { paper._updates.sort = true; return; } - this.sortLayerExact(); + this.sortExact(); } - sortLayerExact() { + 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')); diff --git a/packages/joint-core/test/jointjs/basic.js b/packages/joint-core/test/jointjs/basic.js index b073651685..3f6f93a57a 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.getLayer('cells'), 'sortLayer'); + var spy = sinon.spy(this.paper.getLayer('cells'), 'sort'); var r1View = this.paper.findViewByModel(r1); var r2View = this.paper.findViewByModel(r2); diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js index 9fb2f59496..4e198f34bd 100644 --- a/packages/joint-core/test/jointjs/dia/Paper.js +++ b/packages/joint-core/test/jointjs/dia/Paper.js @@ -977,7 +977,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { rect3.translate(10, 10); assert.ok(sortLayersExactSpy.notCalled); // ADD CELLS - var sortLayerExactSpy = sinon.spy(paper.getLayer('cells'), 'sortLayerExact'); + var sortLayerExactSpy = sinon.spy(paper.getLayer('cells'), 'sortExact'); graph.clear(); graph.addCells([rect1, rect2, rect3]); assert.equal(sortLayerExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0); diff --git a/packages/joint-core/test/jointjs/paper.js b/packages/joint-core/test/jointjs/paper.js index 51fefda631..5a4bfc86f6 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 sortLayer()', function(assert) { + QUnit.test('paper.addCell() number of sort()', function(assert) { - var spy = sinon.spy(this.paper.getLayer('cells'), 'sortLayer'); + var spy = sinon.spy(this.paper.getLayer('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 sortLayer()', function(assert) { + QUnit.test('paper.addCells() number of sort()', function(assert) { - var spy = sinon.spy(this.paper.getLayer('cells'), 'sortLayer'); + var spy = sinon.spy(this.paper.getLayer('cells'), 'sort'); var r1 = new joint.shapes.standard.Rectangle; var r2 = new joint.shapes.standard.Rectangle; diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index b1af1b0218..a0d65e5fd2 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -2011,18 +2011,22 @@ export namespace dia { remove(cell: Cell): void; - //clear(): void; + reset(): void; minZIndex(): number; maxZIndex(): number; + + addToGraph(graph: Graph): void; + + removeFromGraph(): void; } class GraphLayerView extends LayerView { - protected sortLayer(): void; + protected sort(): void; - protected sortLayerExact(): void; + protected sortExact(): void; } namespace ToolsView { From 4152946eda121e4bfcaf39c4c8c16199816c9b30 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 14 Jul 2025 12:16:28 +0200 Subject: [PATCH 35/54] refactor --- packages/joint-core/src/dia/Paper.mjs | 19 +++---- .../dia/controllers/GraphLayersController.mjs | 4 -- .../joint-core/src/dia/layers/GraphLayer.mjs | 10 ---- .../src/dia/layers/GraphLayerView.mjs | 38 +++++--------- .../src/dia/layers/GridLayerView.mjs | 41 +++++++--------- .../joint-core/src/dia/layers/LayerView.mjs | 49 +++++++++---------- packages/joint-core/test/jointjs/index.html | 5 +- packages/joint-core/types/joint.d.ts | 4 -- 8 files changed, 68 insertions(+), 102 deletions(-) diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 227b795b90..c4278c2898 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -403,6 +403,7 @@ export const Paper = View.extend({ this._layersSettings = [{ type: 'GridLayerView', name: LayersNames.GRID, + patterns: this.constructor.gridPatterns }, { name: LayersNames.BACK, }, { @@ -778,30 +779,30 @@ export const Paper = View.extend({ V(this.svg).prepend(V.createSVGStyle(css)); }, - createLayer(attributes) { - attributes.paper = this; + createLayer(options) { + options.paper = this; - let type = attributes.type; + let type = options.type; - if (attributes.model) { - const modelType = attributes.model.get('type') || attributes.model.constructor.name; + 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(attributes); + return new viewConstructor(options); }, - renderLayer: function(attributes) { - const layerView = this.createLayer(attributes); + renderLayer: function(options) { + const layerView = this.createLayer(options); this.addLayer(layerView); return layerView; }, renderLayers: function(layers) { this.removeLayers(); - layers.forEach(attributes => this.renderLayer(attributes)); + layers.forEach(options => this.renderLayer(options)); // Throws an exception if doesn't exist const cellsLayerView = this.getLayer(LayersNames.CELLS); const toolsLayerView = this.getLayer(LayersNames.TOOLS); diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index b13b783a15..e3f1a68b21 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -105,7 +105,6 @@ export class GraphLayersController extends Listener { this.layers = this.layers.concat([layer]); - layer.addToGraph(this.graph); layersMap[layer.name] = layer; this.graph.set('layers', this.layers); @@ -124,9 +123,6 @@ export class GraphLayersController extends Listener { this.layers = this.layers.filter(l => l.name !== layerName); - const layer = layersMap[layerName]; - layer.removeFromGraph(); - delete this.layersMap[layerName]; this.graph.set('layers', this.layers); } diff --git a/packages/joint-core/src/dia/layers/GraphLayer.mjs b/packages/joint-core/src/dia/layers/GraphLayer.mjs index 2145fc196d..16c1a93997 100644 --- a/packages/joint-core/src/dia/layers/GraphLayer.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayer.mjs @@ -58,14 +58,4 @@ export class GraphLayer extends Model { const lastCell = this.get('cells').last(); return lastCell ? (lastCell.get('z') || 0) : 0; } - - addToGraph(graph) { - this.graph = graph; - this.trigger('addToGraph', this.graph); - } - - removeFromGraph() { - this.graph = null; - this.trigger('removeFromGraph'); - } } diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index 975a4d1200..5de98880d1 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -1,30 +1,18 @@ import { LayerView } from './LayerView.mjs'; import { sortElements } from '../../util/index.mjs'; -export class GraphLayerView extends LayerView { +export const GraphLayerView = LayerView.extend({ - init(...args) { - super.init(...args); + SORT_DELAYING_BATCHES: ['add', 'reset', 'to-front', 'to-back'], - const { model } = this; - - if (model.graph) { - this.startListening(model.graph); - } + init() { + LayerView.prototype.init.apply(this, arguments); - this.listenTo(model, 'addedToGraph', (graph) => { - this.stopListening(); - this.startListening(graph); - }); - - this.listenTo(model, 'removedFromGraph', () => { - this.stopListening(); - }); - } + const { options: { paper } } = this; + const graph = paper.model; - get SORT_DELAYING_BATCHES() { - return ['add', 'reset', 'to-front', 'to-back']; - } + this.startListening(graph); + }, startListening(graph) { const { model } = this; @@ -42,7 +30,7 @@ export class GraphLayerView extends LayerView { this.sort(); } }); - } + }, sort() { const { options: { paper } } = this; @@ -59,7 +47,7 @@ export class GraphLayerView extends LayerView { return; } this.sortExact(); - } + }, sortExact() { // Run insertion sort algorithm in order to efficiently sort DOM elements according to their @@ -74,7 +62,7 @@ export class GraphLayerView extends LayerView { const zB = cellB.attributes.z || 0; return (zA === zB) ? 0 : (zA < zB) ? -1 : 1; }); - } + }, getCellViewNode(cellId) { const cellNode = this.el.querySelector(`[model-id="${cellId}"]`); @@ -82,7 +70,7 @@ export class GraphLayerView extends LayerView { return null; } return cellNode; - } + }, // TODO: make it work properly from inside of paper _prepareRemove() { @@ -97,4 +85,4 @@ export class GraphLayerView extends LayerView { } }); } -} +}); diff --git a/packages/joint-core/src/dia/layers/GridLayerView.mjs b/packages/joint-core/src/dia/layers/GridLayerView.mjs index a703dd40ce..e147107804 100644 --- a/packages/joint-core/src/dia/layers/GridLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GridLayerView.mjs @@ -9,32 +9,27 @@ import { } from '../../util/index.mjs'; import V from '../../V/index.mjs'; -export class GridLayerView extends LayerView { +export const GridLayerView = LayerView.extend({ - preinitialize() { - super.preinitialize(); + style: { + 'pointer-events': 'none' + }, - this.style = { - 'pointer-events': 'none' - } - } - - init(...args) { - super.init(...args); + _gridCache: null, + _gridSettings: null, + init() { + LayerView.prototype.init.apply(this, arguments); const { options: { paper }} = this; - - this.options.patterns = paper.constructor.gridPatterns; - this._gridCache = null; this._gridSettings = []; this.listenTo(paper, 'transform resize', this.updateGrid); - } + }, setGrid(drawGrid) { this._gridSettings = this.getGridSettings(drawGrid); this.renderGrid(); - } + }, getGridSettings(drawGrid) { const gridSettings = []; @@ -45,14 +40,14 @@ export class GridLayerView extends LayerView { }); } return gridSettings; - } + }, removeGrid() { const { _gridCache: grid } = this; if (!grid) return; grid.root.remove(); this._gridCache = null; - } + }, renderGrid() { @@ -97,7 +92,7 @@ export class GridLayerView extends LayerView { refs.root.appendTo(this.el); this.updateGrid(); - } + }, updateGrid() { @@ -116,11 +111,11 @@ export class GridLayerView extends LayerView { options.update(vPattern.node.firstChild, options, paper); } }); - } + }, _getPatternId(index) { return `pattern_${this.options.paper.cid}_${index}`; - } + }, _getGridRefs() { let { _gridCache: grid } = this; @@ -144,7 +139,7 @@ export class GridLayerView extends LayerView { } }; return grid; - } + }, _resolveDrawGridOption(opt) { @@ -180,11 +175,11 @@ export class GridLayerView extends LayerView { } return isArray ? options : [options]; - } + }, isEmpty() { const { _gridCache: grid } = this; return this.el.children.length === (grid ? 1 : 0); } -} +}); diff --git a/packages/joint-core/src/dia/layers/LayerView.mjs b/packages/joint-core/src/dia/layers/LayerView.mjs index 57f9b204ff..50508ea8b4 100644 --- a/packages/joint-core/src/dia/layers/LayerView.mjs +++ b/packages/joint-core/src/dia/layers/LayerView.mjs @@ -1,32 +1,36 @@ import { View } from '../../mvc/index.mjs'; import { addClassNamePrefix } from '../../util/util.mjs'; -export class LayerView extends View { +export const LayerView = View.extend({ - preinitialize() { - this.tagName = 'g'; - this.svgElement = true; + tagName: 'g', + svgElement: true, + pivotNodes: null, + defaultTheme: null, + + init: function() { this.pivotNodes = {}; - } + this.name = this.options.name || ''; + }, - className() { + className: function() { const { name } = this.options; if (!name) return null; return addClassNamePrefix(`${name}-layer`); - } + }, - insertSortedNode(node, z) { + insertSortedNode: function(node, z) { this.el.insertBefore(node, this.insertPivot(z)); - } + }, - insertNode(node) { + insertNode: function(node) { const { el } = this; if (node.parentNode !== el) { el.appendChild(node); } - } + }, - insertPivot(z) { + insertPivot: function(z) { const { el, pivotNodes } = this; z = +z; z || (z = 0); @@ -50,21 +54,16 @@ export class LayerView extends View { el.insertBefore(pivotNode, el.firstChild); } return pivotNode; - } + }, - removePivots() { + removePivots: function() { const { el, pivotNodes } = this; - for (let z in pivotNodes) { - el.removeChild(pivotNodes[z]); - } + for (let z in pivotNodes) el.removeChild(pivotNodes[z]); this.pivotNodes = {}; - } + }, - isEmpty() { + isEmpty: function() { + // Check if the layer has any child elements (pivot comments are not counted). return this.el.children.length === 0; - } - - get name() { - return this.options.name; - } -} + }, +}); diff --git a/packages/joint-core/test/jointjs/index.html b/packages/joint-core/test/jointjs/index.html index 3b05711a2d..df5131727f 100644 --- a/packages/joint-core/test/jointjs/index.html +++ b/packages/joint-core/test/jointjs/index.html @@ -13,7 +13,8 @@ - + + diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index a0d65e5fd2..521c95d27f 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -2016,10 +2016,6 @@ export namespace dia { minZIndex(): number; maxZIndex(): number; - - addToGraph(graph: Graph): void; - - removeFromGraph(): void; } class GraphLayerView extends LayerView { From 2c6c5d92100c3e303b7821ba35a77b34f71ab520 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 14 Jul 2025 13:39:38 +0200 Subject: [PATCH 36/54] up --- packages/joint-core/src/dia/Cell.mjs | 31 +++++++++++-------- .../controllers/EmbeddingLayersController.mjs | 14 ++++++--- .../dia/controllers/GraphLayersController.mjs | 8 ++--- .../joint-core/src/dia/layers/GraphLayer.mjs | 3 +- packages/joint-core/types/joint.d.ts | 3 ++ 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index 1471f39e8c..f4e65fd111 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -948,24 +948,29 @@ export const Cell = Model.extend({ .difference(this.position()); }, - setLayer(layerName) { - if (layerName) { - this.set('layer', layerName); + layer: function(layerName, opt) { + // if strictly null unset the layer + if (layerName === null) { + return this.unset('layer', opt); } - }, - - unsetLayer() { - this.unset('layer'); - }, - layer() { - let layer = this.get('layer') || null; - if (layer == null && this.graph) { + // if undefined return the current layer name + if (layerName === undefined) { + let layer = this.get('layer') || null; // If the cell is part of a graph, use the graph's default layer. - layer = this.graph.getDefaultLayer().name; + if (layer == null && this.graph) { + layer = this.graph.getDefaultLayer().name; + } + + return layer; + } + + // otherwise set the layer name + if (!isString(layerName)) { + throw new Error('Layer name must be a string.'); } - return layer; + return this.set('layer', layerName, opt); } }, { diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 537e8357ae..0a15583d9a 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -15,7 +15,7 @@ export class EmbeddingLayersController extends Listener { startListening() { const { graph, paper } = this; - this.listenTo(graph, 'remove', (_appContext, cell) => { + this.listenTo(graph, 'remove', (_context, cell) => { if (graph.hasLayer(cell.id)) { const layer = graph.getLayer(cell.id); const cells = layer.get('cells').models; @@ -28,7 +28,7 @@ export class EmbeddingLayersController extends Listener { } }); - this.listenTo(graph, 'add', (_appContext, cell) => { + this.listenTo(graph, 'add', (_context, cell) => { const parentId = cell.get('parent'); if (parentId) { @@ -36,7 +36,7 @@ export class EmbeddingLayersController extends Listener { } }); - this.listenTo(graph, 'reset', (_appContext, { models: cells }) => { + this.listenTo(graph, 'reset', (_context, { models: cells }) => { cells.forEach(cell => { const parentId = cell.get('parent'); @@ -46,11 +46,11 @@ export class EmbeddingLayersController extends Listener { }); }); - this.listenTo(graph, 'change:parent', (_appContext, cell, parentId) => { + this.listenTo(graph, 'change:parent', (_context, cell, parentId) => { this.onParentChange(cell, parentId); }); - this.listenTo(paper, 'cell:inserted', (_appContext, cellView) => { + this.listenTo(paper, 'cell:inserted', (_context, cellView) => { const cellId = cellView.model.id; if (paper.hasLayer(cellId)) { const layerView = paper.getLayer(cellId); @@ -70,6 +70,7 @@ export class EmbeddingLayersController extends Listener { if (parentId) { let layer; + // Create new layer if it's not exist if (!graph.hasLayer(parentId)) { layer = new GraphLayer({ name: parentId @@ -91,10 +92,13 @@ export class EmbeddingLayersController extends Listener { const cellId = layer.name; const layerView = paper.createLayer({ name: cellId, model: layer }); + // Do not append to the DOM immediately + // Layer will be appended with the correspondent container paper.addLayer(layerView, { doNotAppend: true }); const cellView = paper.findViewByModel(cellId); if (cellView.isMounted()) { + // Append the layer if the container is already mounted cellView.el.after(layerView.el); } } diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index e3f1a68b21..47f643043e 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -26,15 +26,15 @@ export class GraphLayersController extends Listener { startListening() { const { graph } = this; - this.listenTo(graph, 'add', (_appContext, cell) => { + this.listenTo(graph, 'add', (_context, cell) => { this.onAdd(cell); }); - this.listenTo(graph, 'remove', (_appContext, cell) => { + this.listenTo(graph, 'remove', (_context, cell) => { this.onRemove(cell); }); - this.listenTo(graph, 'reset', (_appContext, { models: cells }) => { + this.listenTo(graph, 'reset', (_context, { models: cells }) => { const { layersMap } = this; for (let layerName in layersMap) { @@ -92,7 +92,7 @@ export class GraphLayersController extends Listener { layer.add(cell); } else { layer.add(cell); - cell.setLayer(layer.name); + cell.layer(layer.name); } } diff --git a/packages/joint-core/src/dia/layers/GraphLayer.mjs b/packages/joint-core/src/dia/layers/GraphLayer.mjs index 16c1a93997..eae1229e12 100644 --- a/packages/joint-core/src/dia/layers/GraphLayer.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayer.mjs @@ -41,7 +41,8 @@ export class GraphLayer extends Model { } remove(cell) { - cell.unsetLayer(); + // unsets the layer making it default for the purpose of the DOM location + cell.layer(null); this.get('cells').remove(cell); } diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 521c95d27f..50c2e9c977 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -533,6 +533,9 @@ export namespace dia { z(): number; + layer(): string; + layer(name: string | null, opt?: Graph.Options): this; + angle(): number; getBBox(): g.Rect; From 29ca501d09506f77e8ca83d22d6ee026c49d5f8e Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 14 Jul 2025 13:44:25 +0200 Subject: [PATCH 37/54] revert --- packages/joint-core/test/jointjs/index.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/joint-core/test/jointjs/index.html b/packages/joint-core/test/jointjs/index.html index df5131727f..3b05711a2d 100644 --- a/packages/joint-core/test/jointjs/index.html +++ b/packages/joint-core/test/jointjs/index.html @@ -13,8 +13,7 @@ - - + From ec9a33a22faea759951948ebc10f5d6eaeb0f2a0 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 14 Jul 2025 13:47:30 +0200 Subject: [PATCH 38/54] up --- packages/joint-core/src/dia/layers/LayerView.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/joint-core/src/dia/layers/LayerView.mjs b/packages/joint-core/src/dia/layers/LayerView.mjs index 50508ea8b4..3897cbaa3d 100644 --- a/packages/joint-core/src/dia/layers/LayerView.mjs +++ b/packages/joint-core/src/dia/layers/LayerView.mjs @@ -8,9 +8,13 @@ export const LayerView = View.extend({ pivotNodes: null, defaultTheme: null, + options: { + name: '' + }, + init: function() { this.pivotNodes = {}; - this.name = this.options.name || ''; + this.name = this.options.name; }, className: function() { From 505fc5040a35b15fbd79641b0dae470a2ee2e0df Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 15 Jul 2025 14:39:05 +0200 Subject: [PATCH 39/54] up --- packages/joint-core/src/dia/Graph.mjs | 4 ---- .../controllers/EmbeddingLayersController.mjs | 17 ++++------------ .../dia/controllers/GraphLayersController.mjs | 20 +++++++++---------- .../joint-core/src/dia/layers/GraphLayer.mjs | 7 +++++++ 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index d7336cde2b..d876fe4321 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -304,10 +304,6 @@ export const Graph = Model.extend({ return this.layersController.maxZIndex(layerName); }, - addToLayer: function(cell, layer) { - this.layersController.addToLayer(cell, layer); - }, - addCell: function(cell, opt) { if (Array.isArray(cell)) { diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 0a15583d9a..8cefb63dbe 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -20,7 +20,7 @@ export class EmbeddingLayersController extends Listener { const layer = graph.getLayer(cell.id); const cells = layer.get('cells').models; cells.forEach((cell) => { - graph.addToLayer(cell); + cell.layer(null); // Move all cells to the default layer }); graph.removeLayer(layer); @@ -62,28 +62,19 @@ export class EmbeddingLayersController extends Listener { onParentChange(cell, parentId) { const { graph } = this; - const currentLayerName = cell.layer(); - - if (graph.hasLayer(currentLayerName)) { - graph.getLayer(currentLayerName).remove(cell); - } - if (parentId) { - let layer; // Create new layer if it's not exist if (!graph.hasLayer(parentId)) { - layer = new GraphLayer({ + const layer = new GraphLayer({ name: parentId }); graph.addLayer(layer); this.insertLayerView(layer); - } else { - layer = graph.getLayer(parentId); } - graph.addToLayer(cell, layer); + cell.layer(parentId); // Set the layer for the cell } else { - graph.addToLayer(cell); + cell.layer(null); // Move to the default layer } } diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs index 47f643043e..58f7d6ba25 100644 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs @@ -45,6 +45,16 @@ export class GraphLayersController extends Listener { this.onAdd(cell, true); }); }); + + this.listenTo(graph, 'change:layer', (_context, cell, layerName) => { + if (!layerName) { + layerName = this.defaultLayerName; + } + + if (this.hasLayer(layerName)) { + this.layersMap[layerName].add(cell); + } + }); } onAdd(cell, reset = false) { @@ -86,16 +96,6 @@ export class GraphLayersController extends Listener { return this.layersMap[this.defaultLayerName]; } - addToLayer(cell, layer) { - if (!layer) { - layer = this.getDefaultLayer(); - layer.add(cell); - } else { - layer.add(cell); - cell.layer(layer.name); - } - } - addLayer(layer, _opt) { const { layersMap } = this; diff --git a/packages/joint-core/src/dia/layers/GraphLayer.mjs b/packages/joint-core/src/dia/layers/GraphLayer.mjs index eae1229e12..4f589c2b93 100644 --- a/packages/joint-core/src/dia/layers/GraphLayer.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayer.mjs @@ -31,6 +31,13 @@ export class GraphLayer extends Model { cells.sort(); }); + cells.on('change:layer', (cell, layerName) => { + // If the cell's layer is changed, we need to remove it from this layer. + if (layerName !== this.name) { + this.get('cells').remove(cell); + } + }); + // Make all the events fired in the `cells` collection available. // to the outside world. cells.on('all', this.trigger, this); From 6eb29f1604effd6367fe2b950af83438c6a1f0bb Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 16 Jul 2025 18:48:04 +0200 Subject: [PATCH 40/54] wip --- packages/joint-core/src/dia/Graph.mjs | 6 --- packages/joint-core/src/dia/Paper.mjs | 29 +------------- .../controllers/EmbeddingLayersController.mjs | 5 +-- .../joint-core/src/dia/layers/GraphLayer.mjs | 23 +++++++---- .../src/dia/layers/GraphLayerView.mjs | 38 ++++++++++++++++--- 5 files changed, 51 insertions(+), 50 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index d876fe4321..4d1dfa5c02 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -56,12 +56,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; - } }); diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index c4278c2898..18165481e6 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -57,7 +57,7 @@ export const LayersNames = { LABELS: 'labels' }; -const sortingTypes = { +export const sortingTypes = { NONE: 'sorting-none', APPROX: 'sorting-approximate', EXACT: 'sorting-exact' @@ -365,7 +365,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. @@ -470,7 +469,6 @@ 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, 'batch:stop', this.onGraphBatchStop); @@ -495,17 +493,6 @@ 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); @@ -675,10 +662,6 @@ export const Paper = View.extend({ const layerView = this._requireLayerView(layer); - if (layerView instanceof GraphLayerView) { - layerView._prepareRemove(); - } - if (!layerView.isEmpty()) { throw new Error('dia.Paper: The layer is not empty.'); } @@ -1942,15 +1925,7 @@ export const Paper = View.extend({ const layerName = model.layer(); const layerView = this.getLayer(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); diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 8cefb63dbe..5d898869ea 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -18,10 +18,7 @@ export class EmbeddingLayersController extends Listener { this.listenTo(graph, 'remove', (_context, cell) => { if (graph.hasLayer(cell.id)) { const layer = graph.getLayer(cell.id); - const cells = layer.get('cells').models; - cells.forEach((cell) => { - cell.layer(null); // Move all cells to the default layer - }); + layer.reset(); graph.removeLayer(layer); this.removeLayerView(layer); diff --git a/packages/joint-core/src/dia/layers/GraphLayer.mjs b/packages/joint-core/src/dia/layers/GraphLayer.mjs index 4f589c2b93..77f69992db 100644 --- a/packages/joint-core/src/dia/layers/GraphLayer.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayer.mjs @@ -27,14 +27,16 @@ export class GraphLayer extends Model { const cells = new LayerCells(); this.set('cells', cells); - cells.on('change:z', () => { + cells.on('change:z', (cell, _, opt) => { cells.sort(); + + this.trigger('updateCell', cell, opt); }); cells.on('change:layer', (cell, layerName) => { // If the cell's layer is changed, we need to remove it from this layer. if (layerName !== this.name) { - this.get('cells').remove(cell); + this.cells.remove(cell); } }); @@ -44,26 +46,33 @@ export class GraphLayer extends Model { } add(cell) { - this.get('cells').add(cell); + this.cells.add(cell); + this.trigger('updateCell', cell); } remove(cell) { // unsets the layer making it default for the purpose of the DOM location cell.layer(null); - this.get('cells').remove(cell); + this.cells.remove(cell); } reset() { - this.get('cells').reset(); + this.cells.toArray().forEach(cell => { + this.remove(cell); + }); } minZIndex() { - const firstCell = this.get('cells').first(); + const firstCell = this.cells.first(); return firstCell ? (firstCell.get('z') || 0) : 0; } maxZIndex() { - const lastCell = this.get('cells').last(); + const lastCell = this.cells.last(); return lastCell ? (lastCell.get('z') || 0) : 0; } + + get cells() { + return this.get('cells'); + } } diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index 5de98880d1..e5b7ecdd22 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -1,5 +1,6 @@ import { LayerView } from './LayerView.mjs'; import { sortElements } from '../../util/index.mjs'; +import { sortingTypes } from '../Paper.mjs'; export const GraphLayerView = LayerView.extend({ @@ -8,14 +9,12 @@ export const GraphLayerView = LayerView.extend({ init() { LayerView.prototype.init.apply(this, arguments); - const { options: { paper } } = this; - const graph = paper.model; - - this.startListening(graph); + this.startListening(); }, - startListening(graph) { - const { model } = this; + startListening() { + const { model, options: { paper } } = this; + const graph = paper.model; this.listenTo(model, 'sort', () => { if (graph.hasActiveBatch(this.SORT_DELAYING_BATCHES)) return; @@ -30,6 +29,18 @@ export const GraphLayerView = LayerView.extend({ this.sort(); } }); + + this.listenTo(model, 'updateCell', (cell, opt) => { + if ( + cell.hasChanged('layer') || + (cell.hasChanged('z') && paper.options.sorting === sortingTypes.APPROX) + ) { + const view = paper.findViewByModel(cell); + if (view) { + paper.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt); + } + } + }); }, sort() { @@ -64,6 +75,21 @@ export const GraphLayerView = LayerView.extend({ }); }, + 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; + } + }, + getCellViewNode(cellId) { const cellNode = this.el.querySelector(`[model-id="${cellId}"]`); if (!cellNode) { From a94376e3b73653c7601c7fa0bf611d1dd8ef7c70 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Thu, 17 Jul 2025 12:52:50 +0200 Subject: [PATCH 41/54] async removing --- packages/joint-core/src/dia/Paper.mjs | 97 ++++++++++++++----- .../controllers/EmbeddingLayersController.mjs | 41 +++----- .../src/dia/layers/GraphLayerView.mjs | 14 --- .../joint-core/src/dia/layers/LayerView.mjs | 2 + 4 files changed, 85 insertions(+), 69 deletions(-) diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 18165481e6..abd2352eab 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -427,6 +427,9 @@ export const Paper = View.extend({ this._setDimensions(); this.startListening(); + this._graphLayers = []; + this.updateGraphLayers(this.model.get('layers')); + if (options.useLayersForEmbedding) { this.embeddingLayersController = new EmbeddingLayersController({ graph: model, paper: this }); } @@ -470,7 +473,8 @@ export const Paper = View.extend({ this.listenTo(model, 'add', this.onCellAdded) .listenTo(model, 'remove', this.onCellRemoved) .listenTo(model, 'reset', this.onGraphReset) - .listenTo(model, 'batch:stop', this.onGraphBatchStop); + .listenTo(model, 'batch:stop', this.onGraphBatchStop) + .listenTo(model, 'change:layers', this.onLayersChange); this.on('cell:highlight', this.onCellHighlight) .on('cell:unhighlight', this.onCellUnhighlight) @@ -510,6 +514,26 @@ export const Paper = View.extend({ } }, + onLayersChange: function(_, layers) { + this.updateGraphLayers(layers) + }, + + updateGraphLayers: function(layers) { + const removedLayerNames = this._graphLayers.filter(layer => !layers.some(l => l.name === layer.name)).map(layer => layer.name); + removedLayerNames.forEach(layerName => this.requestLayerRemove(layerName)); + + this._graphLayers = this.model.get('layers'); + + this._graphLayers.forEach(layer => { + if (!this.hasLayer(layer.name)) { + this.renderLayer({ + name: layer.name, + model: layer + }); + } + }); + }, + cloneOptions: function() { const { options } = this; @@ -618,24 +642,18 @@ export const Paper = View.extend({ _unregisterLayer(layerView) { const { _layers: { viewsMap, order }} = this; const layerName = layerView.name; + if (order.indexOf(layerName) !== -1) { order.splice(order.indexOf(layerName), 1); } + delete viewsMap[layerName]; }, - _registerLayer(layerView, beforeLayerView, ignoreOrder) { - const { _layers: { viewsMap, order }} = this; + _registerLayer(layerView) { + const { _layers: { viewsMap }} = this; const layerName = layerView.name; - if (!ignoreOrder) { - if (beforeLayerView) { - const beforeLayerName = beforeLayerView.name; - order.splice(order.indexOf(beforeLayerName), 0, layerName); - } else { - order.push(layerName); - } - } viewsMap[layerName] = layerView; }, @@ -668,7 +686,19 @@ export const Paper = View.extend({ this._removeLayer(layerView); }, - addLayer(layerView, options = {}) { + requestLayerRemove(layer) { + if (!layer) { + throw new Error('dia.Paper: The layer view must be provided.'); + } + + const layerView = this._requireLayerView(layer); + + const { FLAG_REMOVE, UPDATE_PRIORITY } = layerView; + + this.requestViewUpdate(layerView, FLAG_REMOVE, UPDATE_PRIORITY); + }, + + addLayer(layerView) { if (!layerView) { throw new Error('dia.Paper: The layer view must be provided.'); } @@ -684,18 +714,26 @@ export const Paper = View.extend({ if (!(layerView instanceof LayerView)) { throw new Error('dia.Paper: The layer view is not an instance of dia.LayerView.'); } - const { insertBefore, doNotAppend } = options; - if (doNotAppend) { - this._registerLayer(layerView, null, true); + + this._registerLayer(layerView); + }, + + insertLayer(layer, insertBefore) { + if (!this.hasLayer(layer.name)) { + this.addLayer(layer); + } + + const { _layers: { order }} = this; + const layerName = layer.name; + + if (!insertBefore) { + order.push(layerName); + this.layers.appendChild(layer.el); } else { - if (!insertBefore) { - this._registerLayer(layerView, null); - this.layers.appendChild(layerView.el); - } else { - const beforeLayerView = this._requireLayerView(insertBefore); - this._registerLayer(layerView, beforeLayerView); - this.layers.insertBefore(layerView.el, beforeLayerView.el); - } + const beforeLayerView = this._requireLayerView(insertBefore); + const beforeLayerName = beforeLayerView.name; + order.splice(order.indexOf(beforeLayerName), 0, layerName); + this.layers.insertBefore(layer.el, beforeLayerView.el); } }, @@ -714,7 +752,7 @@ export const Paper = View.extend({ return; } this._unregisterLayer(layer); - this.addLayer(layer, { insertBefore }); + this.insertLayer(layer, insertBefore); }, getLayerNames() { @@ -785,7 +823,10 @@ export const Paper = View.extend({ renderLayers: function(layers) { this.removeLayers(); - layers.forEach(options => this.renderLayer(options)); + layers.forEach(options => { + const layerView = this.renderLayer(options); + this.insertLayer(layerView); + }); // Throws an exception if doesn't exist const cellsLayerView = this.getLayer(LayersNames.CELLS); const toolsLayerView = this.getLayer(LayersNames.TOOLS); @@ -1060,6 +1101,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 GraphLayerView) { + if (flag & FLAG_REMOVE) { + this.removeLayer(view); + return 0; + } + } if (view instanceof CellView) { if (flag & FLAG_REMOVE) { this.removeView(model); diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 5d898869ea..429c865d33 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -19,9 +19,7 @@ export class EmbeddingLayersController extends Listener { if (graph.hasLayer(cell.id)) { const layer = graph.getLayer(cell.id); layer.reset(); - graph.removeLayer(layer); - this.removeLayerView(layer); } }); @@ -50,14 +48,13 @@ export class EmbeddingLayersController extends Listener { this.listenTo(paper, 'cell:inserted', (_context, cellView) => { const cellId = cellView.model.id; if (paper.hasLayer(cellId)) { - const layerView = paper.getLayer(cellId); - cellView.el.after(layerView.el); + this.insertEmbeddedLayer(cellView); } }); } onParentChange(cell, parentId) { - const { graph } = this; + const { graph, paper } = this; if (parentId) { // Create new layer if it's not exist @@ -66,7 +63,11 @@ export class EmbeddingLayersController extends Listener { name: parentId }); graph.addLayer(layer); - this.insertLayerView(layer); + + const cellView = paper.findViewByModel(parentId); + if (cellView.isMounted()) { + this.insertEmbeddedLayer(cellView); + } } cell.layer(parentId); // Set the layer for the cell @@ -75,29 +76,9 @@ export class EmbeddingLayersController extends Listener { } } - insertLayerView(layer) { - const { paper } = this; - - const cellId = layer.name; - const layerView = paper.createLayer({ name: cellId, model: layer }); - // Do not append to the DOM immediately - // Layer will be appended with the correspondent container - paper.addLayer(layerView, { doNotAppend: true }); - - const cellView = paper.findViewByModel(cellId); - if (cellView.isMounted()) { - // Append the layer if the container is already mounted - cellView.el.after(layerView.el); - } - } - - removeLayerView(layer) { - const { paper } = this; - - const cellId = layer.name; - if (paper.hasLayer(cellId)) { - const layerView = paper.getLayer(cellId); - paper.removeLayer(layerView); - } + insertEmbeddedLayer(cellView) { + const cellId = cellView.model.id; + const layerView = this.paper.getLayer(cellId); + cellView.el.after(layerView.el); } } diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GraphLayerView.mjs index e5b7ecdd22..9d50c7f740 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GraphLayerView.mjs @@ -97,18 +97,4 @@ export const GraphLayerView = LayerView.extend({ } return cellNode; }, - - // TODO: make it work properly from inside of paper - _prepareRemove() { - const cellNodes = Array.from(this.el.children).filter(node => node.getAttribute('model-id')); - const cells = this.model.get('cells'); - - cellNodes.forEach((node) => { - const cellId = node.getAttribute('model-id'); - - if (!cells.has(cellId)) { - this.el.removeChild(node); - } - }); - } }); diff --git a/packages/joint-core/src/dia/layers/LayerView.mjs b/packages/joint-core/src/dia/layers/LayerView.mjs index 3897cbaa3d..42073ebfcb 100644 --- a/packages/joint-core/src/dia/layers/LayerView.mjs +++ b/packages/joint-core/src/dia/layers/LayerView.mjs @@ -8,6 +8,8 @@ export const LayerView = View.extend({ pivotNodes: null, defaultTheme: null, + UPDATE_PRIORITY: 10, + options: { name: '' }, From ed79f8e6d6ec0b56d31d7a297d5c0ab759e9bd19 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 21 Jul 2025 15:07:14 +0200 Subject: [PATCH 42/54] wip (probably doesn't work) --- packages/joint-core/src/dia/Cell.mjs | 46 ++--- packages/joint-core/src/dia/Graph.mjs | 74 +++---- packages/joint-core/src/dia/Paper.mjs | 104 ++++------ .../controllers/EmbeddingLayersController.mjs | 20 +- .../dia/controllers/GraphLayersController.mjs | 185 ------------------ .../src/dia/controllers/GroupsController.mjs | 185 ++++++++++++++++++ .../GraphLayer.mjs => groups/Group.mjs} | 31 +-- packages/joint-core/src/dia/index.mjs | 8 +- .../{GridLayerView.mjs => GridLayer.mjs} | 6 +- .../{GraphLayerView.mjs => GroupLayer.mjs} | 15 +- .../dia/layers/{LayerView.mjs => Layer.mjs} | 2 +- packages/joint-core/types/joint.d.ts | 26 +-- 12 files changed, 344 insertions(+), 358 deletions(-) delete mode 100644 packages/joint-core/src/dia/controllers/GraphLayersController.mjs create mode 100644 packages/joint-core/src/dia/controllers/GroupsController.mjs rename packages/joint-core/src/dia/{layers/GraphLayer.mjs => groups/Group.mjs} (62%) rename packages/joint-core/src/dia/layers/{GridLayerView.mjs => GridLayer.mjs} (97%) rename packages/joint-core/src/dia/layers/{GraphLayerView.mjs => GroupLayer.mjs} (86%) rename packages/joint-core/src/dia/layers/{LayerView.mjs => Layer.mjs} (98%) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index f4e65fd111..ed9c6654e2 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -261,14 +261,14 @@ export const Cell = Model.extend({ const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z()); - const layerName = this.layer(); + const groupName = this.group(); - const maxZ = graph.maxZIndex(layerName); + const maxZ = graph.maxZIndex(groupName); let z = maxZ - cells.length + 1; - const layerCells = graph.getLayerCells(layerName); + const groupCells = graph.getGroupCells(groupName); - let shouldUpdate = (layerCells.indexOf(sortedCells[0]) !== (layerCells.length - cells.length)); + let shouldUpdate = (groupCells.indexOf(sortedCells[0]) !== (groupCells.length - cells.length)); if (!shouldUpdate) { shouldUpdate = sortedCells.some(function(cell, index) { return cell.z() !== z + index; @@ -306,13 +306,13 @@ export const Cell = Model.extend({ const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z()); - const layerName = this.layer(); + const groupName = this.group(); - let z = graph.minZIndex(layerName); + let z = graph.minZIndex(groupName); - const layerCells = graph.getLayerCells(layerName); + const groupCells = graph.getGroupCells(groupName); - let shouldUpdate = (layerCells.indexOf(sortedCells[0]) !== 0); + let shouldUpdate = (groupCells.indexOf(sortedCells[0]) !== 0); if (!shouldUpdate) { shouldUpdate = sortedCells.some(function(cell, index) { return cell.z() !== z + index; @@ -948,29 +948,29 @@ export const Cell = Model.extend({ .difference(this.position()); }, - layer: function(layerName, opt) { - // if strictly null unset the layer - if (layerName === null) { - return this.unset('layer', opt); + group: function(groupName, opt) { + // if strictly null unset the group + if (groupName === null) { + return this.unset('group', opt); } - // if undefined return the current layer name - if (layerName === undefined) { - let layer = this.get('layer') || null; - // If the cell is part of a graph, use the graph's default layer. - if (layer == null && this.graph) { - layer = this.graph.getDefaultLayer().name; + // if undefined return the current group name + if (groupName === undefined) { + let group = this.get('group') || null; + // If the cell is part of a graph, use the graph's default group. + if (group == null && this.graph) { + group = this.graph.getDefaultGroup().name; } - return layer; + return group; } - // otherwise set the layer name - if (!isString(layerName)) { - throw new Error('Layer name must be a string.'); + // otherwise set the group name + if (!isString(groupName)) { + throw new Error('Group name must be a string.'); } - return this.set('layer', layerName, opt); + return this.set('group', groupName, opt); } }, { diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 4d1dfa5c02..0b28359c12 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -1,12 +1,12 @@ import * as util from '../util/index.mjs'; import * as g from '../g/index.mjs'; -import { GraphLayer } from './layers/GraphLayer.mjs'; +import { Group } from './groups/Group.mjs'; 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 { GraphLayersController } from './controllers/GraphLayersController.mjs'; +import { GroupsController } from './controllers/GroupsController.mjs'; const GraphCells = Collection.extend({ @@ -61,18 +61,18 @@ const GraphCells = Collection.extend({ export const Graph = Model.extend({ - defaultLayerName: 'cells', + defaultGroupName: 'cells', initialize: function(attrs, opt) { opt = opt || {}; - const defaultLayer = new GraphLayer({ - name: this.defaultLayerName + const defaultGroup = new Group({ + name: this.defaultGroupName }); - this.layersController = new GraphLayersController({ graph: this }); - this.layersController.addLayer(defaultLayer); + this.groupsController = new GroupsController({ graph: this }); + this.groupsController.addGroup(defaultGroup); // Passing `cellModel` function in the options object to graph allows for // setting models based on attribute objects. This is especially handy @@ -290,12 +290,12 @@ export const Graph = Model.extend({ return cell; }, - minZIndex: function(layerName) { - return this.layersController.minZIndex(layerName); + minZIndex: function(groupName) { + return this.groupsController.minZIndex(groupName); }, - maxZIndex: function(layerName) { - return this.layersController.maxZIndex(layerName); + maxZIndex: function(groupName) { + return this.groupsController.maxZIndex(groupName); }, addCell: function(cell, opt) { @@ -414,32 +414,32 @@ export const Graph = Model.extend({ this.stopBatch(batchName); }, - addLayer(layer, opt) { - this.layersController.addLayer(layer, opt); + addGroup(layer, opt) { + this.groupsController.addGroup(layer, opt); }, - removeLayer(layer, opt) { - this.layersController.removeLayer(layer.name, opt); + removeGroup(layer, opt) { + this.groupsController.removeGroup(layer.name, opt); }, - getDefaultLayer() { - return this.layersController.getDefaultLayer(); + getDefaultGroup() { + return this.groupsController.getDefaultGroup(); }, - getLayer(name) { - return this.layersController.getLayer(name); + getGroup(name) { + return this.groupsController.getGroup(name); }, - hasLayer(name) { - return this.layersController.hasLayer(name); + hasGroup(name) { + return this.groupsController.hasGroup(name); }, - getLayers() { - return this.layersController.getLayers(); + getGroups() { + return this.groupsController.getGroups(); }, - getLayerCells(layerName) { - return this.layersController.getLayerCells(layerName); + getGroupCells(groupName) { + return this.groupsController.getGroupCells(groupName); }, // Get a cell by `id`. @@ -450,7 +450,7 @@ export const Graph = Model.extend({ getCells: function() { // Preserve old order without layers - return this.layersController.getCells(); + return this.groupsController.getCells(); }, getElements: function() { @@ -463,18 +463,24 @@ export const Graph = Model.extend({ return this.getCells().filter(cell => cell.isLink()); }, - // @deprecated - getFirstCell: function() { - - const cells = this.getCells(); + getFirstCell: function(groupName) { + let cells; + if (!groupName) { + cells = this.getCells(); + } else { + cells = this.groupsController.getGroupCells(groupName); + } return cells[0]; }, - // @deprecated - getLastCell: function() { - - const cells = this.getCells(); + getLastCell: function(groupName) { + let cells; + if (!groupName) { + cells = this.getCells(); + } else { + cells = this.groupsController.getGroupCells(groupName); + } return cells[cells.length - 1]; }, diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index abd2352eab..bf21967192 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -37,15 +37,15 @@ import { ElementView } from './ElementView.mjs'; import { LinkView } from './LinkView.mjs'; import { Cell } from './Cell.mjs'; import { Graph } from './Graph.mjs'; -import { LayerView } from './layers/LayerView.mjs'; -import { GraphLayerView } from './layers/GraphLayerView.mjs'; +import { Layer } from './layers/Layer.mjs'; +import { GroupLayer } from './layers/GroupLayer.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 { GridLayerView } from './layers/GridLayerView.mjs'; +import { GridLayer } from './layers/GridLayer.mjs'; import { EmbeddingLayersController } from './controllers/EmbeddingLayersController.mjs'; export const LayersNames = { @@ -290,8 +290,8 @@ export const Paper = View.extend({ cellViewNamespace: null, layerViewNamespace: { - 'GridLayerView': GridLayerView, - 'GraphLayerView': GraphLayerView, + 'GridLayer': GridLayer, + 'GroupLayer': GroupLayer, }, routerNamespace: null, @@ -400,7 +400,7 @@ export const Paper = View.extend({ // Paper layers this._layersSettings = [{ - type: 'GridLayerView', + type: 'GridLayer', name: LayersNames.GRID, patterns: this.constructor.gridPatterns }, { @@ -409,7 +409,7 @@ export const Paper = View.extend({ name: LayersNames.LABELS, }, { name: LayersNames.CELLS, - model: this.model.getDefaultLayer() + model: this.model.getDefaultGroup() }, { name: LayersNames.FRONT }, { @@ -427,8 +427,8 @@ export const Paper = View.extend({ this._setDimensions(); this.startListening(); - this._graphLayers = []; - this.updateGraphLayers(this.model.get('layers')); + this._groups = []; + this.updateGroups(); if (options.useLayersForEmbedding) { this.embeddingLayersController = new EmbeddingLayersController({ graph: model, paper: this }); @@ -474,7 +474,7 @@ export const Paper = View.extend({ .listenTo(model, 'remove', this.onCellRemoved) .listenTo(model, 'reset', this.onGraphReset) .listenTo(model, 'batch:stop', this.onGraphBatchStop) - .listenTo(model, 'change:layers', this.onLayersChange); + .listenTo(model, 'change:groups', this.updateGroups); this.on('cell:highlight', this.onCellHighlight) .on('cell:unhighlight', this.onCellUnhighlight) @@ -514,21 +514,17 @@ export const Paper = View.extend({ } }, - onLayersChange: function(_, layers) { - this.updateGraphLayers(layers) - }, - - updateGraphLayers: function(layers) { - const removedLayerNames = this._graphLayers.filter(layer => !layers.some(l => l.name === layer.name)).map(layer => layer.name); - removedLayerNames.forEach(layerName => this.requestLayerRemove(layerName)); + updateGroups: function() { + const removedGroupNames = this._groups.filter(group => !groups.some(l => l.name === group.name)).map(group => group.name); + removedGroupNames.forEach(groupName => this.requestLayerRemove(groupName)); - this._graphLayers = this.model.get('layers'); + this._groups = this.model.get('groups'); - this._graphLayers.forEach(layer => { - if (!this.hasLayer(layer.name)) { + this._groups.forEach(group => { + if (!this.hasLayer(group.name)) { this.renderLayer({ - name: layer.name, - model: layer + name: group.name, + model: group }); } }); @@ -634,14 +630,14 @@ export const Paper = View.extend({ return this.getLayer(layerName).el; }, - _removeLayer(layerView) { - this._unregisterLayer(layerView); - layerView.remove(); + _removeLayer(layer) { + this._unregisterLayer(layer); + layer.remove(); }, - _unregisterLayer(layerView) { + _unregisterLayer(layer) { const { _layers: { viewsMap, order }} = this; - const layerName = layerView.name; + const layerName = layer.name; if (order.indexOf(layerName) !== -1) { order.splice(order.indexOf(layerName), 1); @@ -650,21 +646,21 @@ export const Paper = View.extend({ delete viewsMap[layerName]; }, - _registerLayer(layerView) { + _registerLayer(layer) { const { _layers: { viewsMap }} = this; - const layerName = layerView.name; + const layerName = layer.name; - viewsMap[layerName] = layerView; + viewsMap[layerName] = layer; }, - _requireLayerView(layer) { + _requireLayer(layer) { let layerName; if (typeof layer === 'string') { layerName = layer; - } else if (layer instanceof LayerView) { + } else if (layer instanceof Layer) { layerName = layer.name; } else { - throw new Error('dia.Paper: The layer view is not an instance of dia.LayerView.'); + throw new Error('dia.Paper: The layer is not an instance of dia.Layer.'); } if (!this.hasLayer(layerName)) { @@ -675,10 +671,10 @@ export const Paper = View.extend({ removeLayer(layer) { if (!layer) { - throw new Error('dia.Paper: The layer view must be provided.'); + throw new Error('dia.Paper: The layer must be provided.'); } - const layerView = this._requireLayerView(layer); + const layerView = this._requireLayer(layer); if (!layerView.isEmpty()) { throw new Error('dia.Paper: The layer is not empty.'); @@ -698,29 +694,9 @@ export const Paper = View.extend({ this.requestViewUpdate(layerView, FLAG_REMOVE, UPDATE_PRIORITY); }, - addLayer(layerView) { - if (!layerView) { - throw new Error('dia.Paper: The layer view must be provided.'); - } - - const layerName = layerView.name; - - if (!layerName || typeof layerName !== 'string') { - throw new Error('dia.Paper: The layer should has a name.'); - } - if (this.hasLayer(layerName)) { - throw new Error(`dia.Paper: The layer "${layerName}" already exists.`); - } - if (!(layerView instanceof LayerView)) { - throw new Error('dia.Paper: The layer view is not an instance of dia.LayerView.'); - } - - this._registerLayer(layerView); - }, - insertLayer(layer, insertBefore) { if (!this.hasLayer(layer.name)) { - this.addLayer(layer); + throw new Error(`dia.Paper: Unknown layer "${layer.name}".`); } const { _layers: { order }} = this; @@ -807,17 +783,23 @@ export const Paper = View.extend({ if (options.model) { const modelType = options.model.get('type') || options.model.constructor.name; - type = modelType + 'View'; + type = modelType + 'Layer'; } - const viewConstructor = this.options.layerViewNamespace[type] || LayerView; + const viewConstructor = this.options.layerViewNamespace[type] || Layer; return new viewConstructor(options); }, renderLayer: function(options) { const layerView = this.createLayer(options); - this.addLayer(layerView); + const layerName = layerView.name; + + if (this.hasLayer(layerName)) { + throw new Error(`dia.Paper: The layer "${layerName}" already exists.`); + } + + this._registerLayer(layerView); return layerView; }, @@ -1101,7 +1083,7 @@ 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 GraphLayerView) { + if (view instanceof GroupLayer) { if (flag & FLAG_REMOVE) { this.removeLayer(view); return 0; @@ -1963,7 +1945,7 @@ export const Paper = View.extend({ sortLayersExact: function() { const { _layers: { viewsMap }} = this; - Object.values(viewsMap).filter(view => view instanceof GraphLayerView).forEach(view => view.sortExact()); + Object.values(viewsMap).filter(view => view instanceof GroupLayer).forEach(view => view.sortExact()); }, insertView: function(view, isInitialInsert) { diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 429c865d33..adfd54996c 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -1,5 +1,5 @@ import { Listener } from '../../mvc/Listener.mjs'; -import { GraphLayer } from '../layers/GraphLayer.mjs'; +import { Group } from '../groups/Group.mjs'; export class EmbeddingLayersController extends Listener { @@ -16,10 +16,10 @@ export class EmbeddingLayersController extends Listener { const { graph, paper } = this; this.listenTo(graph, 'remove', (_context, cell) => { - if (graph.hasLayer(cell.id)) { - const layer = graph.getLayer(cell.id); - layer.reset(); - graph.removeLayer(layer); + if (graph.hasGroup(cell.id)) { + const group = graph.getGroup(cell.id); + group.reset(); + graph.removeGroup(group); } }); @@ -58,11 +58,11 @@ export class EmbeddingLayersController extends Listener { if (parentId) { // Create new layer if it's not exist - if (!graph.hasLayer(parentId)) { - const layer = new GraphLayer({ + if (!graph.hasGroup(parentId)) { + const group = new Group({ name: parentId }); - graph.addLayer(layer); + graph.addGroup(group); const cellView = paper.findViewByModel(parentId); if (cellView.isMounted()) { @@ -78,7 +78,7 @@ export class EmbeddingLayersController extends Listener { insertEmbeddedLayer(cellView) { const cellId = cellView.model.id; - const layerView = this.paper.getLayer(cellId); - cellView.el.after(layerView.el); + const layer = this.paper.getLayer(cellId); + cellView.el.after(layer.el); } } diff --git a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs b/packages/joint-core/src/dia/controllers/GraphLayersController.mjs deleted file mode 100644 index 58f7d6ba25..0000000000 --- a/packages/joint-core/src/dia/controllers/GraphLayersController.mjs +++ /dev/null @@ -1,185 +0,0 @@ -import { Listener } from '../../mvc/Listener.mjs'; - -export class GraphLayersController extends Listener { - - constructor(context) { - super(context); - - this.graph = context.graph; - - if (!this.graph.has('layers')) { - this.graph.set('layers', []); - } - - this.layers = this.graph.get('layers'); - this.layersMap = {}; - - this.layers.forEach(layer => { - this.layersMap[layer.name] = layer; - }); - - this.defaultLayerName = this.graph.defaultLayerName; - - 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.listenTo(graph, 'reset', (_context, { models: cells }) => { - const { layersMap } = this; - - for (let layerName in layersMap) { - layersMap[layerName].reset(); - } - - cells.forEach(cell => { - this.onAdd(cell, true); - }); - }); - - this.listenTo(graph, 'change:layer', (_context, cell, layerName) => { - if (!layerName) { - layerName = this.defaultLayerName; - } - - if (this.hasLayer(layerName)) { - this.layersMap[layerName].add(cell); - } - }); - } - - onAdd(cell, reset = false) { - const { layersMap } = this; - - const layerName = cell.layer() || this.defaultLayerName; - const layer = layersMap[layerName]; - - if (!layer) { - throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); - } - - // compatibility - // in the version before layers, z-index was not set on reset - if (!reset) { - if (!cell.has('z')) { - cell.set('z', layer.maxZIndex() + 1); - } - } - - // mandatory add to the layer - // so every cell now will have a layer specified - layer.add(cell); - } - - onRemove(cell) { - const { layersMap } = this; - - const layerName = cell.layer() || this.defaultLayerName; - - const layer = layersMap[layerName]; - - if (layer) { - layer.remove(cell); - } - } - - getDefaultLayer() { - return this.layersMap[this.defaultLayerName]; - } - - addLayer(layer, _opt) { - const { layersMap } = this; - - if (layersMap[layer.name]) { - throw new Error(`dia.Graph: Layer with name '${layer.name}' already exists.`); - } - - this.layers = this.layers.concat([layer]); - - layersMap[layer.name] = layer; - - this.graph.set('layers', this.layers); - } - - removeLayer(layerName, _opt) { - const { layersMap, defaultLayerName } = this; - - if (layerName === defaultLayerName) { - throw new Error('dia.Graph: default layer cannot be removed.'); - } - - if (!layersMap[layerName]) { - throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); - } - - this.layers = this.layers.filter(l => l.name !== layerName); - - delete this.layersMap[layerName]; - this.graph.set('layers', this.layers); - } - - minZIndex(layerName) { - const { layersMap, defaultLayerName } = this; - - layerName = layerName || defaultLayerName; - - const layer = layersMap[layerName]; - - return layer.minZIndex(); - } - - maxZIndex(layerName) { - const { layersMap, defaultLayerName } = this; - - layerName = layerName || defaultLayerName; - - const layer = layersMap[layerName]; - - return layer.maxZIndex(); - } - - hasLayer(layerName) { - return !!this.layersMap[layerName]; - } - - getLayer(layerName) { - if (!this.layersMap[layerName]) { - throw new Error(`dia.Graph: Layer with name '${layerName}' does not exist.`); - } - - return this.layersMap[layerName]; - } - - getLayers() { - const layers = []; - - for (let layerName in this.layersMap) { - layers.push(this.layersMap[layerName]); - } - - return layers; - } - - getLayerCells(layerName) { - return this.layersMap[layerName].get('cells').toArray(); - } - - getCells() { - const cells = []; - - for (let layerName in this.layersMap) { - cells.push(...this.layersMap[layerName].get('cells')); - } - - return cells; - } -} diff --git a/packages/joint-core/src/dia/controllers/GroupsController.mjs b/packages/joint-core/src/dia/controllers/GroupsController.mjs new file mode 100644 index 0000000000..e271bcc827 --- /dev/null +++ b/packages/joint-core/src/dia/controllers/GroupsController.mjs @@ -0,0 +1,185 @@ +import { Listener } from '../../mvc/Listener.mjs'; + +export class GroupsController extends Listener { + + constructor(context) { + super(context); + + this.graph = context.graph; + + if (!this.graph.has('groups')) { + this.graph.set('groups', []); + } + + this.groups = this.graph.get('groups'); + this.groupsMap = {}; + + this.groups.forEach(layer => { + this.groupsMap[layer.name] = layer; + }); + + this.defaultGroupName = this.graph.defaultGroupName; + + 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.listenTo(graph, 'reset', (_context, { models: cells }) => { + const { groupsMap } = this; + + for (let groupName in groupsMap) { + groupsMap[groupName].reset(); + } + + cells.forEach(cell => { + this.onAdd(cell, true); + }); + }); + + this.listenTo(graph, 'change:layer', (_context, cell, groupName) => { + if (!groupName) { + groupName = this.defaultGroupName; + } + + if (this.hasGroup(groupName)) { + this.groupsMap[groupName].add(cell); + } + }); + } + + onAdd(cell, reset = false) { + const { groupsMap } = this; + + const groupName = cell.layer() || this.defaultGroupName; + const layer = groupsMap[groupName]; + + if (!layer) { + throw new Error(`dia.Graph: Layer with name '${groupName}' does not exist.`); + } + + // 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); + } + } + + // mandatory add to the layer + // so every cell now will have a layer specified + layer.add(cell); + } + + onRemove(cell) { + const { groupsMap } = this; + + const groupName = cell.layer() || this.defaultGroupName; + + const layer = groupsMap[groupName]; + + if (layer) { + layer.remove(cell); + } + } + + getDefaultGroup() { + return this.groupsMap[this.defaultGroupName]; + } + + addGroup(group, _opt) { + const { groupsMap } = this; + + if (groupsMap[group.name]) { + throw new Error(`dia.Graph: Layer with name '${group.name}' already exists.`); + } + + this.groups = this.groups.concat([group]); + + groupsMap[group.name] = group; + + this.graph.set('groups', this.groups); + } + + removeGroup(groupName, _opt) { + const { groupsMap, defaultGroupName } = this; + + if (groupName === defaultGroupName) { + throw new Error('dia.Graph: default layer cannot be removed.'); + } + + if (!groupsMap[groupName]) { + throw new Error(`dia.Graph: Layer with name '${groupName}' does not exist.`); + } + + this.groups = this.groups.filter(l => l.name !== groupName); + + delete this.groupsMap[groupName]; + this.graph.set('groups', this.groups); + } + + minZIndex(groupName) { + const { groupsMap, defaultGroupName } = this; + + groupName = groupName || defaultGroupName; + + const layer = groupsMap[groupName]; + + return layer.minZIndex(); + } + + maxZIndex(groupName) { + const { groupsMap, defaultGroupName } = this; + + groupName = groupName || defaultGroupName; + + const layer = groupsMap[groupName]; + + return layer.maxZIndex(); + } + + hasGroup(groupName) { + return !!this.groupsMap[groupName]; + } + + getGroup(groupName) { + if (!this.groupsMap[groupName]) { + throw new Error(`dia.Graph: Layer with name '${groupName}' does not exist.`); + } + + return this.groupsMap[groupName]; + } + + getGroups() { + const groups = []; + + for (let groupName in this.groupsMap) { + groups.push(this.groupsMap[groupName]); + } + + return groups; + } + + getGroupCells(groupName) { + return this.groupsMap[groupName].get('cells').toArray(); + } + + getCells() { + const cells = []; + + for (let groupName in this.groupsMap) { + cells.push(...this.groupsMap[groupName].get('cells')); + } + + return cells; + } +} diff --git a/packages/joint-core/src/dia/layers/GraphLayer.mjs b/packages/joint-core/src/dia/groups/Group.mjs similarity index 62% rename from packages/joint-core/src/dia/layers/GraphLayer.mjs rename to packages/joint-core/src/dia/groups/Group.mjs index 77f69992db..b5a1210046 100644 --- a/packages/joint-core/src/dia/layers/GraphLayer.mjs +++ b/packages/joint-core/src/dia/groups/Group.mjs @@ -1,6 +1,6 @@ import { Model, Collection } from '../../mvc/index.mjs'; -class LayerCells extends Collection { +export class GroupCollection extends Collection { // `comparator` makes it easy to sort cells based on their `z` index. comparator(model) { @@ -8,12 +8,11 @@ class LayerCells extends Collection { } } -export class GraphLayer extends Model { +export class Group extends Model { defaults() { return { - type: 'GraphLayer', - displayName: '', + type: 'Group', hidden: false, locked: false, }; @@ -22,32 +21,26 @@ export class GraphLayer extends Model { initialize(attrs) { super.initialize(attrs); - this.name = attrs.name; + this.cells = new GroupCollection(); - const cells = new LayerCells(); - this.set('cells', cells); - - cells.on('change:z', (cell, _, opt) => { - cells.sort(); - - this.trigger('updateCell', cell, opt); + this.cells.on('change:z', (cell, _, opt) => { + this.cells.sort(); }); - cells.on('change:layer', (cell, layerName) => { - // If the cell's layer is changed, we need to remove it from this layer. - if (layerName !== this.name) { + this.cells.on('change:group', (cell, groupName) => { + // If the cell's group id is changed, we need to remove it from this group. + if (groupName !== this.name) { this.cells.remove(cell); } }); // Make all the events fired in the `cells` collection available. // to the outside world. - cells.on('all', this.trigger, this); + this.cells.on('all', this.trigger, this); } add(cell) { this.cells.add(cell); - this.trigger('updateCell', cell); } remove(cell) { @@ -71,8 +64,4 @@ export class GraphLayer extends Model { const lastCell = this.cells.last(); return lastCell ? (lastCell.get('z') || 0) : 0; } - - get cells() { - return this.get('cells'); - } } diff --git a/packages/joint-core/src/dia/index.mjs b/packages/joint-core/src/dia/index.mjs index f55b97f59c..97f55963d7 100644 --- a/packages/joint-core/src/dia/index.mjs +++ b/packages/joint-core/src/dia/index.mjs @@ -10,7 +10,7 @@ export * from './Paper.mjs'; export * from './ToolView.mjs'; export * from './ToolsView.mjs'; export * from './HighlighterView.mjs'; -export * from './layers/GraphLayer.mjs'; -export * from './layers/LayerView.mjs'; -export * from './layers/GridLayerView.mjs'; -export * from './layers/GraphLayerView.mjs'; +export * from './layers/GroupLayer.mjs'; +export * from './layers/Layer.mjs'; +export * from './layers/GridLayer.mjs'; +export * from './groups/Group.mjs'; diff --git a/packages/joint-core/src/dia/layers/GridLayerView.mjs b/packages/joint-core/src/dia/layers/GridLayer.mjs similarity index 97% rename from packages/joint-core/src/dia/layers/GridLayerView.mjs rename to packages/joint-core/src/dia/layers/GridLayer.mjs index e147107804..660667e174 100644 --- a/packages/joint-core/src/dia/layers/GridLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GridLayer.mjs @@ -1,4 +1,4 @@ -import { LayerView } from './LayerView.mjs'; +import { Layer } from './Layer.mjs'; import { isFunction, isString, @@ -9,7 +9,7 @@ import { } from '../../util/index.mjs'; import V from '../../V/index.mjs'; -export const GridLayerView = LayerView.extend({ +export const GridLayer = Layer.extend({ style: { 'pointer-events': 'none' @@ -19,7 +19,7 @@ export const GridLayerView = LayerView.extend({ _gridSettings: null, init() { - LayerView.prototype.init.apply(this, arguments); + Layer.prototype.init.apply(this, arguments); const { options: { paper }} = this; this._gridCache = null; this._gridSettings = []; diff --git a/packages/joint-core/src/dia/layers/GraphLayerView.mjs b/packages/joint-core/src/dia/layers/GroupLayer.mjs similarity index 86% rename from packages/joint-core/src/dia/layers/GraphLayerView.mjs rename to packages/joint-core/src/dia/layers/GroupLayer.mjs index 9d50c7f740..1d222abd6a 100644 --- a/packages/joint-core/src/dia/layers/GraphLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GroupLayer.mjs @@ -1,13 +1,13 @@ -import { LayerView } from './LayerView.mjs'; +import { Layer } from './Layer.mjs'; import { sortElements } from '../../util/index.mjs'; import { sortingTypes } from '../Paper.mjs'; -export const GraphLayerView = LayerView.extend({ +export const GroupLayer = Layer.extend({ - SORT_DELAYING_BATCHES: ['add', 'reset', 'to-front', 'to-back'], + SORT_DELAYING_EDGING_BATCHES: ['add', 'reset', 'to-front', 'to-back'], init() { - LayerView.prototype.init.apply(this, arguments); + Layer.prototype.init.apply(this, arguments); this.startListening(); }, @@ -30,6 +30,13 @@ export const GraphLayerView = LayerView.extend({ } }); + this.listenTo(model, 'add', (cell, opt) => { + const view = paper.findViewByModel(cell); + if (view) { + paper.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt); + } + }); + this.listenTo(model, 'updateCell', (cell, opt) => { if ( cell.hasChanged('layer') || diff --git a/packages/joint-core/src/dia/layers/LayerView.mjs b/packages/joint-core/src/dia/layers/Layer.mjs similarity index 98% rename from packages/joint-core/src/dia/layers/LayerView.mjs rename to packages/joint-core/src/dia/layers/Layer.mjs index 42073ebfcb..3546f74fcf 100644 --- a/packages/joint-core/src/dia/layers/LayerView.mjs +++ b/packages/joint-core/src/dia/layers/Layer.mjs @@ -1,7 +1,7 @@ import { View } from '../../mvc/index.mjs'; import { addClassNamePrefix } from '../../util/util.mjs'; -export const LayerView = View.extend({ +export const Layer = View.extend({ tagName: 'g', svgElement: true, diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 50c2e9c977..e18c99e8f1 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -1771,25 +1771,25 @@ export namespace dia { getLayerNode(layerName: Paper.Layers | string): SVGGElement; - getLayer(layerName: Paper.Layers | string): LayerView; + getLayer(layerName: Paper.Layers | string): Layer; hasLayer(layerName: Paper.Layers | string): boolean; - renderLayers(layers: Array<{ name: string }>): void; - protected removeLayers(): void; protected resetLayers(): void; - addLayer(layerView: LayerView, options?: { insertBefore?: string | LayerView }): void; + renderLayer(options: Omit): void; + + insertLayer(layer: Layer, insertBefore?: string | Layer): void; - removeLayer(LayerView: LayerView): void; + removeLayer(Layer: Layer): void; - moveLayer(layerView: LayerView, insertBefore: string | LayerView | null): void; + moveLayer(layer: Layer, insertBefore?: string | Layer): void; getLayerNames(): string[]; - getLayers(): Array; + getLayers(): Array; // rendering @@ -1982,18 +1982,20 @@ export namespace dia { scaleContentToFit(opt?: Paper.ScaleContentOptions): void; } - namespace LayerView { + namespace Layer { interface Options extends mvc.ViewOptions { name: string; + paper: Paper; + type?: string; } } - class LayerView extends mvc.View { + class Layer extends mvc.View { - constructor(opt?: LayerView.Options); + constructor(opt?: Layer.Options); - options: LayerView.Options; + options: Layer.Options; pivotNodes: { [z: number]: Comment }; @@ -2021,7 +2023,7 @@ export namespace dia { maxZIndex(): number; } - class GraphLayerView extends LayerView { + class GraphLayer extends Layer { protected sort(): void; From 8358fa80a65b0416e2888d04d848c0cc007f4d09 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 23 Jul 2025 16:08:42 +0200 Subject: [PATCH 43/54] up --- packages/joint-core/src/dia/Cell.mjs | 46 ++-- packages/joint-core/src/dia/Graph.mjs | 63 +++-- packages/joint-core/src/dia/Paper.mjs | 246 +++++++++--------- packages/joint-core/src/dia/ToolsView.mjs | 4 +- .../dia/controllers/CellLayersController.mjs | 201 ++++++++++++++ .../controllers/EmbeddingLayersController.mjs | 23 +- .../src/dia/controllers/GroupsController.mjs | 185 ------------- .../joint-core/src/dia/groups/CellGroup.mjs | 62 +++++ .../joint-core/src/dia/groups/CellLayer.mjs | 44 ++++ packages/joint-core/src/dia/groups/Group.mjs | 67 ----- packages/joint-core/src/dia/index.mjs | 9 +- .../{GroupLayer.mjs => CellLayerView.mjs} | 4 +- .../{GridLayer.mjs => GridLayerView.mjs} | 4 +- .../dia/layers/{Layer.mjs => LayerView.mjs} | 11 +- 14 files changed, 502 insertions(+), 467 deletions(-) create mode 100644 packages/joint-core/src/dia/controllers/CellLayersController.mjs delete mode 100644 packages/joint-core/src/dia/controllers/GroupsController.mjs create mode 100644 packages/joint-core/src/dia/groups/CellGroup.mjs create mode 100644 packages/joint-core/src/dia/groups/CellLayer.mjs delete mode 100644 packages/joint-core/src/dia/groups/Group.mjs rename packages/joint-core/src/dia/layers/{GroupLayer.mjs => CellLayerView.mjs} (97%) rename packages/joint-core/src/dia/layers/{GridLayer.mjs => GridLayerView.mjs} (98%) rename packages/joint-core/src/dia/layers/{Layer.mjs => LayerView.mjs} (89%) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index ed9c6654e2..aece4ab97a 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -261,14 +261,14 @@ export const Cell = Model.extend({ const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z()); - const groupName = this.group(); + const layerId = this.layer(); - const maxZ = graph.maxZIndex(groupName); + const maxZ = graph.maxZIndex(layerId); let z = maxZ - cells.length + 1; - const groupCells = graph.getGroupCells(groupName); + const layerCells = graph.getCellLayerCells(layerId); - let shouldUpdate = (groupCells.indexOf(sortedCells[0]) !== (groupCells.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; @@ -306,13 +306,13 @@ export const Cell = Model.extend({ const sortedCells = opt.foregroundEmbeds ? cells : sortBy(cells, cell => cell.z()); - const groupName = this.group(); + const layerId = this.layer(); - let z = graph.minZIndex(groupName); + let z = graph.minZIndex(layerId); - const groupCells = graph.getGroupCells(groupName); + const layerCells = graph.getCellLayerCells(layerId); - let shouldUpdate = (groupCells.indexOf(sortedCells[0]) !== 0); + let shouldUpdate = (layerCells.indexOf(sortedCells[0]) !== 0); if (!shouldUpdate) { shouldUpdate = sortedCells.some(function(cell, index) { return cell.z() !== z + index; @@ -948,29 +948,29 @@ export const Cell = Model.extend({ .difference(this.position()); }, - group: function(groupName, opt) { - // if strictly null unset the group - if (groupName === null) { - return this.unset('group', opt); + layer: function(layerId, opt) { + // if strictly null unset the layer + if (layerId === null) { + return this.unset('layer', opt); } - // if undefined return the current group name - if (groupName === undefined) { - let group = this.get('group') || null; - // If the cell is part of a graph, use the graph's default group. - if (group == null && this.graph) { - group = this.graph.getDefaultGroup().name; + // if undefined return the current layer id + if (layerId === undefined) { + const 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 group; + return layerId; } - // otherwise set the group name - if (!isString(groupName)) { - throw new Error('Group name must be a string.'); + // otherwise set the layer id + if (!isString(layerId)) { + throw new Error('Layer id must be a string.'); } - return this.set('group', groupName, opt); + return this.set('layer', layerId, opt); } }, { diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 0b28359c12..d9113f1b2e 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -1,12 +1,12 @@ import * as util from '../util/index.mjs'; import * as g from '../g/index.mjs'; -import { Group } from './groups/Group.mjs'; 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 { GroupsController } from './controllers/GroupsController.mjs'; +import { CellLayer } from './groups/CellLayer.mjs'; +import { CellLayersController } from './controllers/CellLayersController.mjs'; const GraphCells = Collection.extend({ @@ -61,18 +61,13 @@ const GraphCells = Collection.extend({ export const Graph = Model.extend({ - defaultGroupName: 'cells', + defaultCellLayerId: 'cells', initialize: function(attrs, opt) { opt = opt || {}; - const defaultGroup = new Group({ - name: this.defaultGroupName - }); - - this.groupsController = new GroupsController({ graph: this }); - this.groupsController.addGroup(defaultGroup); + 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 @@ -290,12 +285,12 @@ export const Graph = Model.extend({ return cell; }, - minZIndex: function(groupName) { - return this.groupsController.minZIndex(groupName); + minZIndex: function(layerId) { + return this.cellLayersController.minZIndex(layerId); }, - maxZIndex: function(groupName) { - return this.groupsController.maxZIndex(groupName); + maxZIndex: function(layerId) { + return this.cellLayersController.maxZIndex(layerId); }, addCell: function(cell, opt) { @@ -414,32 +409,32 @@ export const Graph = Model.extend({ this.stopBatch(batchName); }, - addGroup(layer, opt) { - this.groupsController.addGroup(layer, opt); + addCellLayer(cellLayer, opt) { + this.cellLayersController.addCellLayer(cellLayer, opt); }, - removeGroup(layer, opt) { - this.groupsController.removeGroup(layer.name, opt); + removeCellLayer(cellLayer, opt) { + this.cellLayersController.removeCellLayer(cellLayer.id, opt); }, - getDefaultGroup() { - return this.groupsController.getDefaultGroup(); + getDefaultCellLayer() { + return this.cellLayersController.getDefaultCellLayer(); }, - getGroup(name) { - return this.groupsController.getGroup(name); + getCellLayer(layerId) { + return this.cellLayersController.getCellLayer(layerId); }, - hasGroup(name) { - return this.groupsController.hasGroup(name); + hasCellLayer(layerId) { + return this.cellLayersController.hasCellLayer(layerId); }, - getGroups() { - return this.groupsController.getGroups(); + getCellLayers() { + return this.cellLayersController.getCellLayers(); }, - getGroupCells(groupName) { - return this.groupsController.getGroupCells(groupName); + getCellLayerCells(layerId) { + return this.cellLayersController.getCellLayerCells(layerId); }, // Get a cell by `id`. @@ -450,7 +445,7 @@ export const Graph = Model.extend({ getCells: function() { // Preserve old order without layers - return this.groupsController.getCells(); + return this.cellLayersController.getCells(); }, getElements: function() { @@ -463,23 +458,23 @@ export const Graph = Model.extend({ return this.getCells().filter(cell => cell.isLink()); }, - getFirstCell: function(groupName) { + getFirstCell: function(layerId) { let cells; - if (!groupName) { + if (!layerId) { cells = this.getCells(); } else { - cells = this.groupsController.getGroupCells(groupName); + cells = this.cellLayersController.getCellLayer(layerId).cells; } return cells[0]; }, - getLastCell: function(groupName) { + getLastCell: function(layerId) { let cells; - if (!groupName) { + if (!layerId) { cells = this.getCells(); } else { - cells = this.groupsController.getGroupCells(groupName); + cells = this.cellLayersController.getCellLayer(layerId).cells; } return cells[cells.length - 1]; diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index bf21967192..d18f5de431 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -37,20 +37,19 @@ import { ElementView } from './ElementView.mjs'; import { LinkView } from './LinkView.mjs'; import { Cell } from './Cell.mjs'; import { Graph } from './Graph.mjs'; -import { Layer } from './layers/Layer.mjs'; -import { GroupLayer } from './layers/GroupLayer.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 LayersNames = { +export const LayersIds = { GRID: 'grid', - CELLS: 'cells', BACK: 'back', FRONT: 'front', TOOLS: 'tools', @@ -290,8 +289,8 @@ export const Paper = View.extend({ cellViewNamespace: null, layerViewNamespace: { - 'GridLayer': GridLayer, - 'GroupLayer': GroupLayer, + 'GridLayerView': GridLayerView, + 'CellLayerView': CellLayerView, }, routerNamespace: null, @@ -398,22 +397,19 @@ export const Paper = View.extend({ const model = this.model = options.model || new Graph; - // Paper layers + // Paper layers except the cell layers. this._layersSettings = [{ - type: 'GridLayer', - name: LayersNames.GRID, + id: LayersIds.GRID, + type: 'GridLayerView', patterns: this.constructor.gridPatterns }, { - name: LayersNames.BACK, + id: LayersIds.BACK, }, { - name: LayersNames.LABELS, + id: LayersIds.LABELS, }, { - name: LayersNames.CELLS, - model: this.model.getDefaultGroup() + id: LayersIds.FRONT }, { - name: LayersNames.FRONT - }, { - name: LayersNames.TOOLS + id: LayersIds.TOOLS }]; // Layers (SVGGroups) @@ -427,8 +423,8 @@ export const Paper = View.extend({ this._setDimensions(); this.startListening(); - this._groups = []; - this.updateGroups(); + // current cell layers model attributes from the Graph + this._cellLayers = []; if (options.useLayersForEmbedding) { this.embeddingLayersController = new EmbeddingLayersController({ graph: model, paper: this }); @@ -474,7 +470,7 @@ export const Paper = View.extend({ .listenTo(model, 'remove', this.onCellRemoved) .listenTo(model, 'reset', this.onGraphReset) .listenTo(model, 'batch:stop', this.onGraphBatchStop) - .listenTo(model, 'change:groups', this.updateGroups); + .listenTo(model, 'change:cellLayers', this.updateCellLayers); this.on('cell:highlight', this.onCellHighlight) .on('cell:unhighlight', this.onCellUnhighlight) @@ -514,20 +510,22 @@ export const Paper = View.extend({ } }, - updateGroups: function() { - const removedGroupNames = this._groups.filter(group => !groups.some(l => l.name === group.name)).map(group => group.name); - removedGroupNames.forEach(groupName => this.requestLayerRemove(groupName)); + updateCellLayers: function(_graph, cellLayers) { + const removedCellLayerViewIds = this._cellLayers.filter(cellLayerView => !cellLayers.some(l => l.id === cellLayerView.id)).map(cellLayerView => cellLayerView.id); + removedCellLayerViewIds.forEach(cellLayerViewId => this.requestLayerViewRemove(cellLayerViewId)); - this._groups = this.model.get('groups'); + cellLayers.forEach(cellLayer => { + if (!this.hasLayer(cellLayer.id)) { + const cellLayerModel = this.model.getCellLayer(cellLayer.id); - this._groups.forEach(group => { - if (!this.hasLayer(group.name)) { - this.renderLayer({ - name: group.name, - model: group + this.renderLayerView({ + id: cellLayer.id, + model: cellLayerModel }); } }); + + this._cellLayers = cellLayers; }, cloneOptions: function() { @@ -614,131 +612,110 @@ export const Paper = View.extend({ }]; }, - hasLayer(layerName) { - return (layerName in this._layers.viewsMap); + hasLayerView(layerId) { + return (layerId in this._layers.viewsMap); }, - getLayer(layerName) { + getLayerView(layerId) { const { _layers: { viewsMap }} = this; - if (layerName in viewsMap) { - return viewsMap[layerName]; + if (layerId in viewsMap) { + return viewsMap[layerId]; } - throw new Error(`dia.Paper: Unknown layer "${layerName}".`); + throw new Error(`dia.Paper: Unknown layer "${layerId}".`); }, - getLayerNode(layerName) { - return this.getLayer(layerName).el; + getLayerViewNode(layerId) { + return this.getLayerView(layerId).el; }, - _removeLayer(layer) { - this._unregisterLayer(layer); - layer.remove(); + _removeLayerView(layerView) { + this._unregisterLayerView(layerView); + layerView.remove(); }, - _unregisterLayer(layer) { + _unregisterLayerView(layerView) { const { _layers: { viewsMap, order }} = this; - const layerName = layer.name; + const layerId = layerView.id; - if (order.indexOf(layerName) !== -1) { - order.splice(order.indexOf(layerName), 1); + if (order.indexOf(layerId) !== -1) { + order.splice(order.indexOf(layerId), 1); } - delete viewsMap[layerName]; + delete viewsMap[layerId]; }, - _registerLayer(layer) { + _registerLayerView(layerView) { const { _layers: { viewsMap }} = this; - const layerName = layer.name; + const layerId = layerView.id; - viewsMap[layerName] = layer; + viewsMap[layerId] = layerView; }, - _requireLayer(layer) { - let layerName; - if (typeof layer === 'string') { - layerName = layer; - } else if (layer instanceof Layer) { - layerName = layer.name; - } else { - throw new Error('dia.Paper: The layer is not an instance of dia.Layer.'); + _requireLayerView(layerView) { + let layerId; + if (isString(layerView)) { + layerId = layerView; + } else if (layerView instanceof LayerView) { + layerId = layerView.id; } - if (!this.hasLayer(layerName)) { - throw new Error(`dia.Paper: Unknown layer "${layerName}".`); + if (!this.hasLayer(layerId)) { + throw new Error(`dia.Paper: Unknown layer "${layerId}".`); } - return this.getLayer(layerName); + return this.getLayerView(layerId); }, - removeLayer(layer) { - if (!layer) { - throw new Error('dia.Paper: The layer must be provided.'); - } - - const layerView = this._requireLayer(layer); + removeLayerView(layerView) { + layerView = this._requireLayerView(layerView); if (!layerView.isEmpty()) { throw new Error('dia.Paper: The layer is not empty.'); } - this._removeLayer(layerView); + this._removeLayerView(layerView); }, - requestLayerRemove(layer) { - if (!layer) { - throw new Error('dia.Paper: The layer view must be provided.'); - } - - const layerView = this._requireLayerView(layer); + 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); }, - insertLayer(layer, insertBefore) { - if (!this.hasLayer(layer.name)) { - throw new Error(`dia.Paper: Unknown layer "${layer.name}".`); + insertLayerView(layerView, insertBefore) { + const layerId = layerView.id; + + if (!this.hasLayerView(layerId)) { + throw new Error(`dia.Paper: Unknown layer "${layerId}".`); } const { _layers: { order }} = this; - const layerName = layer.name; + + // remove from order in case of a move command + if (order.indexOf(layerId) !== -1) { + order.splice(order.indexOf(layerId), 1); + } if (!insertBefore) { - order.push(layerName); - this.layers.appendChild(layer.el); + order.push(layerId); + this.layers.appendChild(layerView.el); } else { const beforeLayerView = this._requireLayerView(insertBefore); - const beforeLayerName = beforeLayerView.name; - order.splice(order.indexOf(beforeLayerName), 0, layerName); - this.layers.insertBefore(layer.el, beforeLayerView.el); + const beforeLayerViewId = beforeLayerView.id; + order.splice(order.indexOf(beforeLayerViewId), 0, layerId); + this.layers.insertBefore(layerView.el, beforeLayerView.el); } }, - moveLayer(layer, insertBefore) { - if (!layer) { - throw new Error('dia.Paper: The layer view is not an instance of dia.LayerView.'); - } - - if (!this.hasLayer(layer.name)) { - throw new Error(`dia.Paper: Unknown layer "${layer.name}".`); - } - - if (insertBefore) { - const insertBeforeLayer = this._requireLayerView(insertBefore); - if (layer === insertBeforeLayer) - return; - } - this._unregisterLayer(layer); - this.insertLayer(layer, insertBefore); - }, - - getLayerNames() { - // Returns a sorted array of layer names. + getLayersOrder() { + // 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.getLayer(name)); + // Returns a sorted array of ordered layer views. + return this.getLayersOrder().map(id => this.getLayerView(id)); }, render: function() { @@ -754,7 +731,7 @@ export const Paper = View.extend({ this.defs = defs; this.layers = layers; - this.renderLayers(this._layersSettings); + this.renderLayerViews(); V.ensureId(svg); @@ -776,14 +753,14 @@ export const Paper = View.extend({ V(this.svg).prepend(V.createSVGStyle(css)); }, - createLayer(options) { + createLayerView(options) { options.paper = this; let type = options.type; if (options.model) { const modelType = options.model.get('type') || options.model.constructor.name; - type = modelType + 'Layer'; + type = modelType + 'View'; } const viewConstructor = this.options.layerViewNamespace[type] || Layer; @@ -791,28 +768,37 @@ export const Paper = View.extend({ return new viewConstructor(options); }, - renderLayer: function(options) { - const layerView = this.createLayer(options); - const layerName = layerView.name; + renderLayerView: function(options) { + const layerView = this.createLayerView(options); + const layerId = layerView.id; - if (this.hasLayer(layerName)) { - throw new Error(`dia.Paper: The layer "${layerName}" already exists.`); + if (this.hasLayerView(layerId)) { + throw new Error(`dia.Paper: The layer "${layerId}" already exists.`); } - this._registerLayer(layerView); + this._registerLayerView(layerView); return layerView; }, - renderLayers: function(layers) { - this.removeLayers(); - layers.forEach(options => { - const layerView = this.renderLayer(options); - this.insertLayer(layerView); + 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, this.model.get('cellLayers')); + // Insert ordered cell layers + this._cellLayers.filter(cellLayer => cellLayer.order != null).sort((a, b) => b.order - a.order).forEach(cellLayer => { + // insert cell layers before the front layer + const layerView = this.getLayerView(cellLayer.id); + this.insertLayerView(layerView, LayersIds.FRONT); }); // Throws an exception if doesn't exist - const cellsLayerView = this.getLayer(LayersNames.CELLS); - const toolsLayerView = this.getLayer(LayersNames.TOOLS); - const labelsLayerView = this.getLayer(LayersNames.LABELS); + const cellsLayerView = this.getLayerView(this.model.defaultCellLayerId); + const toolsLayerView = this.getLayerView(LayersIds.TOOLS); + const labelsLayerView = this.getLayerView(LayersIds.LABELS); // backwards compatibility this.tools = toolsLayerView.el; this.cells = this.viewport = cellsLayerView.el; @@ -825,12 +811,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()); }, @@ -1083,9 +1069,9 @@ 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 GroupLayer) { + if (view instanceof CellLayerView) { if (flag & FLAG_REMOVE) { - this.removeLayer(view); + this.removeLayerView(view); return 0; } } @@ -1519,7 +1505,7 @@ 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) { @@ -1852,7 +1838,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 ? LayersIds.LABELS : options.labelsLayer }); }, @@ -2852,13 +2838,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.getLayer(LayersNames.GRID).renderGrid(); + this.getLayer(LayersIds.GRID).renderGrid(); } return this; }, setGrid: function(drawGrid) { - this.getLayer(LayersNames.GRID).setGrid(drawGrid); + this.getLayer(LayersIds.GRID).setGrid(drawGrid); return this; }, @@ -3207,7 +3193,7 @@ export const Paper = View.extend({ sorting: sortingTypes, - Layers: LayersNames, + Layers: LayersIds, backgroundPatterns: { diff --git a/packages/joint-core/src/dia/ToolsView.mjs b/packages/joint-core/src/dia/ToolsView.mjs index 7f324861f0..4859c6cba9 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 './Paper.mjs'; +import { LayersIds } from './Paper.mjs'; import { ToolView } from './ToolView.mjs'; export const ToolsView = mvc.View.extend({ @@ -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 = LayersIds.TOOLS, z } = options; if (relatedView) { if (layer) { relatedView.paper.getLayer(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..3d75dd2b93 --- /dev/null +++ b/packages/joint-core/src/dia/controllers/CellLayersController.mjs @@ -0,0 +1,201 @@ +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 = this.graph.defaultCellLayerId; + + if (!this.graph.has('cellLayers')) { + this.graph.set('cellLayers', [{ + id: this.defaultCellLayerId, + default: true, + order: 1 + }]); + } + + this.cellLayersMap = {}; + this.cellLayerAttributes = this.graph.get('cellLayers'); + + this.cellLayerAttributes.forEach(attributes => { + const cellLayer = this.createCellLayer(attributes); + this.cellLayersMap[attributes.id] = cellLayer; + }); + + 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.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) => { + if (!layerId) { + layerId = this.defaultCellLayerId; + } + + if (this.hasCellLayer(layerId)) { + this.cellLayersMap[layerId].add(cell); + } + }); + } + + createCellLayer(attributes) { + const cellLayer = new CellLayer(attributes); + + return cellLayer; + } + + onAdd(cell, reset = false) { + const { cellLayersMap } = this; + + const layerId = cell.layer() || this.defaultCellLayerId; + const layer = cellLayersMap[layerId]; + + if (!layer) { + throw new Error(`dia.Graph: Layer with name '${layerId}' does not exist.`); + } + + // 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); + } + } + + // mandatory add to the layer + // so every cell now will have a layer specified + layer.add(cell); + } + + onRemove(cell) { + const { cellLayersMap } = this; + + const layerId = cell.layer() || this.defaultCellLayerId; + + const layer = cellLayersMap[layerId]; + + if (layer) { + 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] = group; + + this.cellLayerAttributes = this.cellLayerAttributes.concat([{ id: cellLayer.id }]); + + this.graph.set('cellLayers', 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 = cellLayersMap[layerId]; + // reset the layer to remove all cells from it + layer.reset(); + + this.cellLayerAttributes = this.cellLayerAttributes.filter(l => l.id !== layerId); + delete cellLayersMap[layerId]; + + this.graph.set('cellLayers', this.cellLayerAttributes); + } + + minZIndex(layerId) { + const { cellLayersMap, defaultCellLayerId } = this; + + layerId = layerId || defaultCellLayerId; + + const layer = cellLayersMap[layerId]; + + return layer.minZIndex(); + } + + maxZIndex(layerId) { + const { cellLayersMap, defaultCellLayerId } = this; + + layerId = layerId || defaultCellLayerId; + + const layer = cellLayersMap[layerId]; + + return layer.maxZIndex(); + } + + hasCellLayer(layerId) { + return !!this.cellLayersMap[layerId]; + } + + getCellLayer(layerId) { + if (!this.cellLayersMap[layerId]) { + throw new Error(`dia.Graph: 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 index adfd54996c..6b8b51e333 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -1,5 +1,5 @@ import { Listener } from '../../mvc/Listener.mjs'; -import { Group } from '../groups/Group.mjs'; +import { CellLayer } from '../groups/CellLayer.mjs'; export class EmbeddingLayersController extends Listener { @@ -16,10 +16,9 @@ export class EmbeddingLayersController extends Listener { const { graph, paper } = this; this.listenTo(graph, 'remove', (_context, cell) => { - if (graph.hasGroup(cell.id)) { - const group = graph.getGroup(cell.id); - group.reset(); - graph.removeGroup(group); + if (graph.hasCellLayer(cell.id)) { + const cellLayer = graph.getCellLayer(cell.id); + graph.removeCellLayer(cellLayer); } }); @@ -47,7 +46,7 @@ export class EmbeddingLayersController extends Listener { this.listenTo(paper, 'cell:inserted', (_context, cellView) => { const cellId = cellView.model.id; - if (paper.hasLayer(cellId)) { + if (paper.hasLayerView(cellId)) { this.insertEmbeddedLayer(cellView); } }); @@ -58,11 +57,11 @@ export class EmbeddingLayersController extends Listener { if (parentId) { // Create new layer if it's not exist - if (!graph.hasGroup(parentId)) { - const group = new Group({ - name: parentId + if (!graph.hasCellLayer(parentId)) { + const cellLayer = new CellLayer({ + id: parentId }); - graph.addGroup(group); + graph.addCellLayer(cellLayer); const cellView = paper.findViewByModel(parentId); if (cellView.isMounted()) { @@ -78,7 +77,7 @@ export class EmbeddingLayersController extends Listener { insertEmbeddedLayer(cellView) { const cellId = cellView.model.id; - const layer = this.paper.getLayer(cellId); - cellView.el.after(layer.el); + const layerView = this.paper.getLayerView(cellId); + cellView.el.after(layerView.el); } } diff --git a/packages/joint-core/src/dia/controllers/GroupsController.mjs b/packages/joint-core/src/dia/controllers/GroupsController.mjs deleted file mode 100644 index e271bcc827..0000000000 --- a/packages/joint-core/src/dia/controllers/GroupsController.mjs +++ /dev/null @@ -1,185 +0,0 @@ -import { Listener } from '../../mvc/Listener.mjs'; - -export class GroupsController extends Listener { - - constructor(context) { - super(context); - - this.graph = context.graph; - - if (!this.graph.has('groups')) { - this.graph.set('groups', []); - } - - this.groups = this.graph.get('groups'); - this.groupsMap = {}; - - this.groups.forEach(layer => { - this.groupsMap[layer.name] = layer; - }); - - this.defaultGroupName = this.graph.defaultGroupName; - - 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.listenTo(graph, 'reset', (_context, { models: cells }) => { - const { groupsMap } = this; - - for (let groupName in groupsMap) { - groupsMap[groupName].reset(); - } - - cells.forEach(cell => { - this.onAdd(cell, true); - }); - }); - - this.listenTo(graph, 'change:layer', (_context, cell, groupName) => { - if (!groupName) { - groupName = this.defaultGroupName; - } - - if (this.hasGroup(groupName)) { - this.groupsMap[groupName].add(cell); - } - }); - } - - onAdd(cell, reset = false) { - const { groupsMap } = this; - - const groupName = cell.layer() || this.defaultGroupName; - const layer = groupsMap[groupName]; - - if (!layer) { - throw new Error(`dia.Graph: Layer with name '${groupName}' does not exist.`); - } - - // 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); - } - } - - // mandatory add to the layer - // so every cell now will have a layer specified - layer.add(cell); - } - - onRemove(cell) { - const { groupsMap } = this; - - const groupName = cell.layer() || this.defaultGroupName; - - const layer = groupsMap[groupName]; - - if (layer) { - layer.remove(cell); - } - } - - getDefaultGroup() { - return this.groupsMap[this.defaultGroupName]; - } - - addGroup(group, _opt) { - const { groupsMap } = this; - - if (groupsMap[group.name]) { - throw new Error(`dia.Graph: Layer with name '${group.name}' already exists.`); - } - - this.groups = this.groups.concat([group]); - - groupsMap[group.name] = group; - - this.graph.set('groups', this.groups); - } - - removeGroup(groupName, _opt) { - const { groupsMap, defaultGroupName } = this; - - if (groupName === defaultGroupName) { - throw new Error('dia.Graph: default layer cannot be removed.'); - } - - if (!groupsMap[groupName]) { - throw new Error(`dia.Graph: Layer with name '${groupName}' does not exist.`); - } - - this.groups = this.groups.filter(l => l.name !== groupName); - - delete this.groupsMap[groupName]; - this.graph.set('groups', this.groups); - } - - minZIndex(groupName) { - const { groupsMap, defaultGroupName } = this; - - groupName = groupName || defaultGroupName; - - const layer = groupsMap[groupName]; - - return layer.minZIndex(); - } - - maxZIndex(groupName) { - const { groupsMap, defaultGroupName } = this; - - groupName = groupName || defaultGroupName; - - const layer = groupsMap[groupName]; - - return layer.maxZIndex(); - } - - hasGroup(groupName) { - return !!this.groupsMap[groupName]; - } - - getGroup(groupName) { - if (!this.groupsMap[groupName]) { - throw new Error(`dia.Graph: Layer with name '${groupName}' does not exist.`); - } - - return this.groupsMap[groupName]; - } - - getGroups() { - const groups = []; - - for (let groupName in this.groupsMap) { - groups.push(this.groupsMap[groupName]); - } - - return groups; - } - - getGroupCells(groupName) { - return this.groupsMap[groupName].get('cells').toArray(); - } - - getCells() { - const cells = []; - - for (let groupName in this.groupsMap) { - cells.push(...this.groupsMap[groupName].get('cells')); - } - - return cells; - } -} 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..d18a0c8537 --- /dev/null +++ b/packages/joint-core/src/dia/groups/CellGroup.mjs @@ -0,0 +1,62 @@ +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 === 'add' || event === 'remove') && collection !== this) return; + 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 attrs.collectionConstructor(); + + // Make all the events fired in the `cells` collection available. + // to the outside world. + this.cells.on('all', this.trigger, this); + } + + add(cell) { + this.cells.add(cell); + } + + remove(cell) { + this.cells.remove(cell); + } + + 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..cb9c99e339 --- /dev/null +++ b/packages/joint-core/src/dia/groups/CellLayer.mjs @@ -0,0 +1,44 @@ +import { CellGroupCollection } 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 Model { + + defaults() { + return { + type: 'CellLayer', + collectionConstructor: CellLayerCollection, + }; + } + + initialize(attrs) { + super.initialize(attrs); + + this.cells.on('change:z', (cell, _, opt) => { + this.cells.sort(); + }); + + this.cells.on('change:layer', (cell, layerId) => { + // If the cell's layer id is changed, we need to remove it from this cell layer. + if (layerId !== this.id) { + this.cells.remove(cell); + } + }); + } + + 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/groups/Group.mjs b/packages/joint-core/src/dia/groups/Group.mjs deleted file mode 100644 index b5a1210046..0000000000 --- a/packages/joint-core/src/dia/groups/Group.mjs +++ /dev/null @@ -1,67 +0,0 @@ -import { Model, Collection } from '../../mvc/index.mjs'; - -export class GroupCollection extends Collection { - - // `comparator` makes it easy to sort cells based on their `z` index. - comparator(model) { - return model.get('z') || 0; - } -} - -export class Group extends Model { - - defaults() { - return { - type: 'Group', - hidden: false, - locked: false, - }; - } - - initialize(attrs) { - super.initialize(attrs); - - this.cells = new GroupCollection(); - - this.cells.on('change:z', (cell, _, opt) => { - this.cells.sort(); - }); - - this.cells.on('change:group', (cell, groupName) => { - // If the cell's group id is changed, we need to remove it from this group. - if (groupName !== this.name) { - this.cells.remove(cell); - } - }); - - // Make all the events fired in the `cells` collection available. - // to the outside world. - this.cells.on('all', this.trigger, this); - } - - add(cell) { - this.cells.add(cell); - } - - remove(cell) { - // unsets the layer making it default for the purpose of the DOM location - cell.layer(null); - this.cells.remove(cell); - } - - reset() { - this.cells.toArray().forEach(cell => { - this.remove(cell); - }); - } - - 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 97f55963d7..b7431c973f 100644 --- a/packages/joint-core/src/dia/index.mjs +++ b/packages/joint-core/src/dia/index.mjs @@ -10,7 +10,8 @@ export * from './Paper.mjs'; export * from './ToolView.mjs'; export * from './ToolsView.mjs'; export * from './HighlighterView.mjs'; -export * from './layers/GroupLayer.mjs'; -export * from './layers/Layer.mjs'; -export * from './layers/GridLayer.mjs'; -export * from './groups/Group.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/GroupLayer.mjs b/packages/joint-core/src/dia/layers/CellLayerView.mjs similarity index 97% rename from packages/joint-core/src/dia/layers/GroupLayer.mjs rename to packages/joint-core/src/dia/layers/CellLayerView.mjs index 1d222abd6a..3ccaabccc1 100644 --- a/packages/joint-core/src/dia/layers/GroupLayer.mjs +++ b/packages/joint-core/src/dia/layers/CellLayerView.mjs @@ -1,8 +1,8 @@ -import { Layer } from './Layer.mjs'; +import { LayerView } from './LayerView.mjs'; import { sortElements } from '../../util/index.mjs'; import { sortingTypes } from '../Paper.mjs'; -export const GroupLayer = Layer.extend({ +export const CellLayerView = LayerView.extend({ SORT_DELAYING_EDGING_BATCHES: ['add', 'reset', 'to-front', 'to-back'], diff --git a/packages/joint-core/src/dia/layers/GridLayer.mjs b/packages/joint-core/src/dia/layers/GridLayerView.mjs similarity index 98% rename from packages/joint-core/src/dia/layers/GridLayer.mjs rename to packages/joint-core/src/dia/layers/GridLayerView.mjs index 660667e174..77a4944969 100644 --- a/packages/joint-core/src/dia/layers/GridLayer.mjs +++ b/packages/joint-core/src/dia/layers/GridLayerView.mjs @@ -1,4 +1,4 @@ -import { Layer } from './Layer.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 = Layer.extend({ +export const GridLayerView = LayerView.extend({ style: { 'pointer-events': 'none' diff --git a/packages/joint-core/src/dia/layers/Layer.mjs b/packages/joint-core/src/dia/layers/LayerView.mjs similarity index 89% rename from packages/joint-core/src/dia/layers/Layer.mjs rename to packages/joint-core/src/dia/layers/LayerView.mjs index 3546f74fcf..3ce03474ec 100644 --- a/packages/joint-core/src/dia/layers/Layer.mjs +++ b/packages/joint-core/src/dia/layers/LayerView.mjs @@ -1,7 +1,7 @@ import { View } from '../../mvc/index.mjs'; import { addClassNamePrefix } from '../../util/util.mjs'; -export const Layer = View.extend({ +export const LayerView = View.extend({ tagName: 'g', svgElement: true, @@ -11,18 +11,17 @@ export const Layer = View.extend({ UPDATE_PRIORITY: 10, options: { - name: '' + id: '' }, init: function() { this.pivotNodes = {}; - this.name = this.options.name; + this.id = this.options.id || this.cid; }, className: function() { - const { name } = this.options; - if (!name) return null; - return addClassNamePrefix(`${name}-layer`); + const { id } = this.options; + return addClassNamePrefix(`${id}-layer`); }, insertSortedNode: function(node, z) { From 5e4b02b70440a1a283002721710a7a6674086065 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 23 Jul 2025 17:15:39 +0200 Subject: [PATCH 44/54] up --- packages/joint-core/src/dia/layers/CellLayerView.mjs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/joint-core/src/dia/layers/CellLayerView.mjs b/packages/joint-core/src/dia/layers/CellLayerView.mjs index 3ccaabccc1..81599646bd 100644 --- a/packages/joint-core/src/dia/layers/CellLayerView.mjs +++ b/packages/joint-core/src/dia/layers/CellLayerView.mjs @@ -37,11 +37,8 @@ export const CellLayerView = LayerView.extend({ } }); - this.listenTo(model, 'updateCell', (cell, opt) => { - if ( - cell.hasChanged('layer') || - (cell.hasChanged('z') && paper.options.sorting === sortingTypes.APPROX) - ) { + this.listenTo(model, 'cell:change:z', (cell, opt) => { + if (paper.options.sorting === sortingTypes.APPROX) { const view = paper.findViewByModel(cell); if (view) { paper.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt); From 8328efe5e15d2744e1f5ed46aa35c8de8bcf5193 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 23 Jul 2025 18:22:17 +0200 Subject: [PATCH 45/54] wip --- packages/joint-core/src/dia/Cell.mjs | 2 +- .../joint-core/src/dia/HighlighterView.mjs | 2 +- packages/joint-core/src/dia/LinkView.mjs | 2 +- packages/joint-core/src/dia/Paper.mjs | 19 +++++++++---------- packages/joint-core/src/dia/ToolsView.mjs | 2 +- .../dia/controllers/CellLayersController.mjs | 2 +- .../joint-core/src/dia/groups/CellGroup.mjs | 2 +- .../joint-core/src/dia/groups/CellLayer.mjs | 6 +++--- .../src/dia/layers/CellLayerView.mjs | 4 ++-- .../src/dia/layers/GridLayerView.mjs | 2 +- 10 files changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index aece4ab97a..e34c98d7fc 100644 --- a/packages/joint-core/src/dia/Cell.mjs +++ b/packages/joint-core/src/dia/Cell.mjs @@ -956,7 +956,7 @@ export const Cell = Model.extend({ // if undefined return the current layer id if (layerId === undefined) { - const layerId = this.get('layer') || null; + 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; diff --git a/packages/joint-core/src/dia/HighlighterView.mjs b/packages/joint-core/src/dia/HighlighterView.mjs index f7b7df9a49..ced89ed005 100644 --- a/packages/joint-core/src/dia/HighlighterView.mjs +++ b/packages/joint-core/src/dia/HighlighterView.mjs @@ -117,7 +117,7 @@ export const HighlighterView = mvc.View.extend({ vGroup = V('g').addClass('highlight-transform').append(el); } this.transformGroup = vGroup; - paper.getLayer(layerName).insertSortedNode(vGroup.node, options.z); + paper.getLayerView(layerName).insertSortedNode(vGroup.node, options.z); } else { // TODO: prepend vs append if (!el.parentNode || el.nextSibling) { diff --git a/packages/joint-core/src/dia/LinkView.mjs b/packages/joint-core/src/dia/LinkView.mjs index 1437dd5194..c1976bf809 100644 --- a/packages/joint-core/src/dia/LinkView.mjs +++ b/packages/joint-core/src/dia/LinkView.mjs @@ -391,7 +391,7 @@ export const LinkView = CellView.extend({ if (!vLabels || !model.hasLabels()) return; const { node } = vLabels; if (options.labelsLayer) { - paper.getLayer(options.labelsLayer).insertSortedNode(node, model.get('z')); + paper.getLayerView(options.labelsLayer).insertSortedNode(node, model.get('z')); } else { if (node.parentNode !== el) { el.appendChild(node); diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index d18f5de431..2d28451b57 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -417,15 +417,14 @@ export const Paper = View.extend({ viewsMap: {}, order: [], }; + // current cell layers model attributes from the Graph + this._cellLayers = []; this.cloneOptions(); this.render(); this._setDimensions(); this.startListening(); - // current cell layers model attributes from the Graph - this._cellLayers = []; - if (options.useLayersForEmbedding) { this.embeddingLayersController = new EmbeddingLayersController({ graph: model, paper: this }); } @@ -494,7 +493,7 @@ export const Paper = View.extend({ }, onGraphReset: function(collection, opt) { - this.resetLayers(); + this.resetLayerViews(); this.resetViews(collection.models, opt); }, @@ -515,7 +514,7 @@ export const Paper = View.extend({ removedCellLayerViewIds.forEach(cellLayerViewId => this.requestLayerViewRemove(cellLayerViewId)); cellLayers.forEach(cellLayer => { - if (!this.hasLayer(cellLayer.id)) { + if (!this.hasLayerView(cellLayer.id)) { const cellLayerModel = this.model.getCellLayer(cellLayer.id); this.renderLayerView({ @@ -659,7 +658,7 @@ export const Paper = View.extend({ layerId = layerView.id; } - if (!this.hasLayer(layerId)) { + if (!this.hasLayerView(layerId)) { throw new Error(`dia.Paper: Unknown layer "${layerId}".`); } return this.getLayerView(layerId); @@ -763,7 +762,7 @@ export const Paper = View.extend({ type = modelType + 'View'; } - const viewConstructor = this.options.layerViewNamespace[type] || Layer; + const viewConstructor = this.options.layerViewNamespace[type] || LayerView; return new viewConstructor(options); }, @@ -1938,7 +1937,7 @@ export const Paper = View.extend({ const { el, model } = view; const layerName = model.layer(); - const layerView = this.getLayer(layerName); + const layerView = this.getLayerView(layerName); layerView.insertCellView(view); @@ -2838,13 +2837,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.getLayer(LayersIds.GRID).renderGrid(); + this.getLayerView(LayersIds.GRID).renderGrid(); } return this; }, setGrid: function(drawGrid) { - this.getLayer(LayersIds.GRID).setGrid(drawGrid); + this.getLayerView(LayersIds.GRID).setGrid(drawGrid); return this; }, diff --git a/packages/joint-core/src/dia/ToolsView.mjs b/packages/joint-core/src/dia/ToolsView.mjs index 4859c6cba9..a4df88d6a6 100644 --- a/packages/joint-core/src/dia/ToolsView.mjs +++ b/packages/joint-core/src/dia/ToolsView.mjs @@ -143,7 +143,7 @@ export const ToolsView = mvc.View.extend({ const { relatedView, layer = LayersIds.TOOLS, z } = options; if (relatedView) { if (layer) { - relatedView.paper.getLayer(layer).insertSortedNode(el, z); + relatedView.paper.getLayerView(layer).insertSortedNode(el, z); } else { relatedView.el.appendChild(el); } diff --git a/packages/joint-core/src/dia/controllers/CellLayersController.mjs b/packages/joint-core/src/dia/controllers/CellLayersController.mjs index 3d75dd2b93..ab918c05ef 100644 --- a/packages/joint-core/src/dia/controllers/CellLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/CellLayersController.mjs @@ -115,7 +115,7 @@ export class CellLayersController extends Listener { throw new Error(`dia.Graph: Layer with id '${cellLayer.id}' already exists.`); } - cellLayersMap[cellLayer.id] = group; + cellLayersMap[cellLayer.id] = cellLayer; this.cellLayerAttributes = this.cellLayerAttributes.concat([{ id: cellLayer.id }]); diff --git a/packages/joint-core/src/dia/groups/CellGroup.mjs b/packages/joint-core/src/dia/groups/CellGroup.mjs index d18a0c8537..aecb41e1f0 100644 --- a/packages/joint-core/src/dia/groups/CellGroup.mjs +++ b/packages/joint-core/src/dia/groups/CellGroup.mjs @@ -39,7 +39,7 @@ export class CellGroup extends Model { initialize(attrs) { super.initialize(attrs); - this.cells = new attrs.collectionConstructor(); + this.cells = new this.attributes.collectionConstructor(); // Make all the events fired in the `cells` collection available. // to the outside world. diff --git a/packages/joint-core/src/dia/groups/CellLayer.mjs b/packages/joint-core/src/dia/groups/CellLayer.mjs index cb9c99e339..d692df39f6 100644 --- a/packages/joint-core/src/dia/groups/CellLayer.mjs +++ b/packages/joint-core/src/dia/groups/CellLayer.mjs @@ -1,4 +1,4 @@ -import { CellGroupCollection } from './CellGroup.mjs'; +import { CellGroupCollection, CellGroup } from './CellGroup.mjs'; export class CellLayerCollection extends CellGroupCollection { @@ -8,7 +8,7 @@ export class CellLayerCollection extends CellGroupCollection { } } -export class CellLayer extends Model { +export class CellLayer extends CellGroup { defaults() { return { @@ -20,7 +20,7 @@ export class CellLayer extends Model { initialize(attrs) { super.initialize(attrs); - this.cells.on('change:z', (cell, _, opt) => { + this.cells.on('change:z', () => { this.cells.sort(); }); diff --git a/packages/joint-core/src/dia/layers/CellLayerView.mjs b/packages/joint-core/src/dia/layers/CellLayerView.mjs index 81599646bd..f5ecf125e3 100644 --- a/packages/joint-core/src/dia/layers/CellLayerView.mjs +++ b/packages/joint-core/src/dia/layers/CellLayerView.mjs @@ -4,10 +4,10 @@ import { sortingTypes } from '../Paper.mjs'; export const CellLayerView = LayerView.extend({ - SORT_DELAYING_EDGING_BATCHES: ['add', 'reset', 'to-front', 'to-back'], + SORT_DELAYING_BATCHES: ['add', 'reset', 'to-front', 'to-back'], init() { - Layer.prototype.init.apply(this, arguments); + LayerView.prototype.init.apply(this, arguments); this.startListening(); }, diff --git a/packages/joint-core/src/dia/layers/GridLayerView.mjs b/packages/joint-core/src/dia/layers/GridLayerView.mjs index 77a4944969..e147107804 100644 --- a/packages/joint-core/src/dia/layers/GridLayerView.mjs +++ b/packages/joint-core/src/dia/layers/GridLayerView.mjs @@ -19,7 +19,7 @@ export const GridLayerView = LayerView.extend({ _gridSettings: null, init() { - Layer.prototype.init.apply(this, arguments); + LayerView.prototype.init.apply(this, arguments); const { options: { paper }} = this; this._gridCache = null; this._gridSettings = []; From 34812b665ff5fc64164d35f29aa9889e4d0eb8d2 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Thu, 24 Jul 2025 13:25:13 +0200 Subject: [PATCH 46/54] up --- packages/joint-core/src/dia/Graph.mjs | 38 +++++++++++-------- packages/joint-core/src/dia/Paper.mjs | 4 +- .../joint-core/src/dia/groups/CellGroup.mjs | 1 - .../joint-core/src/dia/groups/CellLayer.mjs | 2 +- .../src/dia/layers/CellLayerView.mjs | 2 +- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index d9113f1b2e..5d5628777c 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -72,16 +72,15 @@ export const Graph = Model.extend({ // 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); + 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 @@ -107,12 +106,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); + 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) { @@ -203,7 +202,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; }, @@ -217,6 +216,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; @@ -243,7 +251,7 @@ export const Graph = Model.extend({ opt = util.assign({}, opt, { clear: true }); - var collection = this.get('cells'); + var collection = this.cellCollection; if (collection.length === 0) return this; @@ -300,7 +308,7 @@ export const Graph = Model.extend({ return this.addCells(cell, opt); } - this.get('cells').add(this._prepareCell(cell, opt), opt || {}); + this.cellCollection.add(this._prepareCell(cell, opt), opt || {}); return this; }, @@ -333,7 +341,7 @@ export const Graph = Model.extend({ return this._prepareCell(cell, opt); }, this); - this.get('cells').reset(preparedCells, opt); + this.cellCollection.reset(preparedCells, opt); this.stopBatch('reset', opt); @@ -373,7 +381,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 = {}) { @@ -440,7 +448,7 @@ export const Graph = Model.extend({ // Get a cell by `id`. getCell: function(id) { - return this.get('cells').get(id); + return this.cellCollection.get(id); }, getCells: function() { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 2d28451b57..7df61cbe80 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -168,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(); @@ -439,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(); }, diff --git a/packages/joint-core/src/dia/groups/CellGroup.mjs b/packages/joint-core/src/dia/groups/CellGroup.mjs index aecb41e1f0..85a9cacf1a 100644 --- a/packages/joint-core/src/dia/groups/CellGroup.mjs +++ b/packages/joint-core/src/dia/groups/CellGroup.mjs @@ -13,7 +13,6 @@ export class CellGroupCollection extends Collection { _onModelEvent(event, model, collection, options) { if (model) { - if ((event === 'add' || event === 'remove') && collection !== this) return; if (event === 'changeId') { var prevId = this.modelId(model.previousAttributes(), model.idAttribute); var id = this.modelId(model.attributes, model.idAttribute); diff --git a/packages/joint-core/src/dia/groups/CellLayer.mjs b/packages/joint-core/src/dia/groups/CellLayer.mjs index d692df39f6..8806fb00ce 100644 --- a/packages/joint-core/src/dia/groups/CellLayer.mjs +++ b/packages/joint-core/src/dia/groups/CellLayer.mjs @@ -24,7 +24,7 @@ export class CellLayer extends CellGroup { this.cells.sort(); }); - this.cells.on('change:layer', (cell, layerId) => { + this.cells.on('cell:change:layer', (cell, layerId) => { // If the cell's layer id is changed, we need to remove it from this cell layer. if (layerId !== this.id) { this.cells.remove(cell); diff --git a/packages/joint-core/src/dia/layers/CellLayerView.mjs b/packages/joint-core/src/dia/layers/CellLayerView.mjs index f5ecf125e3..cca71de10b 100644 --- a/packages/joint-core/src/dia/layers/CellLayerView.mjs +++ b/packages/joint-core/src/dia/layers/CellLayerView.mjs @@ -30,7 +30,7 @@ export const CellLayerView = LayerView.extend({ } }); - this.listenTo(model, 'add', (cell, opt) => { + this.listenTo(model, 'cell:add', (cell, opt) => { const view = paper.findViewByModel(cell); if (view) { paper.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt); From d861c3575bc9d77b7116fe017d8b52a595d25562 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Thu, 24 Jul 2025 13:45:48 +0200 Subject: [PATCH 47/54] fix --- packages/joint-core/src/dia/groups/CellLayer.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/joint-core/src/dia/groups/CellLayer.mjs b/packages/joint-core/src/dia/groups/CellLayer.mjs index 8806fb00ce..5634835e67 100644 --- a/packages/joint-core/src/dia/groups/CellLayer.mjs +++ b/packages/joint-core/src/dia/groups/CellLayer.mjs @@ -20,7 +20,7 @@ export class CellLayer extends CellGroup { initialize(attrs) { super.initialize(attrs); - this.cells.on('change:z', () => { + this.cells.on('cell:change:z', () => { this.cells.sort(); }); From 3c6aa1d922d66d711589fd542e283f0a0f4e742c Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 25 Jul 2025 10:50:22 +0200 Subject: [PATCH 48/54] fix --- packages/joint-core/src/dia/ElementView.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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. From a852825336a39a915725da0e1f56b8c64b75e39e Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Fri, 25 Jul 2025 16:56:31 +0200 Subject: [PATCH 49/54] final --- packages/joint-core/src/dia/Cell.mjs | 2 +- packages/joint-core/src/dia/Graph.mjs | 1 - packages/joint-core/src/dia/Paper.mjs | 103 +++++--- packages/joint-core/src/dia/ToolsView.mjs | 6 +- .../dia/controllers/CellLayersController.mjs | 40 ++- .../joint-core/src/dia/groups/CellGroup.mjs | 4 +- .../src/dia/layers/CellLayerView.mjs | 28 +- packages/joint-core/test/jointjs/basic.js | 2 +- .../test/jointjs/dia/HighlighterView.js | 4 +- .../joint-core/test/jointjs/dia/LayerView.js | 4 +- packages/joint-core/test/jointjs/dia/Paper.js | 247 +++++++++++------- .../joint-core/test/jointjs/dia/linkTools.js | 2 +- .../joint-core/test/jointjs/layers/basic.js | 18 +- .../test/jointjs/layers/embedding.js | 12 +- packages/joint-core/test/jointjs/paper.js | 10 +- 15 files changed, 272 insertions(+), 211 deletions(-) diff --git a/packages/joint-core/src/dia/Cell.mjs b/packages/joint-core/src/dia/Cell.mjs index e34c98d7fc..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'); diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 5d5628777c..5423d2c8bf 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -5,7 +5,6 @@ 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 { CellLayer } from './groups/CellLayer.mjs'; import { CellLayersController } from './controllers/CellLayersController.mjs'; const GraphCells = Collection.extend({ diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 7df61cbe80..95da183630 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -48,7 +48,7 @@ import $ from '../mvc/Dom/index.mjs'; import { GridLayerView } from './layers/GridLayerView.mjs'; import { EmbeddingLayersController } from './controllers/EmbeddingLayersController.mjs'; -export const LayersIds = { +export const LAYERS = { GRID: 'grid', BACK: 'back', FRONT: 'front', @@ -399,17 +399,17 @@ export const Paper = View.extend({ // Paper layers except the cell layers. this._layersSettings = [{ - id: LayersIds.GRID, + id: LAYERS.GRID, type: 'GridLayerView', patterns: this.constructor.gridPatterns }, { - id: LayersIds.BACK, + id: LAYERS.BACK, }, { - id: LayersIds.LABELS, + id: LAYERS.LABELS, }, { - id: LayersIds.FRONT + id: LAYERS.FRONT }, { - id: LayersIds.TOOLS + id: LAYERS.TOOLS }]; // Layers (SVGGroups) @@ -620,7 +620,7 @@ export const Paper = View.extend({ if (layerId in viewsMap) { return viewsMap[layerId]; } - throw new Error(`dia.Paper: Unknown layer "${layerId}".`); + throw new Error(`dia.Paper: Unknown layer view "${layerId}".`); }, getLayerViewNode(layerId) { @@ -644,6 +644,14 @@ export const Paper = View.extend({ }, _registerLayerView(layerView) { + if (!(layerView instanceof LayerView)) { + throw new Error('dia.Paper: The layer view must be an instance of dia.LayerView.'); + } + + 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; @@ -656,10 +664,12 @@ export const Paper = View.extend({ layerId = layerView; } else if (layerView instanceof LayerView) { layerId = layerView.id; + } else { + throw new Error('dia.Paper: The layer view must be provided.'); } if (!this.hasLayerView(layerId)) { - throw new Error(`dia.Paper: Unknown layer "${layerId}".`); + throw new Error(`dia.Paper: Unknown layer view "${layerId}".`); } return this.getLayerView(layerId); }, @@ -668,7 +678,7 @@ export const Paper = View.extend({ 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._removeLayerView(layerView); }, @@ -683,38 +693,54 @@ export const Paper = View.extend({ }, insertLayerView(layerView, insertBefore) { + if (!(layerView instanceof LayerView)) { + throw new Error('dia.Paper: The layer view must be an instance of dia.LayerView.'); + } + const layerId = layerView.id; if (!this.hasLayerView(layerId)) { - throw new Error(`dia.Paper: Unknown layer "${layerId}".`); + throw new Error(`dia.Paper: Unknown layer view "${layerId}".`); } const { _layers: { order }} = this; - // remove from order in case of a move command - if (order.indexOf(layerId) !== -1) { - order.splice(order.indexOf(layerId), 1); - } - if (!insertBefore) { + // 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); const beforeLayerViewId = beforeLayerView.id; - order.splice(order.indexOf(beforeLayerViewId), 0, layerId); + 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); } }, - getLayersOrder() { + getLayerViewOrder() { // Returns a sorted array of layer view ids. return this._layers.order.slice(); }, - getLayers() { + getOrderedLayerViews() { // Returns a sorted array of ordered layer views. - return this.getLayersOrder().map(id => this.getLayerView(id)); + return this.getLayerViewOrder().map(id => this.getLayerView(id)); }, render: function() { @@ -753,6 +779,10 @@ export const Paper = View.extend({ }, createLayerView(options) { + if (options.id == null) { + throw new Error('dia.Paper: Layer view id is required.'); + } + options.paper = this; let type = options.type; @@ -768,13 +798,16 @@ export const Paper = View.extend({ }, renderLayerView: function(options) { - const layerView = this.createLayerView(options); - const layerId = layerView.id; - - if (this.hasLayerView(layerId)) { - throw new Error(`dia.Paper: The layer "${layerId}" already exists.`); + 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; }, @@ -792,12 +825,12 @@ export const Paper = View.extend({ this._cellLayers.filter(cellLayer => cellLayer.order != null).sort((a, b) => b.order - a.order).forEach(cellLayer => { // insert cell layers before the front layer const layerView = this.getLayerView(cellLayer.id); - this.insertLayerView(layerView, LayersIds.FRONT); + this.insertLayerView(layerView, LAYERS.FRONT); }); // Throws an exception if doesn't exist const cellsLayerView = this.getLayerView(this.model.defaultCellLayerId); - const toolsLayerView = this.getLayerView(LayersIds.TOOLS); - const labelsLayerView = this.getLayerView(LayersIds.LABELS); + const toolsLayerView = this.getLayerView(LAYERS.TOOLS); + const labelsLayerView = this.getLayerView(LAYERS.LABELS); // backwards compatibility this.tools = toolsLayerView.el; this.cells = this.viewport = cellsLayerView.el; @@ -1510,10 +1543,6 @@ export const Paper = View.extend({ if (this.embeddingLayersController) { this.embeddingLayersController.stopListening(); } - - if (this.cellLayersController) { - this.cellLayersController.stopListening(); - } }, getComputedSize: function() { @@ -1837,7 +1866,7 @@ export const Paper = View.extend({ return new ViewClass({ model: cell, interactive: options.interactive, - labelsLayer: options.labelsLayer === true ? LayersIds.LABELS : options.labelsLayer + labelsLayer: options.labelsLayer === true ? LAYERS.LABELS : options.labelsLayer }); }, @@ -1930,11 +1959,11 @@ export const Paper = View.extend({ sortLayersExact: function() { const { _layers: { viewsMap }} = this; - Object.values(viewsMap).filter(view => view instanceof GroupLayer).forEach(view => view.sortExact()); + 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.layer(); const layerView = this.getLayerView(layerName); @@ -2837,13 +2866,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(LayersIds.GRID).renderGrid(); + this.getLayerView(LAYERS.GRID).renderGrid(); } return this; }, setGrid: function(drawGrid) { - this.getLayerView(LayersIds.GRID).setGrid(drawGrid); + this.getLayerView(LAYERS.GRID).setGrid(drawGrid); return this; }, @@ -3192,7 +3221,7 @@ export const Paper = View.extend({ sorting: sortingTypes, - Layers: LayersIds, + Layers: LAYERS, backgroundPatterns: { diff --git a/packages/joint-core/src/dia/ToolsView.mjs b/packages/joint-core/src/dia/ToolsView.mjs index a4df88d6a6..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 { LayersIds } from './Paper.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 = LayersIds.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 index ab918c05ef..7f4072dcfe 100644 --- a/packages/joint-core/src/dia/controllers/CellLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/CellLayersController.mjs @@ -52,14 +52,13 @@ export class CellLayersController extends Listener { }); }); - this.listenTo(graph, 'change:layer', (_context, cell, layerId) => { + this.listenTo(graph, 'change:layer', (_context, cell, layerId, opt) => { if (!layerId) { layerId = this.defaultCellLayerId; } - if (this.hasCellLayer(layerId)) { - this.cellLayersMap[layerId].add(cell); - } + const layer = this.getCellLayer(layerId); + layer.add(cell, opt); }); } @@ -70,14 +69,8 @@ export class CellLayersController extends Listener { } onAdd(cell, reset = false) { - const { cellLayersMap } = this; - const layerId = cell.layer() || this.defaultCellLayerId; - const layer = cellLayersMap[layerId]; - - if (!layer) { - throw new Error(`dia.Graph: Layer with name '${layerId}' does not exist.`); - } + const layer = this.getCellLayer(layerId); // compatibility // in the version before groups, z-index was not set on reset @@ -87,19 +80,16 @@ export class CellLayersController extends Listener { } } - // mandatory add to the layer - // so every cell now will have a layer specified - layer.add(cell); + // 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 { cellLayersMap } = this; - const layerId = cell.layer() || this.defaultCellLayerId; - const layer = cellLayersMap[layerId]; - - if (layer) { + if (this.hasCellLayer(layerId)) { + const layer = this.getCellLayer(layerId); layer.remove(cell); } } @@ -133,7 +123,7 @@ export class CellLayersController extends Listener { throw new Error(`dia.Graph: Layer with id '${layerId}' does not exist.`); } - const layer = cellLayersMap[layerId]; + const layer = this.getCellLayer(layerId); // reset the layer to remove all cells from it layer.reset(); @@ -144,21 +134,21 @@ export class CellLayersController extends Listener { } minZIndex(layerId) { - const { cellLayersMap, defaultCellLayerId } = this; + const { defaultCellLayerId } = this; layerId = layerId || defaultCellLayerId; - const layer = cellLayersMap[layerId]; + const layer = this.getCellLayer(layerId); return layer.minZIndex(); } maxZIndex(layerId) { - const { cellLayersMap, defaultCellLayerId } = this; + const { defaultCellLayerId } = this; layerId = layerId || defaultCellLayerId; - const layer = cellLayersMap[layerId]; + const layer = this.getCellLayer(layerId); return layer.maxZIndex(); } @@ -169,7 +159,7 @@ export class CellLayersController extends Listener { getCellLayer(layerId) { if (!this.cellLayersMap[layerId]) { - throw new Error(`dia.Graph: Layer with id '${layerId}' does not exist.`); + throw new Error(`dia.Graph: Cell layer with id '${layerId}' does not exist.`); } return this.cellLayersMap[layerId]; diff --git a/packages/joint-core/src/dia/groups/CellGroup.mjs b/packages/joint-core/src/dia/groups/CellGroup.mjs index 85a9cacf1a..e687d85bf0 100644 --- a/packages/joint-core/src/dia/groups/CellGroup.mjs +++ b/packages/joint-core/src/dia/groups/CellGroup.mjs @@ -45,8 +45,8 @@ export class CellGroup extends Model { this.cells.on('all', this.trigger, this); } - add(cell) { - this.cells.add(cell); + add(cell, opt) { + this.cells.add(cell, opt); } remove(cell) { diff --git a/packages/joint-core/src/dia/layers/CellLayerView.mjs b/packages/joint-core/src/dia/layers/CellLayerView.mjs index cca71de10b..0fe708b4fa 100644 --- a/packages/joint-core/src/dia/layers/CellLayerView.mjs +++ b/packages/joint-core/src/dia/layers/CellLayerView.mjs @@ -30,14 +30,16 @@ export const CellLayerView = LayerView.extend({ } }); - this.listenTo(model, 'cell:add', (cell, opt) => { - const view = paper.findViewByModel(cell); - if (view) { - paper.requestViewUpdate(view, view.FLAG_INSERT, view.UPDATE_PRIORITY, opt); + 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, opt) => { + this.listenTo(model, 'cell:change:z', (cell, _value, opt) => { if (paper.options.sorting === sortingTypes.APPROX) { const view = paper.findViewByModel(cell); if (view) { @@ -68,11 +70,11 @@ export const CellLayerView = LayerView.extend({ // 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 cells = this.model.get('cells'); + const cellCollection = this.model.cells; sortElements(cellNodes, function(a, b) { - const cellA = cells.get(a.getAttribute('model-id')); - const cellB = cells.get(b.getAttribute('model-id')); + 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; @@ -92,13 +94,5 @@ export const CellLayerView = LayerView.extend({ this.insertNode(el); break; } - }, - - getCellViewNode(cellId) { - const cellNode = this.el.querySelector(`[model-id="${cellId}"]`); - if (!cellNode) { - return null; - } - return cellNode; - }, + } }); diff --git a/packages/joint-core/test/jointjs/basic.js b/packages/joint-core/test/jointjs/basic.js index 3f6f93a57a..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.getLayer('cells'), 'sort'); + 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/LayerView.js b/packages/joint-core/test/jointjs/dia/LayerView.js index 865167e842..95e61d667e 100644 --- a/packages/joint-core/test/jointjs/dia/LayerView.js +++ b/packages/joint-core/test/jointjs/dia/LayerView.js @@ -1,7 +1,7 @@ QUnit.module('joint.dia.LayerView', function(hooks) { - QUnit.test('options: name', function(assert) { - const layer = new joint.dia.LayerView({ 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')); }); diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js index 4e198f34bd..9faf814508 100644 --- a/packages/joint-core/test/jointjs/dia/Paper.js +++ b/packages/joint-core/test/jointjs/dia/Paper.js @@ -977,7 +977,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { rect3.translate(10, 10); assert.ok(sortLayersExactSpy.notCalled); // ADD CELLS - var sortLayerExactSpy = sinon.spy(paper.getLayer('cells'), 'sortExact'); + var sortLayerExactSpy = sinon.spy(paper.getLayerView('cells'), 'sortExact'); graph.clear(); graph.addCells([rect1, rect2, rect3]); assert.equal(sortLayerExactSpy.callCount, paper.options.sorting === Paper.sorting.EXACT ? 1 : 0); @@ -1596,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) { @@ -2097,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({ @@ -2110,83 +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 view 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({ name: 'test' }); + paper.addLayerView({ id: 'test' }); }, - /dia.Paper: The layer view is not an instance of dia.LayerView./, - 'Layer view must be an instance of joint.dia.LayerView.' + /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(new joint.dia.LayerView({ name: 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) { + QUnit.test('adds a new layer view to the Paper', function(assert) { const testLayer = new joint.dia.LayerView({ - name: 'test1' + id: 'test1' }); - assert.equal(paper.getLayerNames().indexOf('test1'), -1); - assert.notOk(paper.hasLayer('test1')); - paper.addLayer(testLayer); - assert.ok(paper.hasLayer('test1')); - assert.equal(paper.getLayers().at(-1), testLayer); + 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.LayerView({ - name: 'test2' - }); - assert.equal(paper.getLayerNames().indexOf('test2'), -1); - assert.notOk(paper.hasLayer('test1')); - paper.addLayer(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() { @@ -2194,7 +2218,7 @@ 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: The layer view must be provided./, 'Layer view must be provided.' @@ -2202,63 +2226,63 @@ QUnit.module('joint.dia.Paper', function(hooks) { }); 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(new joint.dia.LayerView({ name: '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.LayerView({ - name: 'test' + id: 'test' }); - paper.addLayer(testLayer); - assert.ok(paper.hasLayer('test')); - paper.removeLayer(testLayer); - assert.notOk(paper.hasLayer('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: The layer view is not an instance of dia.LayerView./, + /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(new joint.dia.LayerView({ name: '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.' ); }); @@ -2266,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(paper.getLayer(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(paper.getLayer(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(paper.getLayer(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(paper.getLayer(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(paper.getLayer(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); }); @@ -2316,44 +2367,42 @@ QUnit.module('joint.dia.Paper', function(hooks) { 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 r2 = new joint.shapes.standard.Rectangle({ layer: 'test1' }); + const r2 = new joint.shapes.standard.Rectangle({ layer: 'test' }); assert.throws( () => { graph.addCell(r2, { async: false }); }, - /dia.Graph: Layer with name 'test1' does not exist./, - 'Layer "test1" does not exist on Graph Level.' + /dia.Graph: Cell layer with id 'test' does not exist./, + 'Cell layer "test" does not exist in Graph.' ); graph.removeCells([r2], { async: false }); - const test1Layer = new joint.dia.GraphLayer({ name: 'test1' }); - graph.addLayer(test1Layer); - paper.renderLayer({ - name: 'test1', - type: 'GraphLayerView', - model: test1Layer - }); + const testLayer = new joint.dia.CellLayer({ id: 'test' }); + graph.addCellLayer(testLayer); + + assert.ok(paper.hasLayerView('test'), 'Layer view "test" is created in Paper.'); graph.addCell(r2, { async: false }); - assert.ok(paper.getLayerNode('test1').contains(r2.findView(paper).el)); + 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.LayerView({ name: 'test1' }); - paper.addLayer(test1Layer); + const testLayer = new joint.dia.CellLayer({ id: 'test' }); + graph.addCellLayer(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/layers/basic.js b/packages/joint-core/test/jointjs/layers/basic.js index 92bf443298..48b4b02fca 100644 --- a/packages/joint-core/test/jointjs/layers/basic.js +++ b/packages/joint-core/test/jointjs/layers/basic.js @@ -21,21 +21,21 @@ QUnit.module('layers-basic', function(hooks) { }); QUnit.test('Default layers setup', function(assert) { - assert.ok(this.graph.layersController, 'Graph layers controller is created'); + assert.ok(this.graph.cellLayersController, 'Cell layers controller is created'); - const layers = this.graph.get('layers'); + const cellLayers = this.graph.get('cellLayers'); - assert.ok(Array.isArray(layers), 'Graph has layers attribute'); + assert.ok(Array.isArray(cellLayers), 'Graph has cellLayers attribute'); - assert.strictEqual(layers.length, 1, 'Graph has one default layer'); + assert.strictEqual(cellLayers.length, 1, 'Graph has one default cell layer'); - assert.strictEqual(layers[0].name, 'cells', 'Graph has default layer with name "cells"'); + assert.strictEqual(cellLayers[0].id, 'cells', 'Graph has default cell layer with id "cells"'); - assert.ok(this.paper.getLayer('cells'), 'Paper has default layer view for "cells" layer'); + assert.ok(this.paper.getLayerView('cells'), 'Paper has default layer view for "cells" layer'); - const cellsLayerView = this.paper.getLayer('cells'); - const graphDefaultLayer = this.graph.getDefaultLayer(); + const cellsLayerView = this.paper.getLayerView('cells'); + const graphDefaultCellLayer = this.graph.getDefaultCellLayer(); - assert.equal(cellsLayerView.model, graphDefaultLayer, 'Default layer view is linked to the default layer model'); + assert.equal(cellsLayerView.model, graphDefaultCellLayer, 'Default layer view is linked to the default layer model'); }); }); diff --git a/packages/joint-core/test/jointjs/layers/embedding.js b/packages/joint-core/test/jointjs/layers/embedding.js index 65a0213024..176f892006 100644 --- a/packages/joint-core/test/jointjs/layers/embedding.js +++ b/packages/joint-core/test/jointjs/layers/embedding.js @@ -47,15 +47,15 @@ QUnit.module('embedding-layers', function(hooks) { ] }); - assert.ok(this.paper.hasLayer('rect1'), 'Paper has layer for parent cell'); - assert.ok(this.graph.hasLayer('rect1'), 'Graph has layer for parent cell'); + 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.getLayer('rect1'); + const layer = this.graph.getCellLayer('rect1'); - assert.ok(layer.get('cells').has('ellipse1'), 'Graph Layer has cell'); + assert.ok(layer.cells.has('ellipse1'), 'Cell Layer has cell'); - const layerView = this.paper.getLayer('rect1'); + const layerView = this.paper.getLayerView('rect1'); - assert.ok(layerView.getCellViewNode('ellipse1'), 'Layer view has cell view node for embedded cell'); + assert.ok(layerView.el.querySelector(`[model-id="ellipse1"]`), 'Layer view has cell view node for embedded cell'); }); }); diff --git a/packages/joint-core/test/jointjs/paper.js b/packages/joint-core/test/jointjs/paper.js index 5a4bfc86f6..cf6c4b917d 100644 --- a/packages/joint-core/test/jointjs/paper.js +++ b/packages/joint-core/test/jointjs/paper.js @@ -99,7 +99,7 @@ QUnit.module('paper', function(hooks) { QUnit.test('paper.addCell() number of sort()', function(assert) { - var spy = sinon.spy(this.paper.getLayer('cells'), 'sort'); + var spy = sinon.spy(this.paper.getLayerView('cells'), 'sort'); var r1 = new joint.shapes.standard.Rectangle; var r2 = new joint.shapes.standard.Rectangle; @@ -121,7 +121,7 @@ QUnit.module('paper', function(hooks) { QUnit.test('paper.addCells() number of sort()', function(assert) { - var spy = sinon.spy(this.paper.getLayer('cells'), 'sort'); + var spy = sinon.spy(this.paper.getLayerView('cells'), 'sort'); var r1 = new joint.shapes.standard.Rectangle; var r2 = new joint.shapes.standard.Rectangle; @@ -1782,11 +1782,11 @@ QUnit.module('paper', function(hooks) { QUnit.module('draw grid options', function(hooks) { const getGridSettings = function(paper) { - return paper.getLayer(joint.dia.Paper.Layers.GRID)._gridSettings; + return paper.getLayerView(joint.dia.Paper.Layers.GRID)._gridSettings; }; 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) { @@ -1857,7 +1857,7 @@ QUnit.module('paper', function(hooks) { gridSize: 1, drawGridSize: 17 }); - const drawGridSpy = sinon.spy(paper.getLayer(joint.dia.Paper.Layers.GRID), 'renderGrid'); + const drawGridSpy = sinon.spy(paper.getLayerView(joint.dia.Paper.Layers.GRID), 'renderGrid'); paper.setGridSize(5); assert.ok(drawGridSpy.notCalled); drawGridSpy.restore(); From 8782e66b845ccfacd0ce7cbce9d03520f43fec52 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Sat, 26 Jul 2025 13:03:27 +0200 Subject: [PATCH 50/54] typings --- packages/joint-core/types/joint.d.ts | 92 +++++++++++++++++++--------- 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index e18c99e8f1..25b9800124 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -185,8 +185,15 @@ export namespace dia { cellNamespace: any; } + interface CellLayerAttributes { + id: string; + default?: boolean; + order?: number; + } + interface Attributes { cells?: Cells; + cellLayers?: CellLayerAttributes[]; [key: string]: any; } } @@ -205,17 +212,17 @@ export namespace dia { resetCells(cells: Array, opt?: Graph.Options): this; - addLayer(layer: GraphLayer): void; + addCellLayer(layer: CellLayer): void; - removeLayer(layer: GraphLayer): void; + removeCellLayer(layer: CellLayer): void; - getDefaultLayer(): GraphLayer; + getDefaultCellLayer(): CellLayer; - getLayer(name: string): GraphLayer; + getCellLayer(id: string): CellLayer; - hasLayer(name: string): boolean; + hasCellLayer(id: string): boolean; - getLayers(): GraphLayer[]; + getCellLayers(): CellLayer[]; getCell(id: Cell.ID | Cell): Cell; @@ -534,7 +541,7 @@ export namespace dia { z(): number; layer(): string; - layer(name: string | null, opt?: Graph.Options): this; + layer(id: string | null, opt?: Graph.Options): this; angle(): number; @@ -1337,7 +1344,6 @@ export namespace dia { } enum Layers { - CELLS = 'cells', LABELS = 'labels', BACK = 'back', FRONT = 'front', @@ -1769,27 +1775,33 @@ export namespace dia { // layers - getLayerNode(layerName: Paper.Layers | string): SVGGElement; + getLayerViewNode(id: Paper.Layers | string): SVGGElement; + + getLayerView(id: Paper.Layers | string): LayerView; + + hasLayerView(id: Paper.Layers | string): boolean; + + protected removeLayerViews(): void; - getLayer(layerName: Paper.Layers | string): Layer; + protected resetLayerViews(): void; - hasLayer(layerName: Paper.Layers | string): boolean; + renderLayerView(options: Omit): LayerView; - protected removeLayers(): void; + createLayerView(options: Omit): LayerView; - protected resetLayers(): void; + addLayerView(layerView: LayerView): void; - renderLayer(options: Omit): void; + insertLayerView(layerView: LayerView, insertBefore?: string | LayerView): void; - insertLayer(layer: Layer, insertBefore?: string | Layer): void; + removeLayerView(LayerView: LayerView): void; - removeLayer(Layer: Layer): void; + requestLayerViewRemove(layerView: string | LayerView): void; - moveLayer(layer: Layer, insertBefore?: string | Layer): void; + getLayerViewOrder(): string[]; - getLayerNames(): string[]; + getOrderedLayerViews(): Array; - getLayers(): Array; + protected updateCellLayers(graph: Graph, cellLayers: Graph.CellLayerAttributes[]): void; // rendering @@ -1982,20 +1994,20 @@ export namespace dia { scaleContentToFit(opt?: Paper.ScaleContentOptions): void; } - namespace Layer { + namespace LayerView { interface Options extends mvc.ViewOptions { - name: string; + id: string; paper: Paper; type?: string; } } - class Layer extends mvc.View { + class LayerView extends mvc.View { - constructor(opt?: Layer.Options); + constructor(opt?: LayerView.Options); - options: Layer.Options; + options: LayerView.Options; pivotNodes: { [z: number]: Comment }; @@ -2008,26 +2020,50 @@ export namespace dia { removePivots(): void; } - class GraphLayer extends mvc.Model { + namespace CellGroup { + + class CellGroupCollection extends mvc.Collection { + } + + interface Attributes extends mvc.ObjectHash { + type: string; + collectionConstructor: typeof CellGroupCollection; + } + } + + class CellGroup extends mvc.Model { - name: string; + cells: C; - add(cell: Cell): void; + 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 GraphLayer extends Layer { + class CellLayerView extends LayerView { + + protected startListening(): void; protected sort(): void; protected sortExact(): void; + + protected insertCellView(cellView: CellView): void; } namespace ToolsView { From b08e3ec28f823cdf83a6a8c58773153c43fe9912 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 28 Jul 2025 11:28:22 +0200 Subject: [PATCH 51/54] up --- .../src/dia/controllers/CellLayersController.mjs | 8 ++++++++ packages/joint-core/src/dia/groups/CellGroup.mjs | 4 ++-- packages/joint-core/src/dia/groups/CellLayer.mjs | 7 ------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/joint-core/src/dia/controllers/CellLayersController.mjs b/packages/joint-core/src/dia/controllers/CellLayersController.mjs index 7f4072dcfe..b18607b7f1 100644 --- a/packages/joint-core/src/dia/controllers/CellLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/CellLayersController.mjs @@ -56,6 +56,14 @@ export class CellLayersController extends Listener { 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); diff --git a/packages/joint-core/src/dia/groups/CellGroup.mjs b/packages/joint-core/src/dia/groups/CellGroup.mjs index e687d85bf0..3f7f7a083a 100644 --- a/packages/joint-core/src/dia/groups/CellGroup.mjs +++ b/packages/joint-core/src/dia/groups/CellGroup.mjs @@ -49,8 +49,8 @@ export class CellGroup extends Model { this.cells.add(cell, opt); } - remove(cell) { - this.cells.remove(cell); + remove(cell, opt) { + this.cells.remove(cell, opt); } reset() { diff --git a/packages/joint-core/src/dia/groups/CellLayer.mjs b/packages/joint-core/src/dia/groups/CellLayer.mjs index 5634835e67..1f274dc901 100644 --- a/packages/joint-core/src/dia/groups/CellLayer.mjs +++ b/packages/joint-core/src/dia/groups/CellLayer.mjs @@ -23,13 +23,6 @@ export class CellLayer extends CellGroup { this.cells.on('cell:change:z', () => { this.cells.sort(); }); - - this.cells.on('cell:change:layer', (cell, layerId) => { - // If the cell's layer id is changed, we need to remove it from this cell layer. - if (layerId !== this.id) { - this.cells.remove(cell); - } - }); } minZIndex() { From 49f8a8551c9c661918a03337b38d039bcb6bd6c3 Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Mon, 28 Jul 2025 13:55:34 +0200 Subject: [PATCH 52/54] up --- packages/joint-core/src/dia/Graph.mjs | 22 ++++-- packages/joint-core/src/dia/Paper.mjs | 8 +- .../dia/controllers/CellLayersController.mjs | 73 ++++++++++++++---- packages/joint-core/test/jointjs/index.html | 5 +- .../joint-core/test/jointjs/layers/basic.js | 77 ++++++++++++++++++- .../test/jointjs/layers/embedding.js | 64 +++++++++++++++ 6 files changed, 218 insertions(+), 31 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 5423d2c8bf..0d3b4dc453 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -60,8 +60,6 @@ const GraphCells = Collection.extend({ export const Graph = Model.extend({ - defaultCellLayerId: 'cells', - initialize: function(attrs, opt) { opt = opt || {}; @@ -236,14 +234,28 @@ export const Graph = Model.extend({ (attrs = {})[key] = val; } + // Handle `cellLayers` attribute separately. + if (attrs.hasOwnProperty('cellLayers')) { + + } + + 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) { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index 95da183630..edfdc04f4a 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -469,7 +469,7 @@ export const Paper = View.extend({ .listenTo(model, 'remove', this.onCellRemoved) .listenTo(model, 'reset', this.onGraphReset) .listenTo(model, 'batch:stop', this.onGraphBatchStop) - .listenTo(model, 'change:cellLayers', this.updateCellLayers); + .listenTo(model, 'layers:update', this.updateCellLayers); this.on('cell:highlight', this.onCellHighlight) .on('cell:unhighlight', this.onCellUnhighlight) @@ -509,7 +509,7 @@ export const Paper = View.extend({ } }, - updateCellLayers: function(_graph, cellLayers) { + 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)); @@ -820,7 +820,7 @@ export const Paper = View.extend({ this.insertLayerView(layerView); }); // Render the cell layers. - this.updateCellLayers(this.model, this.model.get('cellLayers')); + this.updateCellLayers(this.model.get('cellLayers')); // Insert ordered cell layers this._cellLayers.filter(cellLayer => cellLayer.order != null).sort((a, b) => b.order - a.order).forEach(cellLayer => { // insert cell layers before the front layer @@ -828,7 +828,7 @@ export const Paper = View.extend({ this.insertLayerView(layerView, LAYERS.FRONT); }); // Throws an exception if doesn't exist - const cellsLayerView = this.getLayerView(this.model.defaultCellLayerId); + const cellsLayerView = this.getLayerView(this.model.getDefaultCellLayer().id); const toolsLayerView = this.getLayerView(LAYERS.TOOLS); const labelsLayerView = this.getLayerView(LAYERS.LABELS); // backwards compatibility diff --git a/packages/joint-core/src/dia/controllers/CellLayersController.mjs b/packages/joint-core/src/dia/controllers/CellLayersController.mjs index b18607b7f1..a78458a824 100644 --- a/packages/joint-core/src/dia/controllers/CellLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/CellLayersController.mjs @@ -8,23 +8,10 @@ export class CellLayersController extends Listener { this.graph = context.graph; - this.defaultCellLayerId = this.graph.defaultCellLayerId; - - if (!this.graph.has('cellLayers')) { - this.graph.set('cellLayers', [{ - id: this.defaultCellLayerId, - default: true, - order: 1 - }]); - } + this.defaultCellLayerId = 'cells'; this.cellLayersMap = {}; - this.cellLayerAttributes = this.graph.get('cellLayers'); - - this.cellLayerAttributes.forEach(attributes => { - const cellLayer = this.createCellLayer(attributes); - this.cellLayersMap[attributes.id] = cellLayer; - }); + this.cellLayerAttributes = this.processGraphCellLayersAttribute(this.graph.get('cellLayers')); this.startListening(); } @@ -40,6 +27,21 @@ export class CellLayersController extends Listener { 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.cellLayerAttributes = cellLayers; + + // reset the cell layers map + this.cellLayersMap = {}; + + this.cellLayersAttributes = this.processGraphCellLayersAttribute(cellLayers); + + this.graph.trigger('layers:update', this.cellLayerAttributes); + }); + this.listenTo(graph, 'reset', (_context, { models: cells }) => { const { cellLayersMap } = this; @@ -70,6 +72,41 @@ export class CellLayersController extends Listener { }); } + 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) { + const nextOrder = cellLayerAttributes.reduce((max, attrs) => { + return Math.max(max, attrs.order || 0); + }, 0) + 1; + + cellLayerAttributes.push({ + id: this.defaultCellLayerId, + default: true, + order: nextOrder + }); + } + + 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 }); + return cellLayerAttributes; + } + createCellLayer(attributes) { const cellLayer = new CellLayer(attributes); @@ -117,7 +154,8 @@ export class CellLayersController extends Listener { this.cellLayerAttributes = this.cellLayerAttributes.concat([{ id: cellLayer.id }]); - this.graph.set('cellLayers', this.cellLayerAttributes); + this.graph.set('cellLayers', this.cellLayerAttributes, { controller: this }); + this.graph.trigger('layers:update', this.cellLayerAttributes); } removeCellLayer(layerId, _opt) { @@ -138,7 +176,8 @@ export class CellLayersController extends Listener { this.cellLayerAttributes = this.cellLayerAttributes.filter(l => l.id !== layerId); delete cellLayersMap[layerId]; - this.graph.set('cellLayers', this.cellLayerAttributes); + this.graph.set('cellLayers', this.cellLayerAttributes, { controller: this }); + this.graph.trigger('layers:update', this.cellLayerAttributes); } minZIndex(layerId) { diff --git a/packages/joint-core/test/jointjs/index.html b/packages/joint-core/test/jointjs/index.html index 3b05711a2d..df5131727f 100644 --- a/packages/joint-core/test/jointjs/index.html +++ b/packages/joint-core/test/jointjs/index.html @@ -13,7 +13,8 @@ - + + diff --git a/packages/joint-core/test/jointjs/layers/basic.js b/packages/joint-core/test/jointjs/layers/basic.js index 48b4b02fca..ee4ed47a0b 100644 --- a/packages/joint-core/test/jointjs/layers/basic.js +++ b/packages/joint-core/test/jointjs/layers/basic.js @@ -1,6 +1,6 @@ QUnit.module('layers-basic', function(hooks) { - hooks.beforeEach(function() { + hooks.beforeEach(() => { const fixtureEl = fixtures.getElement(); const paperEl = document.createElement('div'); @@ -13,14 +13,14 @@ QUnit.module('layers-basic', function(hooks) { }); }); - hooks.afterEach(function() { + hooks.afterEach(() => { this.paper.remove(); this.graph = null; this.paper = null; }); - QUnit.test('Default layers setup', function(assert) { + QUnit.test('Default layers setup', (assert) => { assert.ok(this.graph.cellLayersController, 'Cell layers controller is created'); const cellLayers = this.graph.get('cellLayers'); @@ -38,4 +38,75 @@ QUnit.module('layers-basic', function(hooks) { 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 index 176f892006..f1e39bc216 100644 --- a/packages/joint-core/test/jointjs/layers/embedding.js +++ b/packages/joint-core/test/jointjs/layers/embedding.js @@ -27,8 +27,47 @@ QUnit.module('embedding-layers', function(hooks) { 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('from JSON', (assert) => { this.graph.fromJSON({ + cellLayers: [{ + id: 'cells', + default: true, + order: 1 + }, { + id: 'rect1', + }], cells: [ { type: 'standard.Rectangle', @@ -58,4 +97,29 @@ QUnit.module('embedding-layers', function(hooks) { 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,"order":1},{"id":"rect1"}],"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'); + }); }); From c32030d32db08fc7cdb34b99c25a13e4f79204bc Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Tue, 29 Jul 2025 15:46:56 +0200 Subject: [PATCH 53/54] wip --- packages/joint-core/src/dia/Graph.mjs | 5 --- packages/joint-core/src/dia/Paper.mjs | 9 ++-- .../dia/controllers/CellLayersController.mjs | 9 +--- .../controllers/EmbeddingLayersController.mjs | 19 +++++---- packages/joint-core/test/jointjs/index.html | 4 +- .../test/jointjs/layers/embedding.js | 41 +------------------ 6 files changed, 20 insertions(+), 67 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 0d3b4dc453..1d3c0579db 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -234,11 +234,6 @@ export const Graph = Model.extend({ (attrs = {})[key] = val; } - // Handle `cellLayers` attribute separately. - if (attrs.hasOwnProperty('cellLayers')) { - - } - let cells = attrs.cells; // Make sure that `cells` attribute is handled separately via resetCells(). if (cells) { diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index edfdc04f4a..d3c766a61c 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -522,6 +522,9 @@ export const Paper = View.extend({ model: cellLayerModel }); } + + const layerView = this.getLayerView(cellLayer.id); + this.insertLayerView(layerView, LAYERS.FRONT); }); this._cellLayers = cellLayers; @@ -821,12 +824,6 @@ export const Paper = View.extend({ }); // Render the cell layers. this.updateCellLayers(this.model.get('cellLayers')); - // Insert ordered cell layers - this._cellLayers.filter(cellLayer => cellLayer.order != null).sort((a, b) => b.order - a.order).forEach(cellLayer => { - // insert cell layers before the front layer - const layerView = this.getLayerView(cellLayer.id); - this.insertLayerView(layerView, LAYERS.FRONT); - }); // Throws an exception if doesn't exist const cellsLayerView = this.getLayerView(this.model.getDefaultCellLayer().id); const toolsLayerView = this.getLayerView(LAYERS.TOOLS); diff --git a/packages/joint-core/src/dia/controllers/CellLayersController.mjs b/packages/joint-core/src/dia/controllers/CellLayersController.mjs index a78458a824..95c0a2ebb7 100644 --- a/packages/joint-core/src/dia/controllers/CellLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/CellLayersController.mjs @@ -80,20 +80,13 @@ export class CellLayersController extends Listener { 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) { - const nextOrder = cellLayerAttributes.reduce((max, attrs) => { - return Math.max(max, attrs.order || 0); - }, 0) + 1; - cellLayerAttributes.push({ id: this.defaultCellLayerId, - default: true, - order: nextOrder + default: true }); } - if (defaultLayers.length === 1) { this.defaultCellLayerId = defaultLayers[0].id; } diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 6b8b51e333..7b47fccc14 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -9,16 +9,17 @@ export class EmbeddingLayersController extends Listener { this.graph = context.graph; this.paper = context.paper; + this.embeddedCellLayers = {}; + this.startListening(); } startListening() { - const { graph, paper } = this; + const { graph, paper, embeddedCellLayers } = this; this.listenTo(graph, 'remove', (_context, cell) => { - if (graph.hasCellLayer(cell.id)) { - const cellLayer = graph.getCellLayer(cell.id); - graph.removeCellLayer(cellLayer); + if (embeddedCellLayers[cell.id]) { + paper.requestLayerViewRemove(cell.id); } }); @@ -53,15 +54,19 @@ export class EmbeddingLayersController extends Listener { } onParentChange(cell, parentId) { - const { graph, paper } = this; + const { paper, embeddedCellLayers } = this; if (parentId) { // Create new layer if it's not exist - if (!graph.hasCellLayer(parentId)) { + if (!embeddedCellLayers[parentId]) { const cellLayer = new CellLayer({ id: parentId }); - graph.addCellLayer(cellLayer); + embeddedCellLayers[cellLayer] = cellLayer; + paper.renderLayerView({ + id: parentId, + model: cellLayer, + }); const cellView = paper.findViewByModel(parentId); if (cellView.isMounted()) { diff --git a/packages/joint-core/test/jointjs/index.html b/packages/joint-core/test/jointjs/index.html index df5131727f..3668ec27c3 100644 --- a/packages/joint-core/test/jointjs/index.html +++ b/packages/joint-core/test/jointjs/index.html @@ -14,7 +14,7 @@ - + diff --git a/packages/joint-core/test/jointjs/layers/embedding.js b/packages/joint-core/test/jointjs/layers/embedding.js index f1e39bc216..9f9f4e6269 100644 --- a/packages/joint-core/test/jointjs/layers/embedding.js +++ b/packages/joint-core/test/jointjs/layers/embedding.js @@ -59,44 +59,6 @@ QUnit.module('embedding-layers', function(hooks) { assert.ok(layerView.el.querySelector(`[model-id="ellipse1"]`), 'Layer view has cell view node for embedded cell'); }); - QUnit.test('from JSON', (assert) => { - this.graph.fromJSON({ - cellLayers: [{ - id: 'cells', - default: true, - order: 1 - }, { - id: 'rect1', - }], - 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({ @@ -120,6 +82,7 @@ QUnit.module('embedding-layers', function(hooks) { const toJSONstring = JSON.stringify(this.graph.toJSON()); - assert.equal(toJSONstring, '{"cellLayers":[{"id":"cells","default":true,"order":1},{"id":"rect1"}],"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'); + + 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"}]}', 'toJSON returns correct JSON structure'); }); }); From 57f18b0361b2dd3f12aa3ad49b8858138f3562fa Mon Sep 17 00:00:00 2001 From: Arthur Khokhlov Date: Wed, 30 Jul 2025 13:04:06 +0200 Subject: [PATCH 54/54] update --- packages/joint-core/src/dia/Graph.mjs | 4 ++ packages/joint-core/src/dia/Paper.mjs | 7 +-- .../dia/controllers/CellLayersController.mjs | 43 ++++++++++++++----- .../controllers/EmbeddingLayersController.mjs | 13 +++--- packages/joint-core/test/jointjs/dia/Paper.js | 2 + .../test/jointjs/layers/embedding.js | 2 +- 6 files changed, 49 insertions(+), 22 deletions(-) diff --git a/packages/joint-core/src/dia/Graph.mjs b/packages/joint-core/src/dia/Graph.mjs index 1d3c0579db..efc04e5153 100644 --- a/packages/joint-core/src/dia/Graph.mjs +++ b/packages/joint-core/src/dia/Graph.mjs @@ -427,6 +427,10 @@ export const Graph = Model.extend({ this.cellLayersController.addCellLayer(cellLayer, opt); }, + insertCellLayer(cellLayer, insertAt) { + this.cellLayersController.insertCellLayer(cellLayer, insertAt); + }, + removeCellLayer(cellLayer, opt) { this.cellLayersController.removeCellLayer(cellLayer.id, opt); }, diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index d3c766a61c..593d5474f2 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -492,9 +492,9 @@ export const Paper = View.extend({ if (view) this.requestViewUpdate(view, view.FLAG_REMOVE, view.UPDATE_PRIORITY, opt); }, - onGraphReset: function(collection, opt) { + onGraphReset: function(_collection, opt) { this.resetLayerViews(); - this.resetViews(collection.models, opt); + this.resetViews(this.model.getCells(), opt); }, onGraphBatchStop: function(data) { @@ -513,7 +513,8 @@ export const Paper = View.extend({ const removedCellLayerViewIds = this._cellLayers.filter(cellLayerView => !cellLayers.some(l => l.id === cellLayerView.id)).map(cellLayerView => cellLayerView.id); removedCellLayerViewIds.forEach(cellLayerViewId => this.requestLayerViewRemove(cellLayerViewId)); - cellLayers.forEach(cellLayer => { + // reverse cellLayers array to render it in order + [...cellLayers].reverse().forEach(cellLayer => { if (!this.hasLayerView(cellLayer.id)) { const cellLayerModel = this.model.getCellLayer(cellLayer.id); diff --git a/packages/joint-core/src/dia/controllers/CellLayersController.mjs b/packages/joint-core/src/dia/controllers/CellLayersController.mjs index 95c0a2ebb7..957b3216d8 100644 --- a/packages/joint-core/src/dia/controllers/CellLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/CellLayersController.mjs @@ -32,14 +32,7 @@ export class CellLayersController extends Listener { return; // do not process changes triggered by this controller } - this.cellLayerAttributes = cellLayers; - - // reset the cell layers map - this.cellLayersMap = {}; - this.cellLayersAttributes = this.processGraphCellLayersAttribute(cellLayers); - - this.graph.trigger('layers:update', this.cellLayerAttributes); }); this.listenTo(graph, 'reset', (_context, { models: cells }) => { @@ -97,6 +90,7 @@ export class CellLayersController extends Listener { }); this.graph.set('cellLayers', cellLayerAttributes, { controller: this }); + this.graph.trigger('layers:update', cellLayerAttributes); return cellLayerAttributes; } @@ -144,8 +138,31 @@ export class CellLayersController extends Listener { } 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 = this.cellLayerAttributes.concat([{ id: cellLayer.id }]); + this.cellLayerAttributes.splice(insertAt, 0, attributes); this.graph.set('cellLayers', this.cellLayerAttributes, { controller: this }); this.graph.trigger('layers:update', this.cellLayerAttributes); @@ -166,11 +183,15 @@ export class CellLayersController extends Listener { // reset the layer to remove all cells from it layer.reset(); - this.cellLayerAttributes = this.cellLayerAttributes.filter(l => l.id !== layerId); delete cellLayersMap[layerId]; - this.graph.set('cellLayers', this.cellLayerAttributes, { controller: this }); - this.graph.trigger('layers:update', this.cellLayerAttributes); + // 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) { diff --git a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs index 7b47fccc14..7bc147420a 100644 --- a/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs +++ b/packages/joint-core/src/dia/controllers/EmbeddingLayersController.mjs @@ -9,16 +9,15 @@ export class EmbeddingLayersController extends Listener { this.graph = context.graph; this.paper = context.paper; - this.embeddedCellLayers = {}; - this.startListening(); } startListening() { - const { graph, paper, embeddedCellLayers } = this; + const { graph, paper } = this; this.listenTo(graph, 'remove', (_context, cell) => { - if (embeddedCellLayers[cell.id]) { + if (graph.hasCellLayer(cell.id)) { + graph.removeCellLayer(cell.id); paper.requestLayerViewRemove(cell.id); } }); @@ -54,15 +53,15 @@ export class EmbeddingLayersController extends Listener { } onParentChange(cell, parentId) { - const { paper, embeddedCellLayers } = this; + const { paper, graph } = this; if (parentId) { // Create new layer if it's not exist - if (!embeddedCellLayers[parentId]) { + if (!graph.hasCellLayer(parentId)) { const cellLayer = new CellLayer({ id: parentId }); - embeddedCellLayers[cellLayer] = cellLayer; + graph.addCellLayer(cellLayer); paper.renderLayerView({ id: parentId, model: cellLayer, diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js index 9faf814508..94b7e2c383 100644 --- a/packages/joint-core/test/jointjs/dia/Paper.js +++ b/packages/joint-core/test/jointjs/dia/Paper.js @@ -2382,6 +2382,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { 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.'); @@ -2397,6 +2398,7 @@ QUnit.module('joint.dia.Paper', function(hooks) { 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.'); diff --git a/packages/joint-core/test/jointjs/layers/embedding.js b/packages/joint-core/test/jointjs/layers/embedding.js index 9f9f4e6269..d9f2f71133 100644 --- a/packages/joint-core/test/jointjs/layers/embedding.js +++ b/packages/joint-core/test/jointjs/layers/embedding.js @@ -83,6 +83,6 @@ QUnit.module('embedding-layers', function(hooks) { 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"}]}', 'toJSON returns correct JSON structure'); + 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'); }); });