diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
259 lines (214 loc) • 6.36 kB
JavaScript
import { getBBox } from '../../util/Elements.js';
var LOW_PRIORITY = 500;
import {
append as svgAppend,
attr as svgAttr,
create as svgCreate
} from 'tiny-svg';
import {
query as domQuery
} from 'min-dom';
import {
assign,
forEach,
isFunction
} from 'min-dash';
var DEFAULT_PRIORITY = 1000;
/**
* @typedef {import('../../model/Types.js').Element} Element
*
* @typedef {import('./OutlineProvider.js').default} OutlineProvider
* @typedef {import('../../core/EventBus.js').default} EventBus
* @typedef {import('../../core/ElementRegistry.js').default} ElementRegistry
* @typedef {import('../../draw/Styles.js').default} Styles
*/
/**
* @class
*
* A plugin that adds an outline to shapes and connections that may be activated and styled
* via CSS classes.
*
* The outline is created lazily, i.e. only once an element is hovered or selected
* for the first time. This keeps importing large diagrams fast, as we avoid
* creating (and laying out) an outline for every single element up front.
*
* @param {EventBus} eventBus
* @param {Styles} styles
* @param {ElementRegistry} elementRegistry
*/
export default function Outline(eventBus, styles, elementRegistry) {
this._eventBus = eventBus;
this._elementRegistry = elementRegistry;
this._outlineOffset = 5;
this._outlineStyle = styles.cls('djs-outline', [ 'no-fill' ]);
var self = this;
/**
* Lazily create the outline once an element is hovered or selected.
*
* @param {Element} element
*/
function createElementOutline(element) {
self.createOutline(element);
}
eventBus.on('element.hover', function(event) {
createElementOutline(event.element);
});
eventBus.on('selection.changed', function(event) {
forEach(event.newSelection, createElementOutline);
});
// Keep an already created outline in sync with its element. Elements that
// have not been hovered or selected yet do not have an outline; those are
// created (and updated) lazily via the handlers above.
//
// A low priority is necessary, because outlines of labels have to be updated
// after the label bounds have been updated in the renderer.
eventBus.on([ 'shape.changed', 'connection.changed' ], LOW_PRIORITY, function(event) {
var element = event.element,
gfx = event.gfx;
var outline = domQuery('.djs-outline', gfx);
if (outline) {
self.updateOutline(element, outline);
}
});
}
/**
* Ensure the outline for the given element is present, creating, appending
* and updating it on first access.
*
* Outlines are created lazily, i.e. only once an element is hovered or
* selected. Use this to force outline creation, e.g. for features or tests
* that rely on the outline being present.
*
* @param {Element} element
*
* @return {SVGElement} the element's outline
*/
Outline.prototype.createOutline = function(element) {
var gfx = this._elementRegistry.getGraphics(element);
if (!gfx) {
throw new Error('cannot create outline for non-rendered element');
}
var outline = domQuery('.djs-outline', gfx);
if (!outline) {
outline = this.getOutline(element) || this._createOutlineGfx();
svgAppend(gfx, outline);
this.updateOutline(element, outline);
}
return outline;
};
/**
* Create the default outline graphics.
*
* @return {SVGElement} outline
*/
Outline.prototype._createOutlineGfx = function() {
var outline = svgCreate('rect');
svgAttr(outline, assign({
x: 0,
y: 0,
rx: 4,
width: 100,
height: 100
}, this._outlineStyle));
return outline;
};
/**
* Updates the outline of the given element, dispatching to the shape or
* connection specific update depending on the element type.
*
* @param {Element} element
* @param {SVGElement} outline
*/
Outline.prototype.updateOutline = function(element, outline) {
if (element.waypoints) {
this.updateConnectionOutline(outline, element);
} else {
this.updateShapeOutline(outline, element);
}
};
/**
* Updates the outline of a shape respecting the dimension of the
* element and an outline offset.
*
* @param {SVGElement} outline
* @param {Element} element
*/
Outline.prototype.updateShapeOutline = function(outline, element) {
var updated = false;
var providers = this._getProviders();
if (providers.length) {
forEach(providers, function(provider) {
updated = updated || provider.updateOutline(element, outline);
});
}
if (!updated) {
svgAttr(outline, {
x: -this._outlineOffset,
y: -this._outlineOffset,
width: element.width + this._outlineOffset * 2,
height: element.height + this._outlineOffset * 2
});
}
};
/**
* Updates the outline of a connection respecting the bounding box of
* the connection and an outline offset.
* Register an outline provider with the given priority.
*
* @param {SVGElement} outline
* @param {Element} connection
*/
Outline.prototype.updateConnectionOutline = function(outline, connection) {
var bbox = getBBox(connection);
svgAttr(outline, {
x: bbox.x - this._outlineOffset,
y: bbox.y - this._outlineOffset,
width: bbox.width + this._outlineOffset * 2,
height: bbox.height + this._outlineOffset * 2
});
};
/**
* Register an outline provider with the given priority.
*
* @param {number} priority
* @param {OutlineProvider} provider
*/
Outline.prototype.registerProvider = function(priority, provider) {
if (!provider) {
provider = priority;
priority = DEFAULT_PRIORITY;
}
this._eventBus.on('outline.getProviders', priority, function(event) {
event.providers.push(provider);
});
};
/**
* Returns the registered outline providers.
*
* @returns {OutlineProvider[]}
*/
Outline.prototype._getProviders = function() {
var event = this._eventBus.createEvent({
type: 'outline.getProviders',
providers: []
});
this._eventBus.fire(event);
return event.providers;
};
/**
* Returns the outline for an element.
*
* @param {Element} element
*/
Outline.prototype.getOutline = function(element) {
var outline;
var providers = this._getProviders();
forEach(providers, function(provider) {
if (!isFunction(provider.getOutline)) {
return;
}
outline = outline || provider.getOutline(element);
});
return outline;
};
Outline.$inject = [ 'eventBus', 'styles', 'elementRegistry' ];