/** * 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('
'); 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('
'); dom.html(element, '
'); 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() { 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('
'); 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()); 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; })();