big cmap refactoring
This commit is contained in:
@@ -0,0 +1,615 @@
|
||||
/**
|
||||
* Own the diagram surface, component relations and pointer interaction.
|
||||
*
|
||||
* DrawingSurface is internal to DiagramEngine. It coordinates the primitive
|
||||
* renderers, performs hit testing and translates drag gestures to geometry.
|
||||
* Derived from ionstage/cmap 0.1.3, (c) 2015 iOnStage, MIT License.
|
||||
*/
|
||||
import { ComponentList, DisabledConnectorList } from "./drawing-collections.js";
|
||||
import { Connector, DrawingLink as Link, DrawingNode as Node } from "./drawing-components.js";
|
||||
import { DrawingTriple as Triple, LinkConnectorRelation } from "./drawing-relations.js";
|
||||
import { Component, dom, helper } from "./drawing-support.js";
|
||||
|
||||
class Cmap extends Component {
|
||||
constructor(rootElement) {
|
||||
super();
|
||||
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();
|
||||
}
|
||||
|
||||
add(component) {
|
||||
component.parentElement(this.element());
|
||||
this.componentList().add(component);
|
||||
this.updateZIndex();
|
||||
}
|
||||
|
||||
static anotherConnectionType(type) {
|
||||
if (type === Cmap.CONNECTION_TYPE_SOURCE)
|
||||
return Cmap.CONNECTION_TYPE_TARGET;
|
||||
else if (type === Cmap.CONNECTION_TYPE_TARGET)
|
||||
return Cmap.CONNECTION_TYPE_SOURCE;
|
||||
}
|
||||
|
||||
remove(component) {
|
||||
component.parentElement(null);
|
||||
|
||||
if (component instanceof Link)
|
||||
this.hideConnectors(component);
|
||||
|
||||
this.disconnect(component);
|
||||
this.componentList().remove(component);
|
||||
this.updateZIndex();
|
||||
}
|
||||
|
||||
toFront(component) {
|
||||
this.componentList().toFront(component);
|
||||
this.updateZIndex();
|
||||
}
|
||||
|
||||
updateZIndex() {
|
||||
var linkIndex = 0;
|
||||
var nodeIndex = 0;
|
||||
this.componentList().toArray().forEach(function(component) {
|
||||
if (component instanceof Connector)
|
||||
return;
|
||||
|
||||
// Relations always occupy a lower band than concept nodes. Reordering a
|
||||
// selected component therefore only changes its order inside that band.
|
||||
var zIndex = component instanceof Link ?
|
||||
Cmap.LINK_Z_INDEX_BASE + linkIndex++ :
|
||||
Cmap.NODE_Z_INDEX_BASE + nodeIndex++;
|
||||
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(Cmap.CONNECTOR_Z_INDEX_BASE + index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
connect(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();
|
||||
}
|
||||
|
||||
disconnect(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();
|
||||
}
|
||||
|
||||
connectedNode(type, link) {
|
||||
var triple = helper.firstInstance(link.relations(), Triple);
|
||||
|
||||
if (!triple)
|
||||
return null;
|
||||
|
||||
return triple[type + 'Node']();
|
||||
}
|
||||
|
||||
showConnector(type, link) {
|
||||
if (this.connectorVisible(type, link))
|
||||
return;
|
||||
|
||||
var disabledConnectorList = this.disabledConnectorList();
|
||||
var connectorDisabled = disabledConnectorList.contains(type, link);
|
||||
|
||||
if (!connectorDisabled)
|
||||
this.addConnector(type, link);
|
||||
}
|
||||
|
||||
connectorVisible(type, link) {
|
||||
return link.relations().some(function(relation) {
|
||||
return relation instanceof LinkConnectorRelation && relation.type() === type;
|
||||
});
|
||||
}
|
||||
|
||||
addConnector(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);
|
||||
}
|
||||
|
||||
hideConnector(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;
|
||||
}
|
||||
}
|
||||
|
||||
showConnectors(link) {
|
||||
this.showConnector(Cmap.CONNECTION_TYPE_SOURCE, link);
|
||||
this.showConnector(Cmap.CONNECTION_TYPE_TARGET, link);
|
||||
}
|
||||
|
||||
hideConnectors(link) {
|
||||
this.hideConnector(Cmap.CONNECTION_TYPE_SOURCE, link);
|
||||
this.hideConnector(Cmap.CONNECTION_TYPE_TARGET, link);
|
||||
}
|
||||
|
||||
hideAllConnectors() {
|
||||
this.componentList().toArray().forEach(function(component) {
|
||||
if (component instanceof Link)
|
||||
this.hideConnectors(component);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
enableConnector(type, link) {
|
||||
this.disabledConnectorList().remove(type, link);
|
||||
}
|
||||
|
||||
disableConnector(type, link) {
|
||||
// remove showing connector
|
||||
this.hideConnector(type, link);
|
||||
|
||||
this.disabledConnectorList().add(type, link);
|
||||
}
|
||||
|
||||
connectorEnabled(type, link) {
|
||||
return !this.disabledConnectorList().contains(type, link);
|
||||
}
|
||||
|
||||
enableDrag(component) {
|
||||
this.dragDisabledComponentList().remove(component);
|
||||
}
|
||||
|
||||
disableDrag(component) {
|
||||
this.dragDisabledComponentList().add(component);
|
||||
}
|
||||
|
||||
dragEnabled(component) {
|
||||
return !this.dragDisabledComponentList().contains(component);
|
||||
}
|
||||
|
||||
onstart(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();
|
||||
}
|
||||
|
||||
onmove(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onend(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);
|
||||
|
||||
if (component instanceof Connector) {
|
||||
var link = context.link;
|
||||
var triple = helper.firstInstance(link.relations(), Triple);
|
||||
var connectedNode = triple ? triple[context.type + 'Node']() : null;
|
||||
|
||||
if (typeof link.connectionChangeHandler === 'function')
|
||||
link.connectionChangeHandler(context.type, connectedNode, event);
|
||||
}
|
||||
|
||||
this.unfixScrollSize();
|
||||
}
|
||||
|
||||
fixScrollSize() {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
unfixScrollSize() {
|
||||
var translate = 'translate(-1px, -1px)';
|
||||
|
||||
dom.css(this.retainerElement(), {
|
||||
msTransform: translate,
|
||||
transform: translate,
|
||||
webkitTransform: translate
|
||||
});
|
||||
}
|
||||
|
||||
style() {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
retainerStyle() {
|
||||
return {
|
||||
height: '1px',
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
width: '1px'
|
||||
};
|
||||
}
|
||||
|
||||
redraw() {
|
||||
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('<div>');
|
||||
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('<div>');
|
||||
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.LINK_Z_INDEX_BASE = 100;
|
||||
Cmap.NODE_Z_INDEX_BASE = 100000;
|
||||
Cmap.CONNECTOR_Z_INDEX_BASE = 200000;
|
||||
Cmap.CONNECTION_TYPE_SOURCE = 'source';
|
||||
Cmap.CONNECTION_TYPE_TARGET = 'target';
|
||||
|
||||
export { Cmap as DrawingSurface };
|
||||
Reference in New Issue
Block a user