diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
343 lines (268 loc) • 8.3 kB
JavaScript
import {
flatten,
filter,
forEach,
groupBy,
map,
unionBy
} from 'min-dash';
import { saveClear } from '../../util/Removal';
import { getNewAttachShapeDelta } from '../../util/AttachUtil';
import inherits from 'inherits-browser';
import CommandInterceptor from '../../command/CommandInterceptor';
/**
* @typedef {import('didi').Injector} Injector
*
* @typedef {import('../../model/Types').Element} Element
*
* @typedef {import('../../core/Canvas').default} Canvas
* @typedef {import('../../core/EventBus').default} EventBus
* @typedef {import('../rules/Rules').default} Rules
* @typedef {import('../modeling/Modeling').default} Modeling
*/
var LOW_PRIORITY = 251,
HIGH_PRIORITY = 1401;
var MARKER_ATTACH = 'attach-ok';
/**
* Adds the notion of attached elements to the modeler.
*
* Optionally depends on `diagram-js/lib/features/move` to render
* the attached elements during move preview.
*
* Optionally depends on `diagram-js/lib/features/label-support`
* to render attached labels during move preview.
*
* @param {Injector} injector
* @param {EventBus} eventBus
* @param {Canvas} canvas
* @param {Rules} rules
* @param {Modeling} modeling
*/
export default function AttachSupport(injector, eventBus, canvas, rules, modeling) {
CommandInterceptor.call(this, eventBus);
var movePreview = injector.get('movePreview', false);
// remove all the attached elements from the shapes to be validated
// add all the attached shapes to the overall list of moved shapes
eventBus.on('shape.move.start', HIGH_PRIORITY, function(e) {
var context = e.context,
shapes = context.shapes,
validatedShapes = context.validatedShapes;
context.shapes = addAttached(shapes);
context.validatedShapes = removeAttached(validatedShapes);
});
// add attachers to the visual's group
movePreview && eventBus.on('shape.move.start', LOW_PRIORITY, function(e) {
var context = e.context,
shapes = context.shapes,
attachers = getAttachers(shapes);
forEach(attachers, function(attacher) {
movePreview.makeDraggable(context, attacher, true);
forEach(attacher.labels, function(label) {
movePreview.makeDraggable(context, label, true);
});
});
});
// add attach-ok marker to current host
movePreview && eventBus.on('shape.move.start', function(event) {
var context = event.context,
shapes = context.shapes;
if (shapes.length !== 1) {
return;
}
var shape = shapes[0];
var host = shape.host;
if (host) {
canvas.addMarker(host, MARKER_ATTACH);
eventBus.once([
'shape.move.out',
'shape.move.cleanup'
], function() {
canvas.removeMarker(host, MARKER_ATTACH);
});
}
});
// add all attachers to move closure
this.preExecuted('elements.move', HIGH_PRIORITY, function(e) {
var context = e.context,
closure = context.closure,
shapes = context.shapes,
attachers = getAttachers(shapes);
forEach(attachers, function(attacher) {
closure.add(attacher, closure.topLevel[attacher.host.id]);
});
});
// perform the attaching after shapes are done moving
this.postExecuted('elements.move', function(e) {
var context = e.context,
shapes = context.shapes,
newHost = context.newHost,
attachers;
// only single elements can be attached
// multiply elements can be detached
if (newHost && shapes.length !== 1) {
return;
}
if (newHost) {
attachers = shapes;
} else {
// find attachers moved without host
attachers = filter(shapes, function(shape) {
var host = shape.host;
return isAttacher(shape) && !includes(shapes, host);
});
}
forEach(attachers, function(attacher) {
modeling.updateAttachment(attacher, newHost);
});
});
// ensure invalid attachment connections are removed
this.postExecuted('elements.move', function(e) {
var shapes = e.context.shapes;
forEach(shapes, function(shape) {
forEach(shape.attachers, function(attacher) {
// remove invalid outgoing connections
forEach(attacher.outgoing.slice(), function(connection) {
var allowed = rules.allowed('connection.reconnect', {
connection: connection,
source: connection.source,
target: connection.target
});
if (!allowed) {
modeling.removeConnection(connection);
}
});
// remove invalid incoming connections
forEach(attacher.incoming.slice(), function(connection) {
var allowed = rules.allowed('connection.reconnect', {
connection: connection,
source: connection.source,
target: connection.target
});
if (!allowed) {
modeling.removeConnection(connection);
}
});
});
});
});
this.postExecute('shape.create', function(e) {
var context = e.context,
shape = context.shape,
host = context.host;
if (host) {
modeling.updateAttachment(shape, host);
}
});
// update attachments if the host is replaced
this.postExecute('shape.replace', function(e) {
var context = e.context,
oldShape = context.oldShape,
newShape = context.newShape;
// move the attachers to the new host
saveClear(oldShape.attachers, function(attacher) {
var allowed = rules.allowed('elements.move', {
target: newShape,
shapes: [ attacher ]
});
if (allowed === 'attach') {
modeling.updateAttachment(attacher, newShape);
} else {
modeling.removeShape(attacher);
}
});
// move attachers if new host has different size
if (newShape.attachers.length) {
forEach(newShape.attachers, function(attacher) {
var delta = getNewAttachShapeDelta(attacher, oldShape, newShape);
modeling.moveShape(attacher, delta, attacher.parent);
});
}
});
// move shape on host resize
this.postExecute('shape.resize', function(event) {
var context = event.context,
shape = context.shape,
oldBounds = context.oldBounds,
newBounds = context.newBounds,
attachers = shape.attachers,
hints = context.hints || {};
if (hints.attachSupport === false) {
return;
}
forEach(attachers, function(attacher) {
var delta = getNewAttachShapeDelta(attacher, oldBounds, newBounds);
modeling.moveShape(attacher, delta, attacher.parent);
forEach(attacher.labels, function(label) {
modeling.moveShape(label, delta, label.parent);
});
});
});
// remove attachments
this.preExecute('shape.delete', function(event) {
var shape = event.context.shape;
saveClear(shape.attachers, function(attacher) {
modeling.removeShape(attacher);
});
if (shape.host) {
modeling.updateAttachment(shape, null);
}
});
}
inherits(AttachSupport, CommandInterceptor);
AttachSupport.$inject = [
'injector',
'eventBus',
'canvas',
'rules',
'modeling'
];
/**
* Return attachers of the given shapes
*
* @param {Element[]} shapes
* @return {Element[]}
*/
function getAttachers(shapes) {
return flatten(map(shapes, function(s) {
return s.attachers || [];
}));
}
/**
* Return a combined list of elements and
* attachers.
*
* @param {Element[]} elements
* @return {Element[]} filtered
*/
function addAttached(elements) {
var attachers = getAttachers(elements);
return unionBy('id', elements, attachers);
}
/**
* Return a filtered list of elements that do not
* contain attached elements with hosts being part
* of the selection.
*
* @param {Element[]} elements
*
* @return {Element[]} filtered
*/
function removeAttached(elements) {
var ids = groupBy(elements, 'id');
return filter(elements, function(element) {
while (element) {
// host in selection
if (element.host && ids[element.host.id]) {
return false;
}
element = element.parent;
}
return true;
});
}
function isAttacher(shape) {
return !!shape.host;
}
function includes(array, item) {
return array.indexOf(item) !== -1;
}