2236 lines
59 KiB
JavaScript
2236 lines
59 KiB
JavaScript
/**
|
|
* racket-wiki cmap component 0.2.94
|
|
* Based on cmap v0.1.3.
|
|
* (c) 2015 iOnStage
|
|
* Released under the MIT License.
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
var helper = {};
|
|
|
|
helper.toNumber = function(value, defaultValue) {
|
|
return !isNaN(value) ? +value : defaultValue;
|
|
};
|
|
|
|
helper.toString = function(value, defaultValue) {
|
|
return (typeof value !== 'undefined') ? '' + value : defaultValue;
|
|
};
|
|
|
|
helper.toBoolean = function(value, defaultValue) {
|
|
return (typeof value !== 'undefined') ? !!value : defaultValue;
|
|
};
|
|
|
|
helper.toContentType = function(value, defaultValue) {
|
|
if (value === helper.CONTENT_TYPE_TEXT || value === helper.CONTENT_TYPE_HTML)
|
|
return value;
|
|
|
|
return defaultValue;
|
|
};
|
|
|
|
helper.isPlainObject = function(obj) {
|
|
return (typeof obj === 'object' && obj !== null &&
|
|
Object.prototype.toString.call(obj) === '[object Object]');
|
|
};
|
|
|
|
helper.inherits = function(ctor, superCtor) {
|
|
ctor.super_ = superCtor;
|
|
ctor.prototype = Object.create(superCtor.prototype, {
|
|
constructor: {
|
|
value: ctor,
|
|
enumerable: false,
|
|
writable: true,
|
|
configurable: true
|
|
}
|
|
});
|
|
|
|
return ctor;
|
|
};
|
|
|
|
helper.wrap = (function() {
|
|
var Wrapper = function(obj, key) {
|
|
this.obj = obj;
|
|
this.key = key;
|
|
|
|
var wrapper = unwrap.bind(this);
|
|
var proto = Object.getPrototypeOf(obj);
|
|
|
|
for (var key in proto) {
|
|
wrapper[key] = chain(proto[key], obj);
|
|
}
|
|
|
|
return wrapper;
|
|
};
|
|
|
|
var unwrap = function(key) {
|
|
if (this.key === key)
|
|
return this.obj;
|
|
};
|
|
|
|
var chain = function(func, ctx) {
|
|
return function() {
|
|
var ret = func.apply(ctx, arguments);
|
|
|
|
if (typeof ret === 'undefined')
|
|
return this;
|
|
|
|
return ret;
|
|
};
|
|
};
|
|
|
|
return function(obj, key) {
|
|
return new Wrapper(obj, key);
|
|
};
|
|
})();
|
|
|
|
helper.deactivate = function(obj) {
|
|
for (var key in obj) {
|
|
delete obj[key];
|
|
}
|
|
};
|
|
|
|
helper.eachInstance = function(array, ctor, callback) {
|
|
array.filter(function(obj) {
|
|
return obj instanceof ctor;
|
|
}).forEach(callback);
|
|
};
|
|
|
|
helper.firstInstance = function(array, ctor) {
|
|
return array.filter(function(obj) {
|
|
return obj instanceof ctor;
|
|
})[0];
|
|
};
|
|
|
|
helper.diffObj = function(newObj, oldObj) {
|
|
var diff = {};
|
|
|
|
for (var key in newObj) {
|
|
if (!oldObj || newObj[key] !== oldObj[key])
|
|
diff[key] = newObj[key];
|
|
}
|
|
|
|
return diff;
|
|
};
|
|
|
|
helper.pick = function(obj, keys) {
|
|
var ret = {};
|
|
|
|
if (!obj)
|
|
return ret;
|
|
|
|
keys.forEach(function(key) {
|
|
if (key in obj)
|
|
ret[key] = obj[key];
|
|
});
|
|
|
|
return ret;
|
|
};
|
|
|
|
helper.identity = function(value) {
|
|
return value;
|
|
};
|
|
|
|
helper.List = (function() {
|
|
var List = function() {
|
|
this.data = [];
|
|
};
|
|
|
|
List.prototype.add = function(item) {
|
|
if (!this.contains(item))
|
|
this.data.push(item);
|
|
};
|
|
|
|
List.prototype.remove = function(item) {
|
|
var data = this.data;
|
|
|
|
for (var i = data.length - 1; i >= 0; i--) {
|
|
if (this.equal(data[i], item)) {
|
|
data.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
List.prototype.contains = function(item) {
|
|
return this.data.some(function(dataItem) {
|
|
return this.equal(dataItem, item);
|
|
}.bind(this));
|
|
};
|
|
|
|
List.prototype.equal = function(a, b) {
|
|
return a === b;
|
|
};
|
|
|
|
List.prototype.toArray = function() {
|
|
return this.data.slice();
|
|
};
|
|
|
|
return List;
|
|
})();
|
|
|
|
helper.CONTENT_TYPE_TEXT = 'text';
|
|
helper.CONTENT_TYPE_HTML = 'html';
|
|
|
|
var dom = {};
|
|
|
|
dom.disabled = function() {
|
|
return (typeof document === 'undefined');
|
|
};
|
|
|
|
dom.el = function(selector) {
|
|
if (selector.charAt(0) === '<') {
|
|
selector = selector.match(/<(.+)>/)[1];
|
|
return document.createElement(selector);
|
|
}
|
|
};
|
|
|
|
dom.body = function() {
|
|
return document.body;
|
|
};
|
|
|
|
dom.attr = function(el, props) {
|
|
for (var key in props) {
|
|
el.setAttribute(key, props[key]);
|
|
}
|
|
};
|
|
|
|
dom.css = function(el, props) {
|
|
var style = el.style;
|
|
|
|
for (var key in props) {
|
|
style[key] = props[key];
|
|
}
|
|
};
|
|
|
|
dom.rect = function(el) {
|
|
return el.getBoundingClientRect();
|
|
};
|
|
|
|
dom.clientWidth = function(el) {
|
|
return el.clientWidth;
|
|
};
|
|
|
|
dom.clientHeight = function(el) {
|
|
return el.clientHeight;
|
|
};
|
|
|
|
dom.scrollLeft = function(el) {
|
|
return el.scrollLeft;
|
|
};
|
|
|
|
dom.scrollTop = function(el) {
|
|
return el.scrollTop;
|
|
};
|
|
|
|
dom.scrollWidth = function(el) {
|
|
return el.scrollWidth;
|
|
};
|
|
|
|
dom.scrollHeight = function(el) {
|
|
return el.scrollHeight;
|
|
};
|
|
|
|
dom.text = function(el, s) {
|
|
el.textContent = s;
|
|
};
|
|
|
|
dom.html = function(el, s) {
|
|
el.innerHTML = s;
|
|
};
|
|
|
|
dom.append = function(parent, el) {
|
|
parent.appendChild(el);
|
|
};
|
|
|
|
dom.remove = function(el) {
|
|
el.parentNode.removeChild(el);
|
|
};
|
|
|
|
dom.child = function(el, index) {
|
|
return el.childNodes[index];
|
|
};
|
|
|
|
dom.animate = function(callback) {
|
|
return window.requestAnimationFrame(callback);
|
|
};
|
|
|
|
dom.supportsTouch = function() {
|
|
return ('ontouchstart' in window || (typeof DocumentTouch !== 'undefined' && document instanceof DocumentTouch));
|
|
};
|
|
|
|
dom.on = function(el, type, listener) {
|
|
el.addEventListener(type, listener);
|
|
};
|
|
|
|
dom.off = function(el, type, listener) {
|
|
el.removeEventListener(type, listener);
|
|
};
|
|
|
|
dom.pagePoint = function(event, offset) {
|
|
if (dom.supportsTouch())
|
|
event = event.changedTouches[0];
|
|
|
|
return {
|
|
x: event.pageX - (offset ? offset.x : 0),
|
|
y: event.pageY - (offset ? offset.y : 0)
|
|
};
|
|
};
|
|
|
|
dom.clientPoint = function(event, offset) {
|
|
if (dom.supportsTouch())
|
|
event = event.changedTouches[0];
|
|
|
|
return {
|
|
x: event.clientX - (offset ? offset.x : 0),
|
|
y: event.clientY - (offset ? offset.y : 0)
|
|
};
|
|
};
|
|
|
|
dom.cancel = function(event) {
|
|
event.preventDefault();
|
|
};
|
|
|
|
dom.draggable = (function() {
|
|
if (dom.disabled())
|
|
return function() {};
|
|
|
|
var supportsTouch = dom.supportsTouch();
|
|
var EVENT_TYPE_START = supportsTouch ? 'touchstart' : 'mousedown';
|
|
var EVENT_TYPE_MOVE = supportsTouch ? 'touchmove' : 'mousemove';
|
|
var EVENT_TYPE_END = supportsTouch ? 'touchend' : 'mouseup';
|
|
|
|
var Draggable = function(props) {
|
|
this.el = props.el;
|
|
this.onstart = props.onstart;
|
|
this.onmove = props.onmove;
|
|
this.onend = props.onend;
|
|
this.start = start.bind(this);
|
|
this.move = move.bind(this);
|
|
this.end = end.bind(this);
|
|
this.lock = false;
|
|
this.startingPoint = null;
|
|
|
|
dom.on(this.el, EVENT_TYPE_START, this.start);
|
|
};
|
|
|
|
var start = function(event) {
|
|
if (this.lock)
|
|
return;
|
|
|
|
this.lock = true;
|
|
this.startingPoint = dom.pagePoint(event);
|
|
|
|
var el = this.el;
|
|
var onstart = this.onstart;
|
|
|
|
var rect = dom.rect(el);
|
|
var p = dom.clientPoint(event, {
|
|
x: rect.left - dom.scrollLeft(el),
|
|
y: rect.top - dom.scrollTop(el)
|
|
});
|
|
|
|
if (typeof onstart === 'function')
|
|
onstart(p.x, p.y, event);
|
|
|
|
dom.on(document, EVENT_TYPE_MOVE, this.move);
|
|
dom.on(document, EVENT_TYPE_END, this.end);
|
|
};
|
|
|
|
var move = function(event) {
|
|
var onmove = this.onmove;
|
|
var d = dom.pagePoint(event, this.startingPoint);
|
|
|
|
if (typeof onmove === 'function')
|
|
onmove(d.x, d.y, event);
|
|
};
|
|
|
|
var end = function(event) {
|
|
dom.off(document, EVENT_TYPE_MOVE, this.move);
|
|
dom.off(document, EVENT_TYPE_END, this.end);
|
|
|
|
var onend = this.onend;
|
|
var d = dom.pagePoint(event, this.startingPoint);
|
|
|
|
if (typeof onend === 'function')
|
|
onend(d.x, d.y, event);
|
|
|
|
this.lock = false;
|
|
};
|
|
|
|
return function(el, onstart, onmove, onend) {
|
|
new Draggable({
|
|
el: el,
|
|
onstart: onstart,
|
|
onmove: onmove,
|
|
onend: onend
|
|
});
|
|
};
|
|
})();
|
|
|
|
var Component = function() {};
|
|
|
|
Component.prototype.prop = function(initialValue, defaultValue, converter) {
|
|
if (typeof converter !== 'function')
|
|
converter = helper.identity;
|
|
|
|
var cache = converter(initialValue, defaultValue);
|
|
|
|
return function(value) {
|
|
if (typeof value === 'undefined')
|
|
return cache;
|
|
|
|
if (value === cache)
|
|
return;
|
|
|
|
cache = converter(value, cache);
|
|
|
|
this.markDirty();
|
|
};
|
|
};
|
|
|
|
Component.prototype.relations = function() {
|
|
return [];
|
|
};
|
|
|
|
Component.prototype.redraw = function() {};
|
|
|
|
Component.prototype.notifyRendered = function() {
|
|
if (typeof this.renderedHandler === 'function' && this.element())
|
|
this.renderedHandler(this.element());
|
|
};
|
|
|
|
Component.prototype.markDirty = (function() {
|
|
var dirtyComponents = [];
|
|
var requestId = null;
|
|
|
|
var updateRelations = function(index) {
|
|
for (var i = index, len = dirtyComponents.length; i < len; i++) {
|
|
var component = dirtyComponents[i];
|
|
component.relations().forEach(function(relation) {
|
|
relation.update(component);
|
|
});
|
|
}
|
|
|
|
// may be inserted other dirty components by updating relations
|
|
if (dirtyComponents.length > len)
|
|
updateRelations(len);
|
|
};
|
|
|
|
var callback = function() {
|
|
updateRelations(0);
|
|
|
|
dirtyComponents.forEach(function(component) {
|
|
component.redraw();
|
|
});
|
|
|
|
dirtyComponents = [];
|
|
requestId = null;
|
|
};
|
|
|
|
return function() {
|
|
if (dom.disabled())
|
|
return;
|
|
|
|
if (dirtyComponents.indexOf(this) === -1)
|
|
dirtyComponents.push(this);
|
|
|
|
if (requestId !== null)
|
|
return;
|
|
|
|
requestId = dom.animate(callback);
|
|
};
|
|
})();
|
|
|
|
var Node = helper.inherits(function(props) {
|
|
this.visible = true;
|
|
this.content = this.prop(props.content, '', helper.toString);
|
|
this.contentType = this.prop(props.contentType, helper.CONTENT_TYPE_TEXT, helper.toContentType);
|
|
this.x = this.prop(props.x, 0, helper.toNumber);
|
|
this.y = this.prop(props.y, 0, helper.toNumber);
|
|
this.width = this.prop(props.width, 75, helper.toNumber);
|
|
this.height = this.prop(props.height, 30, helper.toNumber);
|
|
this.backgroundColor = this.prop(props.backgroundColor, '#a7cbe6', helper.toString);
|
|
this.borderColor = this.prop(props.borderColor, '#333', helper.toString);
|
|
this.borderWidth = this.prop(props.borderWidth, 2, helper.toNumber);
|
|
this.textColor = this.prop(props.textColor, '#333', helper.toString);
|
|
this.zIndex = this.prop('auto');
|
|
this.element = this.prop(null);
|
|
this.parentElement = this.prop(null);
|
|
this.cache = this.prop({});
|
|
this.relations = this.prop([]);
|
|
this.moveHandler = null;
|
|
}, Component);
|
|
|
|
Node.prototype.cx = function() {
|
|
return this.x() + this.width() / 2;
|
|
};
|
|
|
|
Node.prototype.cy = function() {
|
|
return this.y() + this.height() / 2;
|
|
};
|
|
|
|
Node.prototype.borderRadius = function() {
|
|
return 4;
|
|
};
|
|
|
|
Node.prototype.contains = function(x, y, tolerance) {
|
|
var nx = this.x();
|
|
var ny = this.y();
|
|
var nwidth = this.width();
|
|
var nheight = this.height();
|
|
|
|
return (nx - tolerance <= x && x <= nx + nwidth + tolerance &&
|
|
ny - tolerance <= y && y <= ny + nheight + tolerance);
|
|
};
|
|
|
|
Node.prototype.style = function() {
|
|
var contentType = this.contentType();
|
|
var lineHeight = (contentType === helper.CONTENT_TYPE_TEXT) ? this.height() : 14;
|
|
var textAlign = (contentType === helper.CONTENT_TYPE_TEXT) ? 'center' : 'left';
|
|
var translate = 'translate(' + this.x() + 'px, ' + this.y() + 'px)';
|
|
var borderWidthOffset = this.borderWidth() * 2;
|
|
|
|
return {
|
|
backgroundColor: this.backgroundColor(),
|
|
border: this.borderWidth() + 'px solid ' + this.borderColor(),
|
|
borderRadius: this.borderRadius() + 'px',
|
|
color: this.textColor(),
|
|
display: this.visible ? '' : 'none',
|
|
height: (this.height() - borderWidthOffset) + 'px',
|
|
lineHeight: (lineHeight - borderWidthOffset) + 'px',
|
|
msTransform: translate,
|
|
overflow: 'hidden',
|
|
pointerEvents: 'auto',
|
|
position: 'absolute',
|
|
textAlign: textAlign,
|
|
textOverflow: 'ellipsis',
|
|
transform: translate,
|
|
webkitTransform: translate,
|
|
whiteSpace: 'nowrap',
|
|
width: (this.width() - borderWidthOffset) + 'px',
|
|
zIndex: this.zIndex()
|
|
};
|
|
};
|
|
|
|
Node.prototype.redraw = function() {
|
|
var element = this.element();
|
|
var parentElement = this.parentElement();
|
|
|
|
if (!parentElement && !element)
|
|
return;
|
|
|
|
// add element
|
|
if (parentElement && !element) {
|
|
element = dom.el('<div>');
|
|
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 element
|
|
var content = this.content();
|
|
|
|
if (content !== cache.content) {
|
|
var contentType = this.contentType();
|
|
|
|
if (contentType === helper.CONTENT_TYPE_TEXT)
|
|
dom.text(element, content);
|
|
else if (contentType === helper.CONTENT_TYPE_HTML)
|
|
dom.html(element, content);
|
|
|
|
cache.content = content;
|
|
}
|
|
|
|
var style = this.style();
|
|
|
|
dom.css(element, helper.diffObj(style, cache.style));
|
|
cache.style = style;
|
|
this.notifyRendered();
|
|
};
|
|
|
|
var Link = helper.inherits(function(props) {
|
|
this.visible = true;
|
|
this.content = this.prop(props.content, '', helper.toString);
|
|
this.contentType = this.prop(props.contentType, helper.CONTENT_TYPE_TEXT, helper.toContentType);
|
|
this.cx = this.prop(props.cx, 100, helper.toNumber);
|
|
this.cy = this.prop(props.cy, 40, helper.toNumber);
|
|
this.width = this.prop(props.width, 50, helper.toNumber);
|
|
this.height = this.prop(props.height, 20, helper.toNumber);
|
|
this.backgroundColor = this.prop(props.backgroundColor, 'white', helper.toString);
|
|
this.borderColor = this.prop(props.borderColor, '#333', helper.toString);
|
|
this.borderWidth = this.prop(props.borderWidth, 2, helper.toNumber);
|
|
this.textColor = this.prop(props.textColor, '#333', helper.toString);
|
|
this.sourceX = this.prop(props.sourceX, this.cx() - 70, helper.toNumber);
|
|
this.sourceY = this.prop(props.sourceY, this.cy(), helper.toNumber);
|
|
this.targetX = this.prop(props.targetX, this.cx() + 70, helper.toNumber);
|
|
this.targetY = this.prop(props.targetY, this.cy(), helper.toNumber);
|
|
this.lineColor = this.prop(props.lineColor, '#333', helper.toString);
|
|
this.lineWidth = this.prop(props.lineWidth, 2, helper.toNumber);
|
|
this.hasArrow = this.prop(props.hasArrow, true, helper.toBoolean);
|
|
this.zIndex = this.prop('auto');
|
|
this.element = this.prop(null);
|
|
this.parentElement = this.prop(null);
|
|
this.cache = this.prop({});
|
|
this.relations = this.prop([]);
|
|
}, Component);
|
|
|
|
Link.prototype.straighten = function(sx, sy, tx, ty) {
|
|
if (arguments.length === 0) {
|
|
this.cx((this.sourceX() + this.targetX()) / 2);
|
|
this.cy((this.sourceY() + this.targetY()) / 2);
|
|
|
|
return;
|
|
}
|
|
|
|
this.cx((sx + tx) / 2);
|
|
this.cy((sy + ty) / 2);
|
|
this.sourceX(sx);
|
|
this.sourceY(sy);
|
|
this.targetX(tx);
|
|
this.targetY(ty);
|
|
};
|
|
|
|
Link.prototype.contains = function(x, y, tolerance) {
|
|
var content = this.content();
|
|
var lcx = this.cx();
|
|
var lcy = this.cy();
|
|
|
|
// content area
|
|
if (content) {
|
|
var lwidth = this.width();
|
|
var lheight = this.height();
|
|
|
|
var lx = lcx - lwidth / 2;
|
|
var ly = lcy - lheight / 2;
|
|
|
|
if (lx - tolerance <= x && x <= lx + lwidth + tolerance &&
|
|
ly - tolerance <= y && y <= ly + lheight + tolerance) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var lineWidth = this.lineWidth();
|
|
|
|
// source path
|
|
if (this.containsPath(this.sourceX(), this.sourceY(), lcx, lcy, x, y, lineWidth / 2 + tolerance))
|
|
return true;
|
|
|
|
// target path
|
|
if (this.containsPath(this.targetX(), this.targetY(), lcx, lcy, x, y, lineWidth / 2 + tolerance))
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
Link.prototype.containsPath = function(x0, y0, x1, y1, x, y, d) {
|
|
var ax = x1 - x0;
|
|
var ay = y1 - y0;
|
|
|
|
var bx = x - x0;
|
|
var by = y - y0;
|
|
|
|
var r = (ax * bx + ay * by) / (ax * ax + ay * ay);
|
|
|
|
if (0 <= r && r <= 1) {
|
|
var px = x0 + r * ax;
|
|
var py = y0 + r * ay;
|
|
|
|
var dx = px - x;
|
|
var dy = py - y;
|
|
|
|
if (dx * dx + dy * dy <= d * d)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
Link.prototype.style = function() {
|
|
return {
|
|
display: this.visible ? '' : 'none',
|
|
pointerEvents: 'none',
|
|
position: 'absolute',
|
|
zIndex: this.zIndex()
|
|
};
|
|
};
|
|
|
|
Link.prototype.pathContainerStyle = function() {
|
|
var width = Math.max(this.cx(), this.sourceX(), this.targetX());
|
|
var height = Math.max(this.cy(), this.sourceY(), this.targetY());
|
|
|
|
return {
|
|
height: height + 'px',
|
|
overflow: 'visible',
|
|
position: 'absolute',
|
|
width: width + 'px'
|
|
};
|
|
};
|
|
|
|
Link.prototype.lineAttributes = function() {
|
|
var d = [
|
|
'M', this.sourceX(), this.sourceY(),
|
|
'L', this.cx(), this.cy(),
|
|
'L', this.targetX(), this.targetY()
|
|
].join(' ');
|
|
|
|
return {
|
|
d: d,
|
|
fill: 'none',
|
|
stroke: this.lineColor(),
|
|
'stroke-linecap': 'round',
|
|
'stroke-width': this.lineWidth()
|
|
};
|
|
};
|
|
|
|
Link.prototype.arrowAttributes = function() {
|
|
var cx = this.cx();
|
|
var cy = this.cy();
|
|
var tx = this.targetX();
|
|
var ty = this.targetY();
|
|
|
|
var radians = Math.atan2(ty - cy, tx - cx);
|
|
|
|
var p0 = {
|
|
x: 15 * Math.cos(radians - 26 * Math.PI / 180),
|
|
y: 15 * Math.sin(radians - 26 * Math.PI / 180)
|
|
};
|
|
|
|
var p1 = {
|
|
x: 15 * Math.cos(radians + 26 * Math.PI / 180),
|
|
y: 15 * Math.sin(radians + 26 * Math.PI / 180)
|
|
};
|
|
|
|
var p2 = {
|
|
x: 7 * Math.cos(radians),
|
|
y: 7 * Math.sin(radians)
|
|
};
|
|
|
|
var d = [
|
|
'M', tx - p0.x, ty - p0.y,
|
|
'L', tx, ty,
|
|
'L', tx - p1.x, ty - p1.y,
|
|
'Q', tx - p2.x, ty - p2.y, tx - p0.x, ty - p0.y,
|
|
'Z'
|
|
].join(' ');
|
|
|
|
return {
|
|
d: d,
|
|
fill: this.lineColor(),
|
|
stroke: this.lineColor(),
|
|
'stroke-linejoin': 'round',
|
|
'stroke-width': this.lineWidth(),
|
|
visibility: this.hasArrow() ? 'visible' : 'hidden'
|
|
};
|
|
};
|
|
|
|
Link.prototype.contentStyle = function() {
|
|
var contentType = this.contentType();
|
|
var lineHeight = (contentType === helper.CONTENT_TYPE_TEXT) ? this.height() : 14;
|
|
var textAlign = (contentType === helper.CONTENT_TYPE_TEXT) ? 'center' : 'left';
|
|
var x = this.cx() - this.width() / 2;
|
|
var y = this.cy() - this.height() / 2;
|
|
var translate = 'translate(' + x + 'px, ' + y + 'px)';
|
|
var borderWidthOffset = this.borderWidth() * 2;
|
|
|
|
return {
|
|
backgroundColor: this.backgroundColor(),
|
|
border: this.borderWidth() + 'px solid ' + this.borderColor(),
|
|
borderRadius: '4px',
|
|
color: this.textColor(),
|
|
height: (this.height() - borderWidthOffset) + 'px',
|
|
lineHeight: (lineHeight - borderWidthOffset) + 'px',
|
|
msTransform: translate,
|
|
overflow: 'hidden',
|
|
position: 'absolute',
|
|
textAlign: textAlign,
|
|
textOverflow: 'ellipsis',
|
|
transform: translate,
|
|
visibility: this.content() ? 'visible' : 'hidden',
|
|
webkitTransform: translate,
|
|
whiteSpace: 'nowrap',
|
|
width: (this.width() - borderWidthOffset) + 'px'
|
|
};
|
|
};
|
|
|
|
Link.prototype.redraw = function() {
|
|
var element = this.element();
|
|
var parentElement = this.parentElement();
|
|
|
|
if (!parentElement && !element)
|
|
return;
|
|
|
|
// add element
|
|
if (parentElement && !element) {
|
|
element = dom.el('<div>');
|
|
dom.html(element, '<svg><path></path><path></path></svg><div></div>');
|
|
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('<div>');
|
|
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() {
|
|
var rootElement = this.rootElement();
|
|
|
|
if (!rootElement) {
|
|
rootElement = dom.body();
|
|
dom.css(rootElement, {
|
|
height: '100vh',
|
|
margin: '0',
|
|
width: '100vw'
|
|
});
|
|
this.rootElement(rootElement);
|
|
}
|
|
|
|
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());
|
|
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.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;
|
|
})();
|