343 lines
8.1 KiB
JavaScript
343 lines
8.1 KiB
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').Base} Base
|
||
|
|
*
|
||
|
|
* @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 {Array<Base>} shapes
|
||
|
|
* @return {Array<Base>}
|
||
|
|
*/
|
||
|
|
function getAttachers(shapes) {
|
||
|
|
return flatten(map(shapes, function(s) {
|
||
|
|
return s.attachers || [];
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return a combined list of elements and
|
||
|
|
* attachers.
|
||
|
|
*
|
||
|
|
* @param {Array<Base>} elements
|
||
|
|
* @return {Array<Base>} 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 {Array<Base>} elements
|
||
|
|
*
|
||
|
|
* @return {Array<Base>} 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;
|
||
|
|
}
|