.
* This way we are always able to correctly figure out the size of the svg element
* by querying the parent node.
* (It is not possible to get the size of a svg element cross browser @ 2014-04-01)
*
*
*
*
* @param {CanvasConfig} config
*/
Canvas.prototype._init = function(config) {
const eventBus = this._eventBus;
// html container
const container = this._container = createContainer(config);
const svg = this._svg = svgCreate('svg');
svgAttr(svg, { width: '100%', height: '100%' });
svgAppend(container, svg);
const viewport = this._viewport = createGroup(svg, 'viewport');
// debounce canvas.viewbox.changed events
// for smoother diagram interaction
if (config.deferUpdate !== false) {
this._viewboxChanged = debounce(bind(this._viewboxChanged, this), 300);
}
eventBus.on('diagram.init', () => {
/**
* An event indicating that the canvas is ready to be drawn on.
*
* @memberOf Canvas
*
* @event canvas.init
*
* @type {Object}
* @property {SVGElement} svg the created svg element
* @property {SVGElement} viewport the direct parent of diagram elements and shapes
*/
eventBus.fire('canvas.init', {
svg: svg,
viewport: viewport
});
});
// reset viewbox on shape changes to
// recompute the viewbox
eventBus.on([
'shape.added',
'connection.added',
'shape.removed',
'connection.removed',
'elements.changed',
'root.set'
], () => {
delete this._cachedViewbox;
});
eventBus.on('diagram.destroy', 500, this._destroy, this);
eventBus.on('diagram.clear', 500, this._clear, this);
};
Canvas.prototype._destroy = function() {
this._eventBus.fire('canvas.destroy', {
svg: this._svg,
viewport: this._viewport
});
const parent = this._container.parentNode;
if (parent) {
parent.removeChild(this._container);
}
delete this._svg;
delete this._container;
delete this._layers;
delete this._planes;
delete this._rootElement;
delete this._viewport;
};
Canvas.prototype._clear = function() {
const allElements = this._elementRegistry.getAll();
// remove all elements
allElements.forEach(element => {
const type = getType(element);
if (type === 'root') {
this.removeRootElement(element);
} else {
this._removeElement(element, type);
}
});
// remove all planes
this._planes = [];
this._rootElement = null;
// force recomputation of view box
delete this._cachedViewbox;
};
/**
* Returns the default layer on which
* all elements are drawn.
*
* @return {SVGElement} The SVG element of the layer.
*/
Canvas.prototype.getDefaultLayer = function() {
return this.getLayer(BASE_LAYER, PLANE_LAYER_INDEX);
};
/**
* Returns a layer that is used to draw elements
* or annotations on it.
*
* Non-existing layers retrieved through this method
* will be created. During creation, the optional index
* may be used to create layers below or above existing layers.
* A layer with a certain index is always created above all
* existing layers with the same index.
*
* @param {string} name The name of the layer.
* @param {number} [index] The index of the layer.
*
* @return {SVGElement} The SVG element of the layer.
*/
Canvas.prototype.getLayer = function(name, index) {
if (!name) {
throw new Error('must specify a name');
}
let layer = this._layers[name];
if (!layer) {
layer = this._layers[name] = this._createLayer(name, index);
}
// throw an error if layer creation / retrival is
// requested on different index
if (typeof index !== 'undefined' && layer.index !== index) {
throw new Error('layer <' + name + '> already created at index <' + index + '>');
}
return layer.group;
};
/**
* For a given index, return the number of layers that have a higher index and
* are visible.
*
* This is used to determine the node a layer should be inserted at.
*
* @param {number} index
*
* @return {number}
*/
Canvas.prototype._getChildIndex = function(index) {
return reduce(this._layers, function(childIndex, layer) {
if (layer.visible && index >= layer.index) {
childIndex++;
}
return childIndex;
}, 0);
};
/**
* Creates a given layer and returns it.
*
* @param {string} name
* @param {number} [index=0]
*
* @return {CanvasLayer}
*/
Canvas.prototype._createLayer = function(name, index) {
if (typeof index === 'undefined') {
index = UTILITY_LAYER_INDEX;
}
const childIndex = this._getChildIndex(index);
return {
group: createGroup(this._viewport, 'layer-' + name, childIndex),
index: index,
visible: true
};
};
/**
* Shows a given layer.
*
* @param {string} layer The name of the layer.
*
* @return {SVGElement} The SVG element of the layer.
*/
Canvas.prototype.showLayer = function(name) {
if (!name) {
throw new Error('must specify a name');
}
const layer = this._layers[name];
if (!layer) {
throw new Error('layer <' + name + '> does not exist');
}
const viewport = this._viewport;
const group = layer.group;
const index = layer.index;
if (layer.visible) {
return group;
}
const childIndex = this._getChildIndex(index);
viewport.insertBefore(group, viewport.childNodes[childIndex] || null);
layer.visible = true;
return group;
};
/**
* Hides a given layer.
*
* @param {string} layer The name of the layer.
*
* @return {SVGElement} The SVG element of the layer.
*/
Canvas.prototype.hideLayer = function(name) {
if (!name) {
throw new Error('must specify a name');
}
const layer = this._layers[name];
if (!layer) {
throw new Error('layer <' + name + '> does not exist');
}
const group = layer.group;
if (!layer.visible) {
return group;
}
svgRemove(group);
layer.visible = false;
return group;
};
Canvas.prototype._removeLayer = function(name) {
const layer = this._layers[name];
if (layer) {
delete this._layers[name];
svgRemove(layer.group);
}
};
/**
* Returns the currently active layer. Can be null.
*
* @return {CanvasLayer|null} The active layer of `null`.
*/
Canvas.prototype.getActiveLayer = function() {
const plane = this._findPlaneForRoot(this.getRootElement());
if (!plane) {
return null;
}
return plane.layer;
};
/**
* Returns the plane which contains the given element.
*
* @param {ShapeLike|ConnectionLike|string} element The element or its ID.
*
* @return {RootLike|undefined} The root of the element.
*/
Canvas.prototype.findRoot = function(element) {
if (typeof element === 'string') {
element = this._elementRegistry.get(element);
}
if (!element) {
return;
}
const plane = this._findPlaneForRoot(
findRoot(element)
) || {};
return plane.rootElement;
};
/**
* Return a list of all root elements on the diagram.
*
* @return {(RootLike)[]} The list of root elements.
*/
Canvas.prototype.getRootElements = function() {
return this._planes.map(function(plane) {
return plane.rootElement;
});
};
Canvas.prototype._findPlaneForRoot = function(rootElement) {
return find(this._planes, function(plane) {
return plane.rootElement === rootElement;
});
};
/**
* Returns the html element that encloses the
* drawing canvas.
*
* @return {HTMLElement} The HTML element of the container.
*/
Canvas.prototype.getContainer = function() {
return this._container;
};
// markers //////////////////////
Canvas.prototype._updateMarker = function(element, marker, add) {
let container;
if (!element.id) {
element = this._elementRegistry.get(element);
}
// we need to access all
container = this._elementRegistry._elements[element.id];
if (!container) {
return;
}
forEach([ container.gfx, container.secondaryGfx ], function(gfx) {
if (gfx) {
// invoke either addClass or removeClass based on mode
if (add) {
svgClasses(gfx).add(marker);
} else {
svgClasses(gfx).remove(marker);
}
}
});
/**
* An event indicating that a marker has been updated for an element
*
* @event element.marker.update
* @type {Object}
* @property {Base} element the shape
* @property {SVGElement} gfx the graphical representation of the shape
* @property {string} marker
* @property {boolean} add true if the marker was added, false if it got removed
*/
this._eventBus.fire('element.marker.update', { element: element, gfx: container.gfx, marker: marker, add: !!add });
};
/**
* Adds a marker to an element (basically a css class).
*
* Fires the element.marker.update event, making it possible to
* integrate extension into the marker life-cycle, too.
*
* @example
*
* canvas.addMarker('foo', 'some-marker');
*
* const fooGfx = canvas.getGraphics('foo');
*
* fooGfx; //
...
*
* @param {ShapeLike|ConnectionLike|string} element The element or its ID.
* @param {string} marker The marker.
*/
Canvas.prototype.addMarker = function(element, marker) {
this._updateMarker(element, marker, true);
};
/**
* Remove a marker from an element.
*
* Fires the element.marker.update event, making it possible to
* integrate extension into the marker life-cycle, too.
*
* @param {ShapeLike|ConnectionLike|string} element The element or its ID.
* @param {string} marker The marker.
*/
Canvas.prototype.removeMarker = function(element, marker) {
this._updateMarker(element, marker, false);
};
/**
* Check whether an element has a given marker.
*
* @param {ShapeLike|ConnectionLike|string} element The element or its ID.
* @param {string} marker The marker.
*/
Canvas.prototype.hasMarker = function(element, marker) {
if (!element.id) {
element = this._elementRegistry.get(element);
}
const gfx = this.getGraphics(element);
return svgClasses(gfx).has(marker);
};
/**
* Toggles a marker on an element.
*
* Fires the element.marker.update event, making it possible to
* integrate extension into the marker life-cycle, too.
*
* @param {ShapeLike|ConnectionLike|string} element The element or its ID.
* @param {string} marker The marker.
*/
Canvas.prototype.toggleMarker = function(element, marker) {
if (this.hasMarker(element, marker)) {
this.removeMarker(element, marker);
} else {
this.addMarker(element, marker);
}
};
/**
* Returns the current root element.
*
* Supports two different modes for handling root elements:
*
* 1. if no root element has been added before, an implicit root will be added
* and returned. This is used in applications that don't require explicit
* root elements.
*
* 2. when root elements have been added before calling `getRootElement`,
* root elements can be null. This is used for applications that want to manage
* root elements themselves.
*
* @return {RootLike} The current root element.
*/
Canvas.prototype.getRootElement = function() {
const rootElement = this._rootElement;
// can return null if root elements are present but none was set yet
if (rootElement || this._planes.length) {
return rootElement;
}
return this.setRootElement(this.addRootElement(null));
};
/**
* Adds a given root element and returns it.
*
* @param {ShapeLike} [rootElement] The root element to be added.
*
* @return {RootLike} The added root element or an implicit root element.
*/
Canvas.prototype.addRootElement = function(rootElement) {
const idx = this._rootsIdx++;
if (!rootElement) {
rootElement = {
id: '__implicitroot_' + idx,
children: [],
isImplicit: true
};
}
const layerName = rootElement.layer = 'root-' + idx;
this._ensureValid('root', rootElement);
const layer = this.getLayer(layerName, PLANE_LAYER_INDEX);
this.hideLayer(layerName);
this._addRoot(rootElement, layer);
this._planes.push({
rootElement: rootElement,
layer: layer
});
return rootElement;
};
/**
* Removes a given root element and returns it.
*
* @param {ShapeLike|string} rootElement The root element or its ID.
*
* @return {ShapeLike|undefined} The removed root element.
*/
Canvas.prototype.removeRootElement = function(rootElement) {
if (typeof rootElement === 'string') {
rootElement = this._elementRegistry.get(rootElement);
}
const plane = this._findPlaneForRoot(rootElement);
if (!plane) {
return;
}
// hook up life-cycle events
this._removeRoot(rootElement);
// clean up layer
this._removeLayer(rootElement.layer);
// clean up plane
this._planes = this._planes.filter(function(plane) {
return plane.rootElement !== rootElement;
});
// clean up active root
if (this._rootElement === rootElement) {
this._rootElement = null;
}
return rootElement;
};
/**
* Sets a given element as the new root element for the canvas
* and returns the new root element.
*
* @param {RootLike} rootElement The root element to be set.
*
* @return {RootLike} The set root element.
*/
Canvas.prototype.setRootElement = function(rootElement, override) {
if (isDefined(override)) {
throw new Error('override not supported');
}
if (rootElement === this._rootElement) {
return;
}
let plane;
if (!rootElement) {
throw new Error('rootElement required');
}
plane = this._findPlaneForRoot(rootElement);
// give set add semantics for backwards compatibility
if (!plane) {
rootElement = this.addRootElement(rootElement);
}
this._setRoot(rootElement);
return rootElement;
};
Canvas.prototype._removeRoot = function(element) {
const elementRegistry = this._elementRegistry,
eventBus = this._eventBus;
// simulate element remove event sequence
eventBus.fire('root.remove', { element: element });
eventBus.fire('root.removed', { element: element });
elementRegistry.remove(element);
};
Canvas.prototype._addRoot = function(element, gfx) {
const elementRegistry = this._elementRegistry,
eventBus = this._eventBus;
// resemble element add event sequence
eventBus.fire('root.add', { element: element });
elementRegistry.add(element, gfx);
eventBus.fire('root.added', { element: element, gfx: gfx });
};
Canvas.prototype._setRoot = function(rootElement, layer) {
const currentRoot = this._rootElement;
if (currentRoot) {
// un-associate previous root element