diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
382 lines (312 loc) • 8.56 kB
JavaScript
var MARKER_OK = 'drop-ok',
MARKER_NOT_OK = 'drop-not-ok',
MARKER_ATTACH = 'attach-ok',
MARKER_NEW_PARENT = 'new-parent';
import {
assign,
filter,
find,
forEach,
isArray,
isNumber,
map
} from 'min-dash';
import { getBBox } from '../../util/Elements';
import {
isConnection,
isLabel
} from '../../util/ModelUtil';
/**
* @typedef {import('../../core/Types').ElementLike} Element
* @typedef {import('../../core/Types').ShapeLike} Shape
*
* @typedef {import('../../util/Types').Point} Point
*
* @typedef {import('../../core/Canvas').default} Canvas
* @typedef {import('../dragging/Dragging').default} Dragging
* @typedef {import('../../core/EventBus').default} EventBus
* @typedef {import('../modeling/Modeling').default} Modeling
* @typedef {import('../rules/Rules').default} Rules
*/
var PREFIX = 'create';
var HIGH_PRIORITY = 2000;
/**
* Create new elements through drag and drop.
*
* @param {Canvas} canvas
* @param {Dragging} dragging
* @param {EventBus} eventBus
* @param {Modeling} modeling
* @param {Rules} rules
*/
export default function Create(
canvas,
dragging,
eventBus,
modeling,
rules
) {
// rules //////////
/**
* Check wether elements can be created.
*
* @param {Element[]} elements
* @param {Shape} target
* @param {Point} position
* @param {Element} [source]
*
* @return {boolean|null|Object}
*/
function canCreate(elements, target, position, source, hints) {
if (!target) {
return false;
}
// ignore child elements and external labels
elements = filter(elements, function(element) {
var labelTarget = element.labelTarget;
return !element.parent && !(isLabel(element) && elements.indexOf(labelTarget) !== -1);
});
var shape = find(elements, function(element) {
return !isConnection(element);
});
var attach = false,
connect = false,
create = false;
// (1) attaching single shapes
if (isSingleShape(elements)) {
attach = rules.allowed('shape.attach', {
position: position,
shape: shape,
target: target
});
}
if (!attach) {
// (2) creating elements
if (isSingleShape(elements)) {
create = rules.allowed('shape.create', {
position: position,
shape: shape,
source: source,
target: target
});
} else {
create = rules.allowed('elements.create', {
elements: elements,
position: position,
target: target
});
}
}
var connectionTarget = hints.connectionTarget;
// (3) appending single shapes
if (create || attach) {
if (shape && source) {
connect = rules.allowed('connection.create', {
source: connectionTarget === source ? shape : source,
target: connectionTarget === source ? source : shape,
hints: {
targetParent: target,
targetAttach: attach
}
});
}
return {
attach: attach,
connect: connect
};
}
// ignore wether or not elements can be created
if (create === null || attach === null) {
return null;
}
return false;
}
function setMarker(element, marker) {
[ MARKER_ATTACH, MARKER_OK, MARKER_NOT_OK, MARKER_NEW_PARENT ].forEach(function(m) {
if (m === marker) {
canvas.addMarker(element, m);
} else {
canvas.removeMarker(element, m);
}
});
}
// event handling //////////
eventBus.on([ 'create.move', 'create.hover' ], function(event) {
var context = event.context,
elements = context.elements,
hover = event.hover,
source = context.source,
hints = context.hints || {};
if (!hover) {
context.canExecute = false;
context.target = null;
return;
}
ensureConstraints(event);
var position = {
x: event.x,
y: event.y
};
var canExecute = context.canExecute = hover && canCreate(elements, hover, position, source, hints);
if (hover && canExecute !== null) {
context.target = hover;
if (canExecute && canExecute.attach) {
setMarker(hover, MARKER_ATTACH);
} else {
setMarker(hover, canExecute ? MARKER_NEW_PARENT : MARKER_NOT_OK);
}
}
});
eventBus.on([ 'create.end', 'create.out', 'create.cleanup' ], function(event) {
var hover = event.hover;
if (hover) {
setMarker(hover, null);
}
});
eventBus.on('create.end', function(event) {
var context = event.context,
source = context.source,
shape = context.shape,
elements = context.elements,
target = context.target,
canExecute = context.canExecute,
attach = canExecute && canExecute.attach,
connect = canExecute && canExecute.connect,
hints = context.hints || {};
if (canExecute === false || !target) {
return false;
}
ensureConstraints(event);
var position = {
x: event.x,
y: event.y
};
if (connect) {
shape = modeling.appendShape(source, shape, position, target, {
attach: attach,
connection: connect === true ? {} : connect,
connectionTarget: hints.connectionTarget
});
} else {
elements = modeling.createElements(elements, position, target, assign({}, hints, {
attach: attach
}));
// update shape
shape = find(elements, function(element) {
return !isConnection(element);
});
}
// update elements and shape
assign(context, {
elements: elements,
shape: shape
});
assign(event, {
elements: elements,
shape: shape
});
});
function cancel() {
var context = dragging.context();
if (context && context.prefix === PREFIX) {
dragging.cancel();
}
}
// cancel on <elements.changed> that is not result of <drag.end>
eventBus.on('create.init', function() {
eventBus.on('elements.changed', cancel);
eventBus.once([ 'create.cancel', 'create.end' ], HIGH_PRIORITY, function() {
eventBus.off('elements.changed', cancel);
});
});
// API //////////
/**
* @param event
* @param elements
* @param {any} [context={}]
*/
this.start = function(event, elements, context) {
if (!isArray(elements)) {
elements = [ elements ];
}
var shape = find(elements, function(element) {
return !isConnection(element);
});
if (!shape) {
// at least one shape is required
return;
}
context = assign({
elements: elements,
hints: {},
shape: shape
}, context || {});
// make sure each element has x and y
forEach(elements, function(element) {
if (!isNumber(element.x)) {
element.x = 0;
}
if (!isNumber(element.y)) {
element.y = 0;
}
});
var visibleElements = filter(elements, function(element) {
return !element.hidden;
});
var bbox = getBBox(visibleElements);
// center elements around cursor
forEach(elements, function(element) {
if (isConnection(element)) {
element.waypoints = map(element.waypoints, function(waypoint) {
return {
x: waypoint.x - bbox.x - bbox.width / 2,
y: waypoint.y - bbox.y - bbox.height / 2
};
});
}
assign(element, {
x: element.x - bbox.x - bbox.width / 2,
y: element.y - bbox.y - bbox.height / 2
});
});
dragging.init(event, PREFIX, {
cursor: 'grabbing',
autoActivate: true,
data: {
shape: shape,
elements: elements,
context: context
}
});
};
}
Create.$inject = [
'canvas',
'dragging',
'eventBus',
'modeling',
'rules'
];
// helpers //////////
function ensureConstraints(event) {
var context = event.context,
createConstraints = context.createConstraints;
if (!createConstraints) {
return;
}
if (createConstraints.left) {
event.x = Math.max(event.x, createConstraints.left);
}
if (createConstraints.right) {
event.x = Math.min(event.x, createConstraints.right);
}
if (createConstraints.top) {
event.y = Math.max(event.y, createConstraints.top);
}
if (createConstraints.bottom) {
event.y = Math.min(event.y, createConstraints.bottom);
}
}
function isSingleShape(elements) {
return elements && elements.length === 1 && !isConnection(elements[ 0 ]);
}