jointjs
Version:
JavaScript diagramming library
1,085 lines (1,047 loc) • 37.8 kB
JavaScript
import * as g from '../g/index.mjs';
import V from '../V/index.mjs';
import * as util from '../util/index.mjs';
import * as connectionStrategies from '../connectionStrategies/index.mjs';
import * as mvc from '../mvc/index.mjs';
import { ToolView } from '../dia/ToolView.mjs';
function getAnchor(coords, view, magnet) {
// take advantage of an existing logic inside of the
// pin relative connection strategy
var end = connectionStrategies.pinRelative.call(
this.paper,
{},
view,
magnet,
coords,
this.model
);
return end.anchor;
}
function snapAnchor(coords, view, magnet, type, relatedView, toolView) {
var snapRadius = toolView.options.snapRadius;
var isSource = (type === 'source');
var refIndex = (isSource ? 0 : -1);
var ref = this.model.vertex(refIndex) || this.getEndAnchor(isSource ? 'target' : 'source');
if (ref) {
if (Math.abs(ref.x - coords.x) < snapRadius) coords.x = ref.x;
if (Math.abs(ref.y - coords.y) < snapRadius) coords.y = ref.y;
}
return coords;
}
function getViewBBox(view, useModelGeometry) {
const { model } = view;
if (useModelGeometry) return model.getBBox();
return (model.isLink()) ? view.getConnection().bbox() : view.getNodeUnrotatedBBox(view.el);
}
// Vertex Handles
var VertexHandle = mvc.View.extend({
tagName: 'circle',
svgElement: true,
className: 'marker-vertex',
events: {
mousedown: 'onPointerDown',
touchstart: 'onPointerDown',
dblclick: 'onDoubleClick'
},
documentEvents: {
mousemove: 'onPointerMove',
touchmove: 'onPointerMove',
mouseup: 'onPointerUp',
touchend: 'onPointerUp',
touchcancel: 'onPointerUp'
},
attributes: {
'r': 6,
'fill': '#33334F',
'stroke': '#FFFFFF',
'stroke-width': 2,
'cursor': 'move'
},
position: function(x, y) {
this.vel.attr({ cx: x, cy: y });
},
onPointerDown: function(evt) {
if (this.options.guard(evt)) return;
evt.stopPropagation();
evt.preventDefault();
this.options.paper.undelegateEvents();
this.delegateDocumentEvents(null, evt.data);
this.trigger('will-change', this, evt);
},
onPointerMove: function(evt) {
this.trigger('changing', this, evt);
},
onDoubleClick: function(evt) {
this.trigger('remove', this, evt);
},
onPointerUp: function(evt) {
this.trigger('changed', this, evt);
this.undelegateDocumentEvents();
this.options.paper.delegateEvents();
}
});
var Vertices = ToolView.extend({
name: 'vertices',
options: {
handleClass: VertexHandle,
snapRadius: 20,
redundancyRemoval: true,
vertexAdding: true,
stopPropagation: true
},
children: [{
tagName: 'path',
selector: 'connection',
className: 'joint-vertices-path',
attributes: {
'fill': 'none',
'stroke': 'transparent',
'stroke-width': 10,
'cursor': 'cell'
}
}],
handles: null,
events: {
'mousedown .joint-vertices-path': 'onPathPointerDown',
'touchstart .joint-vertices-path': 'onPathPointerDown'
},
onRender: function() {
if (this.options.vertexAdding) {
this.renderChildren();
this.updatePath();
}
this.resetHandles();
this.renderHandles();
return this;
},
update: function() {
var relatedView = this.relatedView;
var vertices = relatedView.model.vertices();
if (vertices.length === this.handles.length) {
this.updateHandles();
} else {
this.resetHandles();
this.renderHandles();
}
if (this.options.vertexAdding) {
this.updatePath();
}
return this;
},
resetHandles: function() {
var handles = this.handles;
this.handles = [];
this.stopListening();
if (!Array.isArray(handles)) return;
for (var i = 0, n = handles.length; i < n; i++) {
handles[i].remove();
}
},
renderHandles: function() {
var relatedView = this.relatedView;
var vertices = relatedView.model.vertices();
for (var i = 0, n = vertices.length; i < n; i++) {
var vertex = vertices[i];
var handle = new (this.options.handleClass)({
index: i,
paper: this.paper,
guard: evt => this.guard(evt)
});
handle.render();
handle.position(vertex.x, vertex.y);
this.simulateRelatedView(handle.el);
handle.vel.appendTo(this.el);
this.handles.push(handle);
this.startHandleListening(handle);
}
},
updateHandles: function() {
var relatedView = this.relatedView;
var vertices = relatedView.model.vertices();
for (var i = 0, n = vertices.length; i < n; i++) {
var vertex = vertices[i];
var handle = this.handles[i];
if (!handle) return;
handle.position(vertex.x, vertex.y);
}
},
updatePath: function() {
var connection = this.childNodes.connection;
if (connection) connection.setAttribute('d', this.relatedView.getSerializedConnection());
},
startHandleListening: function(handle) {
var relatedView = this.relatedView;
if (relatedView.can('vertexMove')) {
this.listenTo(handle, 'will-change', this.onHandleWillChange);
this.listenTo(handle, 'changing', this.onHandleChanging);
this.listenTo(handle, 'changed', this.onHandleChanged);
}
if (relatedView.can('vertexRemove')) {
this.listenTo(handle, 'remove', this.onHandleRemove);
}
},
getNeighborPoints: function(index) {
var linkView = this.relatedView;
var vertices = linkView.model.vertices();
var prev = (index > 0) ? vertices[index - 1] : linkView.sourceAnchor;
var next = (index < vertices.length - 1) ? vertices[index + 1] : linkView.targetAnchor;
return {
prev: new g.Point(prev),
next: new g.Point(next)
};
},
onHandleWillChange: function(_handle, evt) {
this.focus();
const { relatedView, options } = this;
relatedView.model.startBatch('vertex-move', { ui: true, tool: this.cid });
if (!options.stopPropagation) relatedView.notifyPointerdown(...relatedView.paper.getPointerArgs(evt));
},
onHandleChanging: function(handle, evt) {
const { options, relatedView: linkView } = this;
var index = handle.options.index;
var [normalizedEvent, x, y] = linkView.paper.getPointerArgs(evt);
var vertex = { x, y };
this.snapVertex(vertex, index);
linkView.model.vertex(index, vertex, { ui: true, tool: this.cid });
handle.position(vertex.x, vertex.y);
if (!options.stopPropagation) linkView.notifyPointermove(normalizedEvent, x, y);
},
onHandleChanged: function(_handle, evt) {
const { options, relatedView: linkView } = this;
if (options.vertexAdding) this.updatePath();
if (!options.redundancyRemoval) return;
var verticesRemoved = linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid });
if (verticesRemoved) this.render();
this.blur();
linkView.model.stopBatch('vertex-move', { ui: true, tool: this.cid });
if (this.eventData(evt).vertexAdded) {
linkView.model.stopBatch('vertex-add', { ui: true, tool: this.cid });
}
var [normalizedEvt, x, y] = linkView.paper.getPointerArgs(evt);
if (!options.stopPropagation) linkView.notifyPointerup(normalizedEvt, x, y);
linkView.checkMouseleave(normalizedEvt);
},
snapVertex: function(vertex, index) {
var snapRadius = this.options.snapRadius;
if (snapRadius > 0) {
var neighbors = this.getNeighborPoints(index);
var prev = neighbors.prev;
var next = neighbors.next;
if (Math.abs(vertex.x - prev.x) < snapRadius) {
vertex.x = prev.x;
} else if (Math.abs(vertex.x - next.x) < snapRadius) {
vertex.x = next.x;
}
if (Math.abs(vertex.y - prev.y) < snapRadius) {
vertex.y = neighbors.prev.y;
} else if (Math.abs(vertex.y - next.y) < snapRadius) {
vertex.y = next.y;
}
}
},
onHandleRemove: function(handle, evt) {
var index = handle.options.index;
var linkView = this.relatedView;
linkView.model.removeVertex(index, { ui: true });
if (this.options.vertexAdding) this.updatePath();
linkView.checkMouseleave(util.normalizeEvent(evt));
},
onPathPointerDown: function(evt) {
if (this.guard(evt)) return;
evt.stopPropagation();
evt.preventDefault();
var normalizedEvent = util.normalizeEvent(evt);
var vertex = this.paper.snapToGrid(normalizedEvent.clientX, normalizedEvent.clientY).toJSON();
var relatedView = this.relatedView;
relatedView.model.startBatch('vertex-add', { ui: true, tool: this.cid });
var index = relatedView.getVertexIndex(vertex.x, vertex.y);
this.snapVertex(vertex, index);
relatedView.model.insertVertex(index, vertex, { ui: true, tool: this.cid });
this.render();
var handle = this.handles[index];
this.eventData(normalizedEvent, { vertexAdded: true });
handle.onPointerDown(normalizedEvent);
},
onRemove: function() {
this.resetHandles();
}
}, {
VertexHandle: VertexHandle // keep as class property
});
var SegmentHandle = mvc.View.extend({
tagName: 'g',
svgElement: true,
className: 'marker-segment',
events: {
mousedown: 'onPointerDown',
touchstart: 'onPointerDown'
},
documentEvents: {
mousemove: 'onPointerMove',
touchmove: 'onPointerMove',
mouseup: 'onPointerUp',
touchend: 'onPointerUp',
touchcancel: 'onPointerUp'
},
children: [{
tagName: 'line',
selector: 'line',
attributes: {
'stroke': '#33334F',
'stroke-width': 2,
'fill': 'none',
'pointer-events': 'none'
}
}, {
tagName: 'rect',
selector: 'handle',
attributes: {
'width': 20,
'height': 8,
'x': -10,
'y': -4,
'rx': 4,
'ry': 4,
'fill': '#33334F',
'stroke': '#FFFFFF',
'stroke-width': 2
}
}],
onRender: function() {
this.renderChildren();
},
position: function(x, y, angle, view) {
var matrix = V.createSVGMatrix().translate(x, y).rotate(angle);
var handle = this.childNodes.handle;
handle.setAttribute('transform', V.matrixToTransformString(matrix));
handle.setAttribute('cursor', (angle % 180 === 0) ? 'row-resize' : 'col-resize');
var viewPoint = view.getClosestPoint(new g.Point(x, y));
var line = this.childNodes.line;
line.setAttribute('x1', x);
line.setAttribute('y1', y);
line.setAttribute('x2', viewPoint.x);
line.setAttribute('y2', viewPoint.y);
},
onPointerDown: function(evt) {
if (this.options.guard(evt)) return;
this.trigger('change:start', this, evt);
evt.stopPropagation();
evt.preventDefault();
this.options.paper.undelegateEvents();
this.delegateDocumentEvents(null, evt.data);
},
onPointerMove: function(evt) {
this.trigger('changing', this, evt);
},
onPointerUp: function(evt) {
this.undelegateDocumentEvents();
this.options.paper.delegateEvents();
this.trigger('change:end', this, evt);
},
show: function() {
this.el.style.display = '';
},
hide: function() {
this.el.style.display = 'none';
}
});
var Segments = ToolView.extend({
name: 'segments',
precision: .5,
options: {
handleClass: SegmentHandle,
segmentLengthThreshold: 40,
redundancyRemoval: true,
anchor: getAnchor,
snapRadius: 10,
snapHandle: true,
stopPropagation: true
},
handles: null,
onRender: function() {
this.resetHandles();
var relatedView = this.relatedView;
var vertices = relatedView.model.vertices();
vertices.unshift(relatedView.sourcePoint);
vertices.push(relatedView.targetPoint);
for (var i = 0, n = vertices.length; i < n - 1; i++) {
var vertex = vertices[i];
var nextVertex = vertices[i + 1];
var handle = this.renderHandle(vertex, nextVertex);
this.simulateRelatedView(handle.el);
this.handles.push(handle);
handle.options.index = i;
}
return this;
},
renderHandle: function(vertex, nextVertex) {
var handle = new (this.options.handleClass)({
paper: this.paper,
guard: evt => this.guard(evt)
});
handle.render();
this.updateHandle(handle, vertex, nextVertex);
handle.vel.appendTo(this.el);
this.startHandleListening(handle);
return handle;
},
update: function() {
this.render();
return this;
},
startHandleListening: function(handle) {
this.listenTo(handle, 'change:start', this.onHandleChangeStart);
this.listenTo(handle, 'changing', this.onHandleChanging);
this.listenTo(handle, 'change:end', this.onHandleChangeEnd);
},
resetHandles: function() {
var handles = this.handles;
this.handles = [];
this.stopListening();
if (!Array.isArray(handles)) return;
for (var i = 0, n = handles.length; i < n; i++) {
handles[i].remove();
}
},
shiftHandleIndexes: function(value) {
var handles = this.handles;
for (var i = 0, n = handles.length; i < n; i++) handles[i].options.index += value;
},
resetAnchor: function(type, anchor) {
var relatedModel = this.relatedView.model;
if (anchor) {
relatedModel.prop([type, 'anchor'], anchor, {
rewrite: true,
ui: true,
tool: this.cid
});
} else {
relatedModel.removeProp([type, 'anchor'], {
ui: true,
tool: this.cid
});
}
},
snapHandle: function(handle, position, data) {
var index = handle.options.index;
var linkView = this.relatedView;
var link = linkView.model;
var vertices = link.vertices();
var axis = handle.options.axis;
var prev = vertices[index - 2] || data.sourceAnchor;
var next = vertices[index + 1] || data.targetAnchor;
var snapRadius = this.options.snapRadius;
if (Math.abs(position[axis] - prev[axis]) < snapRadius) {
position[axis] = prev[axis];
} else if (Math.abs(position[axis] - next[axis]) < snapRadius) {
position[axis] = next[axis];
}
return position;
},
onHandleChanging: function(handle, evt) {
const { options } = this;
var data = this.eventData(evt);
var relatedView = this.relatedView;
var paper = relatedView.paper;
var index = handle.options.index - 1;
var normalizedEvent = util.normalizeEvent(evt);
var coords = paper.snapToGrid(normalizedEvent.clientX, normalizedEvent.clientY);
var position = this.snapHandle(handle, coords.clone(), data);
var axis = handle.options.axis;
var offset = (this.options.snapHandle) ? 0 : (coords[axis] - position[axis]);
var link = relatedView.model;
var vertices = util.cloneDeep(link.vertices());
var vertex = vertices[index];
var nextVertex = vertices[index + 1];
var anchorFn = this.options.anchor;
if (typeof anchorFn !== 'function') anchorFn = null;
// First Segment
var sourceView = relatedView.sourceView;
var sourceBBox = relatedView.sourceBBox;
var changeSourceAnchor = false;
var deleteSourceAnchor = false;
if (!vertex) {
vertex = relatedView.sourceAnchor.toJSON();
vertex[axis] = position[axis];
if (sourceBBox.containsPoint(vertex)) {
vertex[axis] = position[axis];
changeSourceAnchor = true;
} else {
// we left the area of the source magnet for the first time
vertices.unshift(vertex);
this.shiftHandleIndexes(1);
deleteSourceAnchor = true;
}
} else if (index === 0) {
if (sourceBBox.containsPoint(vertex)) {
vertices.shift();
this.shiftHandleIndexes(-1);
changeSourceAnchor = true;
} else {
vertex[axis] = position[axis];
deleteSourceAnchor = true;
}
} else {
vertex[axis] = position[axis];
}
if (anchorFn && sourceView) {
if (changeSourceAnchor) {
var sourceAnchorPosition = data.sourceAnchor.clone();
sourceAnchorPosition[axis] = position[axis];
var sourceAnchor = anchorFn.call(relatedView, sourceAnchorPosition, sourceView, relatedView.sourceMagnet || sourceView.el, 'source', relatedView);
this.resetAnchor('source', sourceAnchor);
}
if (deleteSourceAnchor) {
this.resetAnchor('source', data.sourceAnchorDef);
}
}
// Last segment
var targetView = relatedView.targetView;
var targetBBox = relatedView.targetBBox;
var changeTargetAnchor = false;
var deleteTargetAnchor = false;
if (!nextVertex) {
nextVertex = relatedView.targetAnchor.toJSON();
nextVertex[axis] = position[axis];
if (targetBBox.containsPoint(nextVertex)) {
changeTargetAnchor = true;
} else {
// we left the area of the target magnet for the first time
vertices.push(nextVertex);
deleteTargetAnchor = true;
}
} else if (index === vertices.length - 2) {
if (targetBBox.containsPoint(nextVertex)) {
vertices.pop();
changeTargetAnchor = true;
} else {
nextVertex[axis] = position[axis];
deleteTargetAnchor = true;
}
} else {
nextVertex[axis] = position[axis];
}
if (anchorFn && targetView) {
if (changeTargetAnchor) {
var targetAnchorPosition = data.targetAnchor.clone();
targetAnchorPosition[axis] = position[axis];
var targetAnchor = anchorFn.call(relatedView, targetAnchorPosition, targetView, relatedView.targetMagnet || targetView.el, 'target', relatedView);
this.resetAnchor('target', targetAnchor);
}
if (deleteTargetAnchor) {
this.resetAnchor('target', data.targetAnchorDef);
}
}
link.vertices(vertices, { ui: true, tool: this.cid });
this.updateHandle(handle, vertex, nextVertex, offset);
if (!options.stopPropagation) relatedView.notifyPointermove(normalizedEvent, coords.x, coords.y);
},
onHandleChangeStart: function(handle, evt) {
const { options, handles, relatedView: linkView } = this;
const { model, paper } = linkView;
var index = handle.options.index;
if (!Array.isArray(handles)) return;
for (var i = 0, n = handles.length; i < n; i++) {
if (i !== index) handles[i].hide();
}
this.focus();
this.eventData(evt, {
sourceAnchor: linkView.sourceAnchor.clone(),
targetAnchor: linkView.targetAnchor.clone(),
sourceAnchorDef: util.clone(model.prop(['source', 'anchor'])),
targetAnchorDef: util.clone(model.prop(['target', 'anchor']))
});
model.startBatch('segment-move', { ui: true, tool: this.cid });
if (!options.stopPropagation) linkView.notifyPointerdown(...paper.getPointerArgs(evt));
},
onHandleChangeEnd: function(_handle, evt) {
const { options, relatedView: linkView }= this;
const { paper, model } = linkView;
if (options.redundancyRemoval) {
linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid });
}
const normalizedEvent = util.normalizeEvent(evt);
const coords = paper.snapToGrid(normalizedEvent.clientX, normalizedEvent.clientY);
this.render();
this.blur();
model.stopBatch('segment-move', { ui: true, tool: this.cid });
if (!options.stopPropagation) linkView.notifyPointerup(normalizedEvent, coords.x, coords.y);
linkView.checkMouseleave(normalizedEvent);
},
updateHandle: function(handle, vertex, nextVertex, offset) {
var vertical = Math.abs(vertex.x - nextVertex.x) < this.precision;
var horizontal = Math.abs(vertex.y - nextVertex.y) < this.precision;
if (vertical || horizontal) {
var segmentLine = new g.Line(vertex, nextVertex);
var length = segmentLine.length();
if (length < this.options.segmentLengthThreshold) {
handle.hide();
} else {
var position = segmentLine.midpoint();
var axis = (vertical) ? 'x' : 'y';
position[axis] += offset || 0;
var angle = segmentLine.vector().vectorAngle(new g.Point(1, 0));
handle.position(position.x, position.y, angle, this.relatedView);
handle.show();
handle.options.axis = axis;
}
} else {
handle.hide();
}
},
onRemove: function() {
this.resetHandles();
}
}, {
SegmentHandle: SegmentHandle // keep as class property
});
// End Markers
var Arrowhead = ToolView.extend({
tagName: 'path',
xAxisVector: new g.Point(1, 0),
events: {
mousedown: 'onPointerDown',
touchstart: 'onPointerDown'
},
documentEvents: {
mousemove: 'onPointerMove',
touchmove: 'onPointerMove',
mouseup: 'onPointerUp',
touchend: 'onPointerUp',
touchcancel: 'onPointerUp'
},
onRender: function() {
this.update();
},
update: function() {
var ratio = this.ratio;
var view = this.relatedView;
var tangent = view.getTangentAtRatio(ratio);
var position, angle;
if (tangent) {
position = tangent.start;
angle = tangent.vector().vectorAngle(this.xAxisVector) || 0;
} else {
position = view.getPointAtRatio(ratio);
angle = 0;
}
if (!position) return this;
var matrix = V.createSVGMatrix().translate(position.x, position.y).rotate(angle);
this.vel.transform(matrix, { absolute: true });
return this;
},
onPointerDown: function(evt) {
if (this.guard(evt)) return;
evt.stopPropagation();
evt.preventDefault();
var relatedView = this.relatedView;
relatedView.model.startBatch('arrowhead-move', { ui: true, tool: this.cid });
if (relatedView.can('arrowheadMove')) {
relatedView.startArrowheadMove(this.arrowheadType);
this.delegateDocumentEvents();
relatedView.paper.undelegateEvents();
}
this.focus();
this.el.style.pointerEvents = 'none';
},
onPointerMove: function(evt) {
var normalizedEvent = util.normalizeEvent(evt);
var coords = this.paper.snapToGrid(normalizedEvent.clientX, normalizedEvent.clientY);
this.relatedView.pointermove(normalizedEvent, coords.x, coords.y);
},
onPointerUp: function(evt) {
this.undelegateDocumentEvents();
var relatedView = this.relatedView;
var paper = relatedView.paper;
var normalizedEvent = util.normalizeEvent(evt);
var coords = paper.snapToGrid(normalizedEvent.clientX, normalizedEvent.clientY);
relatedView.pointerup(normalizedEvent, coords.x, coords.y);
paper.delegateEvents();
this.blur();
this.el.style.pointerEvents = '';
relatedView.model.stopBatch('arrowhead-move', { ui: true, tool: this.cid });
}
});
var TargetArrowhead = Arrowhead.extend({
name: 'target-arrowhead',
ratio: 1,
arrowheadType: 'target',
attributes: {
'd': 'M -10 -8 10 0 -10 8 Z',
'fill': '#33334F',
'stroke': '#FFFFFF',
'stroke-width': 2,
'cursor': 'move',
'class': 'target-arrowhead'
}
});
var SourceArrowhead = Arrowhead.extend({
name: 'source-arrowhead',
ratio: 0,
arrowheadType: 'source',
attributes: {
'd': 'M 10 -8 -10 0 10 8 Z',
'fill': '#33334F',
'stroke': '#FFFFFF',
'stroke-width': 2,
'cursor': 'move',
'class': 'source-arrowhead'
}
});
var Button = ToolView.extend({
name: 'button',
events: {
'mousedown': 'onPointerDown',
'touchstart': 'onPointerDown'
},
options: {
distance: 0,
offset: 0,
rotate: false
},
onRender: function() {
this.renderChildren(this.options.markup);
this.update();
},
update: function() {
this.position();
return this;
},
position: function() {
const { relatedView: view, vel } = this;
const matrix = view.model.isLink() ? this.getLinkMatrix() : this.getElementMatrix();
vel.transform(matrix, { absolute: true });
},
getElementMatrix() {
const { relatedView: view, options } = this;
let { x = 0, y = 0, offset = {}, useModelGeometry, rotate } = options;
let bbox = getViewBBox(view, useModelGeometry);
const angle = view.model.angle();
if (!rotate) bbox = bbox.bbox(angle);
const { x: offsetX = 0, y: offsetY = 0 } = offset;
if (util.isPercentage(x)) {
x = parseFloat(x) / 100 * bbox.width;
}
if (util.isPercentage(y)) {
y = parseFloat(y) / 100 * bbox.height;
}
let matrix = V.createSVGMatrix().translate(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2);
if (rotate) matrix = matrix.rotate(angle);
matrix = matrix.translate(x + offsetX - bbox.width / 2, y + offsetY - bbox.height / 2);
return matrix;
},
getLinkMatrix() {
const { relatedView: view, options } = this;
const { offset = 0, distance = 0, rotate } = options;
let tangent, position, angle;
if (util.isPercentage(distance)) {
tangent = view.getTangentAtRatio(parseFloat(distance) / 100);
} else {
tangent = view.getTangentAtLength(distance);
}
if (tangent) {
position = tangent.start;
angle = tangent.vector().vectorAngle(new g.Point(1, 0)) || 0;
} else {
position = view.getConnection().start;
angle = 0;
}
let matrix = V.createSVGMatrix()
.translate(position.x, position.y)
.rotate(angle)
.translate(0, offset);
if (!rotate) matrix = matrix.rotate(-angle);
return matrix;
},
onPointerDown: function(evt) {
if (this.guard(evt)) return;
evt.stopPropagation();
evt.preventDefault();
var actionFn = this.options.action;
if (typeof actionFn === 'function') {
actionFn.call(this.relatedView, evt, this.relatedView, this);
}
}
});
var Remove = Button.extend({
children: [{
tagName: 'circle',
selector: 'button',
attributes: {
'r': 7,
'fill': '#FF1D00',
'cursor': 'pointer'
}
}, {
tagName: 'path',
selector: 'icon',
attributes: {
'd': 'M -3 -3 3 3 M -3 3 3 -3',
'fill': 'none',
'stroke': '#FFFFFF',
'stroke-width': 2,
'pointer-events': 'none'
}
}],
options: {
distance: 60,
offset: 0,
action: function(evt, view, tool) {
view.model.remove({ ui: true, tool: tool.cid });
}
}
});
var Boundary = ToolView.extend({
name: 'boundary',
tagName: 'rect',
options: {
padding: 10,
useModelGeometry: false,
},
attributes: {
'fill': 'none',
'stroke': '#33334F',
'stroke-width': .5,
'stroke-dasharray': '5, 5',
'pointer-events': 'none'
},
onRender: function() {
this.update();
},
update: function() {
const { relatedView: view, options, vel } = this;
const { useModelGeometry, rotate } = options;
const padding = util.normalizeSides(options.padding);
let bbox = getViewBBox(view, useModelGeometry).moveAndExpand({
x: -padding.left,
y: -padding.top,
width: padding.left + padding.right,
height: padding.top + padding.bottom
});
var model = view.model;
if (model.isElement()) {
var angle = model.angle();
if (angle) {
if (rotate) {
var origin = model.getBBox().center();
vel.rotate(angle, origin.x, origin.y, { absolute: true });
} else {
bbox = bbox.bbox(angle);
}
}
}
vel.attr(bbox.toJSON());
return this;
}
});
var Anchor = ToolView.extend({
tagName: 'g',
type: null,
children: [{
tagName: 'circle',
selector: 'anchor',
attributes: {
'cursor': 'pointer'
}
}, {
tagName: 'rect',
selector: 'area',
attributes: {
'pointer-events': 'none',
'fill': 'none',
'stroke': '#33334F',
'stroke-dasharray': '2,4',
'rx': 5,
'ry': 5
}
}],
events: {
mousedown: 'onPointerDown',
touchstart: 'onPointerDown',
dblclick: 'onPointerDblClick'
},
documentEvents: {
mousemove: 'onPointerMove',
touchmove: 'onPointerMove',
mouseup: 'onPointerUp',
touchend: 'onPointerUp',
touchcancel: 'onPointerUp'
},
options: {
snap: snapAnchor,
anchor: getAnchor,
resetAnchor: true,
customAnchorAttributes: {
'stroke-width': 4,
'stroke': '#33334F',
'fill': '#FFFFFF',
'r': 5
},
defaultAnchorAttributes: {
'stroke-width': 2,
'stroke': '#FFFFFF',
'fill': '#33334F',
'r': 6
},
areaPadding: 6,
snapRadius: 10,
restrictArea: true,
redundancyRemoval: true
},
onRender: function() {
this.renderChildren();
this.toggleArea(false);
this.update();
},
update: function() {
var type = this.type;
var relatedView = this.relatedView;
var view = relatedView.getEndView(type);
if (view) {
this.updateAnchor();
this.updateArea();
this.el.style.display = '';
} else {
this.el.style.display = 'none';
}
return this;
},
updateAnchor: function() {
var childNodes = this.childNodes;
if (!childNodes) return;
var anchorNode = childNodes.anchor;
if (!anchorNode) return;
var relatedView = this.relatedView;
var type = this.type;
var position = relatedView.getEndAnchor(type);
var options = this.options;
var customAnchor = relatedView.model.prop([type, 'anchor']);
anchorNode.setAttribute('transform', 'translate(' + position.x + ',' + position.y + ')');
var anchorAttributes = (customAnchor) ? options.customAnchorAttributes : options.defaultAnchorAttributes;
for (var attrName in anchorAttributes) {
anchorNode.setAttribute(attrName, anchorAttributes[attrName]);
}
},
updateArea: function() {
var childNodes = this.childNodes;
if (!childNodes) return;
var areaNode = childNodes.area;
if (!areaNode) return;
var relatedView = this.relatedView;
var type = this.type;
var view = relatedView.getEndView(type);
var model = view.model;
var magnet = relatedView.getEndMagnet(type);
var padding = this.options.areaPadding;
if (!isFinite(padding)) padding = 0;
var bbox, angle, center;
if (view.isNodeConnection(magnet)) {
bbox = view.getBBox();
angle = 0;
center = bbox.center();
} else {
bbox = view.getNodeUnrotatedBBox(magnet);
angle = model.angle();
center = bbox.center();
if (angle) center.rotate(model.getBBox().center(), -angle);
// TODO: get the link's magnet rotation into account
}
bbox.inflate(padding);
areaNode.setAttribute('x', -bbox.width / 2);
areaNode.setAttribute('y', -bbox.height / 2);
areaNode.setAttribute('width', bbox.width);
areaNode.setAttribute('height', bbox.height);
areaNode.setAttribute('transform', 'translate(' + center.x + ',' + center.y + ') rotate(' + angle + ')');
},
toggleArea: function(visible) {
this.childNodes.area.style.display = (visible) ? '' : 'none';
},
onPointerDown: function(evt) {
if (this.guard(evt)) return;
evt.stopPropagation();
evt.preventDefault();
this.paper.undelegateEvents();
this.delegateDocumentEvents();
this.focus();
this.toggleArea(this.options.restrictArea);
this.relatedView.model.startBatch('anchor-move', { ui: true, tool: this.cid });
},
resetAnchor: function(anchor) {
var type = this.type;
var relatedModel = this.relatedView.model;
if (anchor) {
relatedModel.prop([type, 'anchor'], anchor, {
rewrite: true,
ui: true,
tool: this.cid
});
} else {
relatedModel.removeProp([type, 'anchor'], {
ui: true,
tool: this.cid
});
}
},
onPointerMove: function(evt) {
var relatedView = this.relatedView;
var type = this.type;
var view = relatedView.getEndView(type);
var model = view.model;
var magnet = relatedView.getEndMagnet(type);
var normalizedEvent = util.normalizeEvent(evt);
var coords = this.paper.clientToLocalPoint(normalizedEvent.clientX, normalizedEvent.clientY);
var snapFn = this.options.snap;
if (typeof snapFn === 'function') {
coords = snapFn.call(relatedView, coords, view, magnet, type, relatedView, this);
coords = new g.Point(coords);
}
if (this.options.restrictArea) {
if (view.isNodeConnection(magnet)) {
// snap coords to the link's connection
var pointAtConnection = view.getClosestPoint(coords);
if (pointAtConnection) coords = pointAtConnection;
} else {
// snap coords within node bbox
var bbox = view.getNodeUnrotatedBBox(magnet);
var angle = model.angle();
var origin = model.getBBox().center();
var rotatedCoords = coords.clone().rotate(origin, angle);
if (!bbox.containsPoint(rotatedCoords)) {
coords = bbox.pointNearestToPoint(rotatedCoords).rotate(origin, -angle);
}
}
}
var anchor;
var anchorFn = this.options.anchor;
if (typeof anchorFn === 'function') {
anchor = anchorFn.call(relatedView, coords, view, magnet, type, relatedView);
}
this.resetAnchor(anchor);
this.update();
},
onPointerUp: function(evt) {
this.paper.delegateEvents();
this.undelegateDocumentEvents();
this.blur();
this.toggleArea(false);
var linkView = this.relatedView;
if (this.options.redundancyRemoval) linkView.removeRedundantLinearVertices({ ui: true, tool: this.cid });
linkView.model.stopBatch('anchor-move', { ui: true, tool: this.cid });
},
onPointerDblClick: function() {
var anchor = this.options.resetAnchor;
if (anchor === false) return; // reset anchor disabled
if (anchor === true) anchor = null; // remove the current anchor
this.resetAnchor(util.cloneDeep(anchor));
this.update();
}
});
var SourceAnchor = Anchor.extend({
name: 'source-anchor',
type: 'source'
});
var TargetAnchor = Anchor.extend({
name: 'target-anchor',
type: 'target'
});
export { Vertices, Segments, SourceArrowhead, TargetArrowhead, SourceAnchor, TargetAnchor, Button, Remove, Boundary };