/** * Render the node, link and connector primitives of a diagram. * * These classes contain geometry and DOM presentation only. Application code * reaches them through DiagramNode and DiagramLink handles. * Derived from ionstage/cmap 0.1.3, (c) 2015 iOnStage, MIT License. */ import { Component, dom, helper } from "./drawing-support.js"; class Node extends Component { constructor(props) { super(); 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; } cx() { return this.x() + this.width() / 2; } cy() { return this.y() + this.height() / 2; } borderRadius() { return 4; } contains(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); } style() { 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() }; } redraw() { 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(); } } class Link extends Component { constructor(props) { super(); 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([]); this.connectionChangeHandler = null; } straighten(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); } contains(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; } containsPath(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; } style() { return { display: this.visible ? '' : 'none', pointerEvents: 'none', position: 'absolute', zIndex: this.zIndex() }; } pathContainerStyle() { 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' }; } lineAttributes() { 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() }; } arrowAttributes() { 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' }; } contentStyle() { 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' }; } redraw() { 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(); } } class Connector extends Component { constructor(props) { super(); 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([]); } r() { return 16; } contains(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); } style() { 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() }; } redraw() { 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'; export { Connector, Link as DrawingLink, Node as DrawingNode };