');
this.element(element);
dom.append(parentElement, element);
this.redraw();
return;
}
// remove element
if (!parentElement && element) {
dom.remove(element);
this.element(null);
this.cache({});
return;
}
var cache = this.cache();
// update path container element
var pathContainerStyle = this.pathContainerStyle();
var pathContainerElement = dom.child(element, 0);
dom.css(pathContainerElement, helper.diffObj(pathContainerStyle, cache.pathContainerElementStyle));
cache.pathContainerElementStyle = contentStyle;
// update line element
var lineAttributes = this.lineAttributes();
var lineElement = dom.child(pathContainerElement, 0);
dom.attr(lineElement, helper.diffObj(lineAttributes, cache.lineAttributes));
cache.lineAttributes = lineAttributes;
// update arrow element
var arrowAttributes = this.arrowAttributes();
var arrowElement = dom.child(pathContainerElement, 1);
dom.attr(arrowElement, helper.diffObj(arrowAttributes, cache.arrowAttributes));
cache.arrowAttributes = arrowAttributes;
// update content element
var content = this.content();
var contentStyle = this.contentStyle();
var contentElement = dom.child(element, 1);
if (content !== cache.content) {
var contentType = this.contentType();
if (contentType === helper.CONTENT_TYPE_TEXT)
dom.text(contentElement, content);
else if (contentType === helper.CONTENT_TYPE_HTML)
dom.html(contentElement, content);
cache.content = content;
}
dom.css(contentElement, helper.diffObj(contentStyle, cache.contentStyle));
cache.contentStyle = contentStyle;
// update container element
var style = this.style();
dom.css(element, helper.diffObj(style, cache.style));
cache.style = style;
this.notifyRendered();
};
var Connector = helper.inherits(function(props) {
this.x = this.prop(props.x, 0, helper.toNumber);
this.y = this.prop(props.y, 0, helper.toNumber);
this.color = this.prop(Connector.COLOR_UNCONNECTED);
this.zIndex = this.prop('auto');
this.element = this.prop(null);
this.parentElement = this.prop(null);
this.cache = this.prop({});
this.relations = this.prop([]);
}, Component);
Connector.prototype.r = function() {
return 16;
};
Connector.prototype.contains = function(x, y, tolerance) {
var dx = x - this.x();
var dy = y - this.y();
var r = this.r() + tolerance;
return (dx * dx + dy * dy <= r * r);
};
Connector.prototype.style = function() {
var r = this.r();
var x = this.x() - r;
var y = this.y() - r;
var translate = 'translate(' + x + 'px, ' + y + 'px)';
return {
backgroundColor: this.color(),
border: '2px solid lightgray',
borderRadius: '50%',
boxSizing: 'border-box',
height: r * 2 + 'px',
msTransform: translate,
opacity: 0.6,
pointerEvents: 'none',
position: 'absolute',
transform: translate,
webkitTransform: translate,
width: r * 2 + 'px',
zIndex: this.zIndex()
};
};
Connector.prototype.redraw = function() {
var element = this.element();
var parentElement = this.parentElement();
if (!parentElement && !element)
return;
// add element
if (parentElement && !element) {
element = dom.el('
');
this.element(element);
dom.append(parentElement, element);
this.redraw();
return;
}
// remove element
if (!parentElement && element) {
dom.remove(element);
this.element(null);
return;
}
var cache = this.cache();
// update element
var style = this.style();
dom.css(element, helper.diffObj(style, cache.style));
cache.style = style;
this.notifyRendered();
};
Connector.COLOR_CONNECTED = 'lightgreen';
Connector.COLOR_UNCONNECTED = 'pink';
var Relation = function() {};
Relation.prototype.prop = function(initialValue) {
var cache = initialValue;
return function(value) {
if (typeof value === 'undefined')
return cache;
cache = value;
};
};
Relation.prototype.update = function() {};
var Triple = helper.inherits(function(props) {
this.link = this.prop(props.link);
this.sourceNode = this.prop(props.sourceNode || null);
this.targetNode = this.prop(props.targetNode || null);
this.skipNextUpdate = this.prop(false);
this.nodePositionsCache = this.prop({});
}, Relation);
Triple.prototype.update = function(changedComponent) {
if (this.skipNextUpdate()) {
this.skipNextUpdate(false);
return;
}
var link = this.link();
var sourceNode = this.sourceNode();
var targetNode = this.targetNode();
if (changedComponent instanceof Node)
this.updateNode(link, sourceNode, targetNode, changedComponent);
else if (changedComponent instanceof Link)
this.updateLink(link, sourceNode, targetNode);
};
Triple.prototype.updateNode = function(link, sourceNode, targetNode, changedNode) {
if (sourceNode && targetNode)
this.rotateLink(link, sourceNode, targetNode, changedNode);
else
this.shiftLink(link, sourceNode, targetNode, changedNode);
this.updateNodePositionsCache();
};
Triple.prototype.rotateLink = function(link, sourceNode, targetNode, changedNode) {
var cache = this.nodePositionsCache();
var sncx = cache.sncx;
var sncy = cache.sncy;
var tncx = cache.tncx;
var tncy = cache.tncy;
var lcx = link.cx();
var lcy = link.cy();
var ts_dx = tncx - sncx;
var ts_dy = tncy - sncy;
var cs_dx = lcx - sncx;
var cs_dy = lcy - sncy;
var ts_rad0 = Math.atan2(ts_dy, ts_dx);
var cs_rad0 = Math.atan2(cs_dy, cs_dx);
// changed node position
if (changedNode === sourceNode) {
sncx = sourceNode.cx();
sncy = sourceNode.cy();
} else if (changedNode === targetNode) {
tncx = targetNode.cx();
tncy = targetNode.cy();
}
// center positions of two nodes are equal
if (cs_rad0 === 0) {
link.cx((sncx + tncx) / 2);
link.cy((sncy + tncy) / 2);
return;
}
var ts_d0 = Math.sqrt(ts_dx * ts_dx + ts_dy * ts_dy);
var cs_d0 = Math.sqrt(cs_dx * cs_dx + cs_dy * cs_dy);
var ts_cs_rad = ts_rad0 - cs_rad0;
ts_dx = tncx - sncx;
ts_dy = tncy - sncy;
var ts_rad1 = Math.atan2(ts_dy, ts_dx);
var cs_rad1 = ts_rad1 - ts_cs_rad;
var ts_d1 = Math.sqrt(ts_dx * ts_dx + ts_dy * ts_dy);
var d_rate = (ts_d0 !== 0) ? ts_d1 / ts_d0 : 1;
var cs_d1 = cs_d0 * d_rate;
lcx = sncx + cs_d1 * Math.cos(cs_rad1);
lcy = sncy + cs_d1 * Math.sin(cs_rad1);
link.cx(lcx);
link.cy(lcy);
};
Triple.prototype.shiftLink = function(link, sourceNode, targetNode, changedNode) {
var cache = this.nodePositionsCache();
var ncx = changedNode.cx();
var ncy = changedNode.cy();
if (changedNode === sourceNode) {
link.targetX(link.targetX() + (ncx - cache.sncx));
link.targetY(link.targetY() + (ncy - cache.sncy));
} else if (changedNode === targetNode) {
link.sourceX(link.sourceX() + (ncx - cache.tncx));
link.sourceY(link.sourceY() + (ncy - cache.tncy));
}
};
Triple.prototype.updateLink = function(link, sourceNode, targetNode) {
var lx, ly, p;
if (sourceNode) {
// connect link to source node
lx = targetNode ? link.cx() : link.targetX();
ly = targetNode ? link.cy() : link.targetY();
p = this.connectedPoint(sourceNode, lx, ly);
link.sourceX(p.x);
link.sourceY(p.y);
}
if (targetNode) {
// connect link to target node
lx = sourceNode ? link.cx() : link.sourceX();
ly = sourceNode ? link.cy() : link.sourceY();
p = this.connectedPoint(targetNode, lx, ly);
link.targetX(p.x);
link.targetY(p.y);
}
if (!sourceNode || !targetNode) {
// link content moves to midpoint
link.cx((link.sourceX() + link.targetX()) / 2);
link.cy((link.sourceY() + link.targetY()) / 2);
}
};
Triple.prototype.updateLinkAngle = function(radians) {
var link = this.link();
var sourceNode = this.sourceNode();
var targetNode = this.targetNode();
var ldx = link.targetX() - link.sourceX();
var ldy = link.targetY() - link.sourceY();
var d = Math.sqrt(ldx * ldx + ldy * ldy);
var connectedNode = sourceNode || targetNode;
var cx = connectedNode.cx();
var cy = connectedNode.cy();
var lx = cx + d * Math.cos(radians);
var ly = cy + d * Math.sin(radians);
var p = this.connectedPoint(connectedNode, lx, ly);
if (connectedNode === sourceNode)
link.straighten(p.x, p.y, lx + p.x - cx, ly + p.y - cy);
else if (connectedNode === targetNode)
link.straighten(lx + p.x - cx, ly + p.y - cy, p.x, p.y);
};
Triple.prototype.updateNodePositionsCache = function() {
var sourceNode = this.sourceNode();
var targetNode = this.targetNode();
var cache = this.nodePositionsCache();
if (sourceNode) {
cache.sncx = sourceNode.cx();
cache.sncy = sourceNode.cy();
}
if (targetNode) {
cache.tncx = targetNode.cx();
cache.tncy = targetNode.cy();
}
};
Triple.prototype.connectedPoint = function(node, lx, ly) {
var nx = node.x();
var ny = node.y();
var nwidth = node.width();
var nheight = node.height();
var ncx = node.cx();
var ncy = node.cy();
var alpha = Math.atan2(ly - ncy, lx - ncx);
var beta = Math.PI / 2 - alpha;
var t = Math.atan2(nheight, nwidth);
var x, y;
// left edge
if (alpha < t - Math.PI || alpha > Math.PI - t) {
x = nx;
y = ncy - nwidth * Math.tan(alpha) / 2;
}
// top edge
else if (alpha < -t) {
x = ncx - nheight * Math.tan(beta) / 2;
y = ny;
}
// right edge
else if (alpha < t) {
x = nx + nwidth;
y = ncy + nwidth * Math.tan(alpha) / 2;
}
// bottom edge
else {
x = ncx + nheight * Math.tan(beta) / 2;
y = ny + nheight;
}
var x0, y0, l, ex, ey;
var r = node.borderRadius();
var atCorner = false;
// top-left corner
if (x < nx + r && y < ny + r) {
x0 = nx + r;
y0 = ny + r;
atCorner = true;
}
// top-right corner
else if (x > nx + nwidth - r && y < ny + r) {
x0 = nx + nwidth - r;
y0 = ny + r;
atCorner = true;
}
// bottom-left corner
else if (x < nx + r && y > ny + nheight - r) {
x0 = nx + r;
y0 = ny + nheight - r;
atCorner = true;
}
// bottom-right corner
else if (x > nx + nwidth - r && y > ny + nheight - r) {
x0 = nx + nwidth - r;
y0 = ny + nheight - r;
atCorner = true;
}
if (atCorner) {
l = Math.sqrt((x0 - x) * (x0 - x) + (y0 - y) * (y0 - y));
ex = (x0 - x) / l;
ey = (y0 - y) / l;
x = x0 - r * ex;
y = y0 - r * ey;
}
return {
x: x,
y: y
};
};
var LinkConnectorRelation = helper.inherits(function(props) {
this.type = this.prop(props.type);
this.link = this.prop(props.link);
this.connector = this.prop(props.connector);
}, Relation);
LinkConnectorRelation.prototype.isConnected = function(isConnected) {
var color = isConnected ? Connector.COLOR_CONNECTED : Connector.COLOR_UNCONNECTED;
this.connector().color(color);
};
LinkConnectorRelation.prototype.update = function(changedComponent) {
var type = this.type();
var link = this.link();
var connector = this.connector();
if (changedComponent === link) {
connector.x(link[type + 'X']());
connector.y(link[type + 'Y']());
}
};
var ComponentList = helper.inherits(function() {
ComponentList.super_.call(this);
}, helper.List);
ComponentList.prototype.toFront = function(component) {
var data = this.data;
var index = data.indexOf(component);
if (index === -1)
return;
data.splice(index, 1);
data.push(component);
};
ComponentList.prototype.fromPoint = function(ctor, x, y) {
var data = this.data;
var closeComponent = null;
for (var i = data.length - 1; i >= 0; i--) {
var component = data[i];
if (!(component instanceof ctor))
continue;
if (component.visible === false)
continue;
if (component.contains(x, y, 0))
return component;
if (!closeComponent && component.contains(x, y, 8))
closeComponent = component;
}
return closeComponent;
};
var DisabledConnectorList = helper.inherits(function() {
DisabledConnectorList.super_.call(this);
}, helper.List);
DisabledConnectorList.prototype.add = function(type, link) {
DisabledConnectorList.super_.prototype.add.call(this, {
type: type,
link: link
});
};
DisabledConnectorList.prototype.remove = function(type, link) {
DisabledConnectorList.super_.prototype.remove.call(this, {
type: type,
link: link
});
};
DisabledConnectorList.prototype.contains = function(type, link) {
return DisabledConnectorList.super_.prototype.contains.call(this, {
type: type,
link: link
});
};
DisabledConnectorList.prototype.equal = function(a, b) {
return a.type === b.type && a.link === b.link;
};
var Cmap = helper.inherits(function(rootElement) {
this.componentList = this.prop(new ComponentList());
this.disabledConnectorList = this.prop(new DisabledConnectorList());
this.dragDisabledComponentList = this.prop(new ComponentList());
this.element = this.prop(null);
this.rootElement = this.prop(rootElement || null);
this.retainerElement = this.prop(null);
this.dragContext = this.prop({});
this.selectionHandler = null;
this.activationHandler = null;
this.lastClickComponent = null;
this.lastClickTime = 0;
this.zoomFactor = 1;
this.markDirty();
}, Component);
Cmap.prototype.add = function(component) {
component.parentElement(this.element());
this.componentList().add(component);
this.updateZIndex();
};
Cmap.prototype.remove = function(component) {
component.parentElement(null);
if (component instanceof Link)
this.hideConnectors(component);
this.disconnect(component);
this.componentList().remove(component);
this.updateZIndex();
};
Cmap.prototype.toFront = function(component) {
this.componentList().toFront(component);
this.updateZIndex();
};
Cmap.prototype.updateZIndex = function() {
this.componentList().toArray().forEach(function(component, index) {
if (component instanceof Connector)
return;
// update z-index of node/link
var zIndex = index * 10;
component.zIndex(zIndex);
if (!(component instanceof Link))
return;
// update connector z-index of link
helper.eachInstance(component.relations(), LinkConnectorRelation, function(relation, index) {
relation.connector().zIndex(zIndex + index + 1);
});
});
};
Cmap.prototype.connect = function(type, node, link) {
var linkRelations = link.relations();
var triple = helper.firstInstance(linkRelations, Triple);
var nodeKey = type + 'Node';
if (triple && triple[nodeKey]())
throw new Error('Already connected');
var anotherType = Cmap.anotherConnectionType(type);
var anotherSideNode = triple ? triple[anotherType + 'Node']() : null;
if (anotherSideNode === node)
throw new Error('Already connected to the ' + anotherType + ' of the link');
if (triple) {
triple[nodeKey](node);
} else {
var tripleProps = {};
tripleProps.link = link;
tripleProps[nodeKey] = node;
triple = new Triple(tripleProps);
// add triple to the beginning of link relations to be ahead of link-connector relation
// connector position won't be updated before triple update
linkRelations.unshift(triple);
}
// add triple to node
node.relations().push(triple);
triple.updateNodePositionsCache();
// update connectors of link
helper.eachInstance(linkRelations, LinkConnectorRelation, function(relation) {
if (relation.type() === type)
relation.isConnected(true);
});
// link content moves to midpoint of connected nodes
if (anotherSideNode) {
link.cx((node.cx() + anotherSideNode.cx()) / 2);
link.cy((node.cy() + anotherSideNode.cy()) / 2);
}
// do not need to mark node dirty (stay unchanged)
link.markDirty();
};
Cmap.prototype.disconnect = function(type, node, link) {
if (type instanceof Component) {
var component = type;
var relations = component.relations().slice();
// disconnect all connections of component
helper.eachInstance(relations, Triple, function(triple) {
var link = triple.link();
var sourceNode = triple.sourceNode();
var targetNode = triple.targetNode();
if (sourceNode && (component === link || component === sourceNode))
this.disconnect(Cmap.CONNECTION_TYPE_SOURCE, sourceNode, link);
if (targetNode && (component === link || component === targetNode))
this.disconnect(Cmap.CONNECTION_TYPE_TARGET, targetNode, link);
}.bind(this));
return;
}
var linkRelations = link.relations();
var triple = helper.firstInstance(linkRelations, Triple);
var nodeKey = type + 'Node';
if (!triple || triple[nodeKey]() !== node)
throw new Error('Not connected');
triple[nodeKey](null);
// remove triple from node
var nodeRelations = node.relations();
nodeRelations.splice(nodeRelations.indexOf(triple), 1);
// remove triple from link
if (!triple.sourceNode() && !triple.targetNode())
linkRelations.splice(linkRelations.indexOf(triple), 1);
// update connectors of link
helper.eachInstance(linkRelations, LinkConnectorRelation, function(relation) {
if (relation.type() === type)
relation.isConnected(false);
});
// do not need to mark node dirty (stay unchanged)
link.markDirty();
};
Cmap.prototype.connectedNode = function(type, link) {
var triple = helper.firstInstance(link.relations(), Triple);
if (!triple)
return null;
return triple[type + 'Node']();
};
Cmap.prototype.showConnector = function(type, link) {
if (this.connectorVisible(type, link))
return;
var disabledConnectorList = this.disabledConnectorList();
var connectorDisabled = disabledConnectorList.contains(type, link);
if (!connectorDisabled)
this.addConnector(type, link);
};
Cmap.prototype.connectorVisible = function(type, link) {
return link.relations().some(function(relation) {
return relation instanceof LinkConnectorRelation && relation.type() === type;
});
};
Cmap.prototype.addConnector = function(type, link) {
var connector = new Connector({
x: link[type + 'X'](),
y: link[type + 'Y']()
});
var linkConnectorRelation = new LinkConnectorRelation({
type: type,
link: link,
connector: connector
});
var linkRelations = link.relations();
var triple = helper.firstInstance(linkRelations, Triple);
var isConnected = (triple && !!triple[type + 'Node']());
linkConnectorRelation.isConnected(isConnected);
linkRelations.push(linkConnectorRelation);
connector.relations().push(linkConnectorRelation);
this.add(connector);
};
Cmap.prototype.hideConnector = function(type, link) {
var linkRelations = link.relations();
for (var i = linkRelations.length - 1; i >= 0; i--) {
var relation = linkRelations[i];
if (!(relation instanceof LinkConnectorRelation) || relation.type() !== type)
continue;
// remove connector component
this.remove(relation.connector());
// remove link-connector relation from link
linkRelations.splice(i, 1);
break;
}
};
Cmap.prototype.showConnectors = function(link) {
this.showConnector(Cmap.CONNECTION_TYPE_SOURCE, link);
this.showConnector(Cmap.CONNECTION_TYPE_TARGET, link);
};
Cmap.prototype.hideConnectors = function(link) {
this.hideConnector(Cmap.CONNECTION_TYPE_SOURCE, link);
this.hideConnector(Cmap.CONNECTION_TYPE_TARGET, link);
};
Cmap.prototype.hideAllConnectors = function() {
this.componentList().toArray().forEach(function(component) {
if (component instanceof Link)
this.hideConnectors(component);
}.bind(this));
};
Cmap.prototype.enableConnector = function(type, link) {
this.disabledConnectorList().remove(type, link);
};
Cmap.prototype.disableConnector = function(type, link) {
// remove showing connector
this.hideConnector(type, link);
this.disabledConnectorList().add(type, link);
};
Cmap.prototype.connectorEnabled = function(type, link) {
return !this.disabledConnectorList().contains(type, link);
};
Cmap.prototype.enableDrag = function(component) {
this.dragDisabledComponentList().remove(component);
};
Cmap.prototype.disableDrag = function(component) {
this.dragDisabledComponentList().add(component);
};
Cmap.prototype.dragEnabled = function(component) {
return !this.dragDisabledComponentList().contains(component);
};
Cmap.prototype.onstart = function(x, y, event) {
var context = this.dragContext();
var component = this.componentList().fromPoint(Component, x, y);
context.component = component;
if (typeof this.selectionHandler === 'function')
this.selectionHandler(component, event);
if (!(component instanceof Connector))
this.hideAllConnectors();
if (!component)
return;
var draggable = !this.dragDisabledComponentList().contains(component);
context.draggable = draggable;
if (!draggable)
return;
dom.cancel(event);
this.toFront(component);
if (component instanceof Node) {
context.x = component.x();
context.y = component.y();
} else if (component instanceof Link) {
context.cx = component.cx();
context.cy = component.cy();
context.sourceX = component.sourceX();
context.sourceY = component.sourceY();
context.targetX = component.targetX();
context.targetY = component.targetY();
context.triple = helper.firstInstance(component.relations(), Triple);
this.showConnectors(component);
} else if (component instanceof Connector) {
var linkConnectorRelation = helper.firstInstance(component.relations(), LinkConnectorRelation);
context.x = x;
context.y = y;
context.type = linkConnectorRelation.type();
context.link = linkConnectorRelation.link();
}
this.fixScrollSize();
};
Cmap.prototype.onmove = function(dx, dy, event) {
var context = this.dragContext();
var component = context.component;
if (!component)
return;
if (!context.draggable)
return;
if (component instanceof Node) {
var nodeX = context.x + dx;
var nodeY = context.y + dy;
if (typeof component.moveHandler === 'function') {
var constrainedPosition = component.moveHandler(nodeX, nodeY);
if (constrainedPosition && isFinite(constrainedPosition.x) && isFinite(constrainedPosition.y)) {
nodeX = constrainedPosition.x;
nodeY = constrainedPosition.y;
}
}
component.x(nodeX);
component.y(nodeY);
} else if (component instanceof Link) {
var cx = context.cx + dx;
var cy = context.cy + dy;
var triple = context.triple;
var connectedNode = null;
if (triple) {
var sourceNode = triple.sourceNode();
var targetNode = triple.targetNode();
if (sourceNode && !targetNode)
connectedNode = sourceNode;
else if (!sourceNode && targetNode)
connectedNode = targetNode;
}
if (connectedNode) {
// only one node connected
var x = cx - connectedNode.cx();
var y = cy - connectedNode.cy();
triple.updateLinkAngle(Math.atan2(y, x));
triple.skipNextUpdate(true);
} else if (!triple || component.content()) {
// not connected or link has content
// (except two nodes connected but link has no content)
component.cx(cx);
component.cy(cy);
component.sourceX(context.sourceX + dx);
component.sourceY(context.sourceY + dy);
component.targetX(context.targetX + dx);
component.targetY(context.targetY + dy);
}
} else if (component instanceof Connector) {
var x = context.x + dx;
var y = context.y + dy;
var type = context.type;
var link = context.link;
var triple = helper.firstInstance(link.relations(), Triple);
var connectedNode = triple ? triple[type + 'Node']() : null;
var node = this.componentList().fromPoint(Node, x, y);
if (connectedNode && connectedNode === node) {
// already connected (do nothing)
return;
}
var anotherType = Cmap.anotherConnectionType(type);
var anotherSideNode = triple ? triple[anotherType + 'Node']() : null;
if (connectedNode && connectedNode !== node) {
this.disconnect(type, connectedNode, link);
connectedNode = null;
}
var needsConnect = !connectedNode && node && anotherSideNode !== node;
if (needsConnect) {
if (anotherSideNode) {
var p = triple.connectedPoint(node, anotherSideNode.cx(), anotherSideNode.cy());
link[type + 'X'](p.x);
link[type + 'Y'](p.y);
triple.update(link);
triple.skipNextUpdate(true);
}
this.connect(type, node, link);
} else {
link[type + 'X'](x);
link[type + 'Y'](y);
if (!anotherSideNode)
link.straighten();
}
}
};
Cmap.prototype.onend = function(dx, dy, event) {
var context = this.dragContext();
var component = context.component;
if (!component) {
this.lastClickComponent = null;
this.lastClickTime = 0;
return;
}
if (!context.draggable) {
this.lastClickComponent = null;
this.lastClickTime = 0;
return;
}
// dx/dy are logical map coordinates; keep the click tolerance at four
// physical screen pixels at every zoom level.
var clickTolerance = 4 / this.zoomFactor;
var isClick = Math.abs(dx) <= clickTolerance && Math.abs(dy) <= clickTolerance;
if (isClick) {
var now = Date.now();
var isDoubleClick = (component === this.lastClickComponent &&
now - this.lastClickTime <= 500);
if (isDoubleClick) {
this.lastClickComponent = null;
this.lastClickTime = 0;
if (typeof this.activationHandler === 'function')
this.activationHandler(component, event);
} else {
this.lastClickComponent = component;
this.lastClickTime = now;
}
} else {
this.lastClickComponent = null;
this.lastClickTime = 0;
}
if (component instanceof Node && typeof component.moveEndHandler === 'function')
component.moveEndHandler(component.x(), component.y(), event);
this.unfixScrollSize();
};
Cmap.prototype.fixScrollSize = function() {
var element = this.element();
var clientWidth = dom.clientWidth(element);
var clientHeight = dom.clientHeight(element);
var scrollWidth = dom.scrollWidth(element);
var scrollHeight = dom.scrollHeight(element);
// check if scrolled
if (clientWidth === scrollWidth && clientHeight === scrollHeight)
return;
var translate = 'translate(' + (scrollWidth - 1) + 'px, ' + (scrollHeight - 1) + 'px)';
dom.css(this.retainerElement(), {
msTransform: translate,
transform: translate,
webkitTransform: translate
});
};
Cmap.prototype.unfixScrollSize = function() {
var translate = 'translate(-1px, -1px)';
dom.css(this.retainerElement(), {
msTransform: translate,
transform: translate,
webkitTransform: translate
});
};
Cmap.prototype.style = function() {
return {
color: '#333',
cursor: 'default',
fontFamily: 'sans-serif',
fontSize: '14px',
height: '100%',
MozUserSelect: 'none',
msUserSelect: 'none',
overflow: 'visible',
position: 'relative',
userSelect: 'none',
webkitUserSelect: 'none',
width: '100%',
zoom: this.zoomFactor
};
};
Cmap.prototype.retainerStyle = function() {
return {
height: '1px',
pointerEvents: 'none',
position: 'absolute',
width: '1px'
};
};
Cmap.prototype.redraw = function() {
if (this.disposed)
return;
var rootElement = this.rootElement();
if (!rootElement) {
rootElement = dom.body();
dom.css(rootElement, {
height: '100vh',
margin: '0',
width: '100vw'
});
this.rootElement(rootElement);
}
var previousElement = this.element();
var element = dom.el('
');
element.className = 'rw-cmap-surface';
dom.draggable(element, function(x, y, event) {
this.onstart(x / this.zoomFactor, y / this.zoomFactor, event);
}.bind(this), function(dx, dy, event) {
this.onmove(dx / this.zoomFactor, dy / this.zoomFactor, event);
}.bind(this), function(dx, dy, event) {
this.onend(dx / this.zoomFactor, dy / this.zoomFactor, event);
}.bind(this));
this.element(element);
this.componentList().toArray().forEach(function(component) {
component.parentElement(element);
});
var retainerElement = dom.el('
');
dom.css(retainerElement, this.retainerStyle());
dom.append(element, retainerElement);
this.retainerElement(retainerElement);
// set initial position of retainer
this.unfixScrollSize();
dom.css(element, this.style());
if (previousElement && previousElement.parentNode)
previousElement.parentNode.removeChild(previousElement);
dom.append(rootElement, element);
};
Cmap.anotherConnectionType = function(type) {
if (type === Cmap.CONNECTION_TYPE_SOURCE)
return Cmap.CONNECTION_TYPE_TARGET;
else if (type === Cmap.CONNECTION_TYPE_TARGET)
return Cmap.CONNECTION_TYPE_SOURCE;
};
Cmap.CONNECTION_TYPE_SOURCE = 'source';
Cmap.CONNECTION_TYPE_TARGET = 'target';
var ComponentModule = function(component, cmap) {
this.component = component;
this.cmap = cmap;
this.wrapper = helper.wrap(this, cmap.component);
component.module = this;
};
ComponentModule.prototype.attr = function(key, value) {
var component = this.component;
var attributeKeys = this.constructor.attributeKeys();
if (typeof key === 'undefined') {
var props = {};
attributeKeys.forEach(function(key) {
props[key] = component[key]();
});
return props;
}
if (helper.isPlainObject(key)) {
var props = key;
for (key in props) {
this.attr(key, props[key]);
}
return;
}
if (attributeKeys.indexOf(key) === -1)
return;
if (typeof value === 'undefined')
return component[key]();
component[key](value);
};
ComponentModule.prototype.remove = function() {
this.cmap.component.remove(this.component);
helper.deactivate(this.wrapper);
this.component = null;
this.cmap = null;
this.wrapper = null;
};
ComponentModule.prototype.toFront = function() {
this.cmap.component.toFront(this.component);
};
ComponentModule.prototype.element = function() {
return this.component.element();
};
ComponentModule.prototype.redraw = function() {
this.component.redraw();
};
ComponentModule.prototype.visible = function(value) {
if (typeof value === 'undefined')
return this.component.visible !== false;
this.component.visible = !!value;
this.component.redraw();
return this.component.visible;
};
ComponentModule.prototype.onRendered = function(handler) {
var module = this;
var component = this.component;
if (handler !== null && typeof handler !== 'function')
throw TypeError('Invalid render handler');
component.renderedHandler = handler ? function(element) {
handler(module.wrapper, element);
} : null;
if (handler && component.element())
handler(module.wrapper, component.element());
};
ComponentModule.prototype.draggable = function(enabled) {
var component = this.component;
var cmap = this.cmap;
if (typeof enabled === 'undefined')
return cmap.component.dragEnabled(component);
if (enabled)
cmap.component.enableDrag(component);
else
cmap.component.disableDrag(component);
};
ComponentModule.attributeKeys = function() {
return [];
};
var NodeModule = helper.inherits(function(props, cmap) {
var component = new Node(helper.pick(props, NodeModule.attributeKeys()));
NodeModule.super_.call(this, component, cmap);
}, ComponentModule);
NodeModule.prototype.remove = function() {
this.cmap.nodeModuleList.remove(this);
NodeModule.super_.prototype.remove.call(this);
};
NodeModule.prototype.onMove = function(handler) {
var module = this;
if (handler !== null && typeof handler !== 'function')
throw TypeError('Invalid move handler');
this.component.moveHandler = handler ? function(x, y) {
return handler(module.wrapper, x, y);
} : null;
};
NodeModule.prototype.onMoveEnd = function(handler) {
var module = this;
if (handler !== null && typeof handler !== 'function')
throw TypeError('Invalid move-end handler');
this.component.moveEndHandler = handler ? function(x, y, event) {
handler(module.wrapper, x, y, event);
} : null;
};
NodeModule.attributeKeys = function() {
return [
'content',
'contentType',
'x',
'y',
'width',
'height',
'backgroundColor',
'borderColor',
'borderWidth',
'textColor'
];
};
var LinkModule = helper.inherits(function(props, cmap) {
var component = new Link(helper.pick(props, LinkModule.attributeKeys()));
LinkModule.super_.call(this, component, cmap);
}, ComponentModule);
LinkModule.prototype.sourceNode = function(node) {
return LinkModule.connectNode(this, Cmap.CONNECTION_TYPE_SOURCE, node);
};
LinkModule.prototype.targetNode = function(node) {
return LinkModule.connectNode(this, Cmap.CONNECTION_TYPE_TARGET, node);
};
LinkModule.prototype.straighten = function() {
var link = this.component;
var triple = helper.firstInstance(link.relations(), Triple);
var sourceNode = triple ? triple.sourceNode() : null;
var targetNode = triple ? triple.targetNode() : null;
if (!sourceNode || !targetNode) {
link.straighten();
return;
}
var sourcePoint = triple.connectedPoint(sourceNode, targetNode.cx(), targetNode.cy());
var targetPoint = triple.connectedPoint(targetNode, sourceNode.cx(), sourceNode.cy());
link.straighten(sourcePoint.x, sourcePoint.y, targetPoint.x, targetPoint.y);
};
LinkModule.prototype.sourceConnectorEnabled = function(enabled) {
return LinkModule.connectorEnabled(this, Cmap.CONNECTION_TYPE_SOURCE, enabled);
};
LinkModule.prototype.targetConnectorEnabled = function(enabled) {
return LinkModule.connectorEnabled(this, Cmap.CONNECTION_TYPE_TARGET, enabled);
};
LinkModule.attributeKeys = function() {
return [
'content',
'contentType',
'cx',
'cy',
'width',
'height',
'backgroundColor',
'borderColor',
'borderWidth',
'textColor',
'sourceX',
'sourceY',
'targetX',
'targetY',
'lineColor',
'lineWidth',
'hasArrow'
];
};
LinkModule.connectNode = function(module, type, node) {
var component = module.component;
var cmap = module.cmap;
var cmapComponent = cmap.component;
var connectedNodeComponent = cmapComponent.connectedNode(type, component);
if (typeof node === 'undefined') {
if (!connectedNodeComponent)
return null;
var connectedNode = cmap.nodeModuleList.fromComponent(connectedNodeComponent);
return connectedNode.wrapper;
}
if (node !== null) {
if (typeof node !== 'function')
throw TypeError('Invalid node');
// unwrap node module
node = node(cmap.component);
if (!(node instanceof NodeModule))
throw TypeError('Invalid node');
// cannot connect the same node to the source and the target of the link
var anotherType = Cmap.anotherConnectionType(type);
var anotherNodeComponent = cmapComponent.connectedNode(anotherType, component);
if (anotherNodeComponent === node.component)
return;
}
if (connectedNodeComponent)
cmapComponent.disconnect(type, connectedNodeComponent, component);
if (node === null)
return;
cmapComponent.connect(type, node.component, component);
};
LinkModule.connectorEnabled = function(module, type, enabled) {
var component = module.component;
var cmap = module.cmap;
var cmapComponent = cmap.component;
if (typeof enabled === 'undefined')
return cmapComponent.connectorEnabled(type, component);
if (enabled) {
cmapComponent.enableConnector(type, component);
// show the connector if another connector is showing
var anotherType = Cmap.anotherConnectionType(type);
if (cmapComponent.connectorVisible(anotherType, component))
cmapComponent.showConnector(type, component);
} else {
cmapComponent.disableConnector(type, component);
}
};
var NodeModuleList = helper.inherits(function() {
NodeModuleList.super_.call(this);
}, helper.List);
NodeModuleList.prototype.fromComponent = function(component) {
var data = this.data;
for (var i = 0, len = data.length; i < len; i++) {
var node = data[i];
if (node.component === component)
return node;
}
return null;
};
var CmapModule = function(element) {
if (!(this instanceof CmapModule))
return new CmapModule(element);
this.component = new Cmap(element);
this.nodeModuleList = new NodeModuleList();
this.selectionHandler = null;
this.activationHandler = null;
this.component.selectionHandler = function(component, event) {
if (typeof this.selectionHandler !== 'function')
return;
var module = component ? component.module : null;
this.selectionHandler(module ? module.wrapper : null, event);
}.bind(this);
this.component.activationHandler = function(component, event) {
if (typeof this.activationHandler !== 'function')
return;
var module = component ? component.module : null;
this.activationHandler(module ? module.wrapper : null, event);
}.bind(this);
return helper.wrap(this, this.component);
};
CmapModule.prototype.onSelection = function(handler) {
if (handler !== null && typeof handler !== 'function')
throw TypeError('Invalid selection handler');
this.selectionHandler = handler;
};
CmapModule.prototype.onActivation = function(handler) {
if (handler !== null && typeof handler !== 'function')
throw TypeError('Invalid activation handler');
this.activationHandler = handler;
};
CmapModule.prototype.destroy = function() {
var component = this.component;
var element = component.element();
this.selectionHandler = null;
this.activationHandler = null;
component.selectionHandler = null;
component.activationHandler = null;
component.componentList().toArray().forEach(function(child) {
child.dispose();
child.parentElement(null);
});
component.dispose();
if (element && element.parentNode)
element.parentNode.removeChild(element);
};
CmapModule.prototype.zoom = function(value) {
if (typeof value === 'undefined')
return this.component.zoomFactor;
value = Number(value);
if (!isFinite(value) || value <= 0)
throw TypeError('Invalid zoom factor');
this.component.zoomFactor = value;
var element = this.component.element();
if (element)
element.style.zoom = String(value);
return value;
};
CmapModule.prototype.node = function(props) {
if (typeof props !== 'undefined' && !helper.isPlainObject(props))
throw TypeError('Type error');
var node = new NodeModule(props, this);
this.component.add(node.component);
this.nodeModuleList.add(node);
return node.wrapper;
};
CmapModule.prototype.link = function(props) {
if (typeof props !== 'undefined' && !helper.isPlainObject(props))
throw TypeError('Type error');
var link = new LinkModule(props, this);
this.component.add(link.component);
return link.wrapper;
};
if (typeof module !== 'undefined' && module.exports)
module.exports = CmapModule;
else
window.Cmap = CmapModule;
})();