diff --git a/lib/sequence-diagram.js b/lib/sequence-diagram.js index 70776f9..bbdfa16 100644 --- a/lib/sequence-diagram.js +++ b/lib/sequence-diagram.js @@ -465,6 +465,16 @@ define('core/EventObject',[],() => { } } + on(type, fn) { + this.addEventListener(type, fn); + return this; + } + + off(type, fn) { + this.removeEventListener(type, fn); + return this; + } + countEventListeners(type) { return (this.listeners.get(type) || []).length; } @@ -3451,69 +3461,221 @@ define('sequence/Generator',['core/ArrayUtilities'], (array) => { }; }); -define('svg/SVGUtilities',[],() => { +define('core/DOMWrapper',[],() => { 'use strict'; - const NS = 'http://www.w3.org/2000/svg'; - - function makeText(text = '') { - return document.createTextNode(text); + function make(value, document) { + if(typeof value === 'string') { + return document.createTextNode(value); + } else if(typeof value === 'number') { + return document.createTextNode(value.toString(10)); + } else if(typeof value === 'object' && value.element) { + return value.element; + } else { + return value; + } } - function setAttributes(target, attrs) { - for(const k in attrs) { - if(attrs.hasOwnProperty(k)) { - target.setAttribute(k, attrs[k]); + function unwrap(node) { + if(node === null) { + return null; + } else if(node.element) { + return node.element; + } else { + return node; + } + } + + class WrappedElement { + constructor(element) { + this.element = element; + } + + addBefore(child = null, before = null) { + if(child === null) { + return this; + } else if(Array.isArray(child)) { + for(const c of child) { + this.addBefore(c, before); + } + } else { + const childElement = make(child, this.element.ownerDocument); + this.element.insertBefore(childElement, unwrap(before)); + } + return this; + } + + add(...child) { + return this.addBefore(child, null); + } + + del(child = null) { + if(child !== null) { + this.element.removeChild(unwrap(child)); + } + return this; + } + + attr(key, value) { + this.element.setAttribute(key, value); + return this; + } + + attrs(attrs) { + for(const k in attrs) { + if(attrs.hasOwnProperty(k)) { + this.element.setAttribute(k, attrs[k]); + } + } + return this; + } + + styles(styles) { + for(const k in styles) { + if(styles.hasOwnProperty(k)) { + this.element.style[k] = styles[k]; + } + } + return this; + } + + setClass(cls) { + return this.attr('class', cls); + } + + addClass(cls) { + const classes = this.element.getAttribute('class'); + if(!classes) { + return this.setClass(cls); + } + const list = classes.split(' '); + if(list.includes(cls)) { + return this; + } + list.push(cls); + return this.attr('class', list.join(' ')); + } + + delClass(cls) { + const classes = this.element.getAttribute('class'); + if(!classes) { + return this; + } + const list = classes.split(' '); + const p = list.indexOf(cls); + if(p !== -1) { + list.splice(p, 1); + this.attr('class', list.join(' ')); + } + return this; + } + + text(text) { + this.element.textContent = text; + return this; + } + + on(event, callback, options = {}) { + if(Array.isArray(event)) { + for(const e of event) { + this.on(e, callback, options); + } + } else { + this.element.addEventListener(event, callback, options); + } + return this; + } + + off(event, callback, options = {}) { + if(Array.isArray(event)) { + for(const e of event) { + this.off(e, callback, options); + } + } else { + this.element.removeEventListener(event, callback, options); + } + return this; + } + + val(value) { + this.element.value = value; + return this; + } + + select(start, end = null) { + this.element.selectionStart = start; + this.element.selectionEnd = (end === null) ? start : end; + return this; + } + + focus() { + this.element.focus(); + return this; + } + + focussed() { + return this.element === this.element.ownerDocument.activeElement; + } + + empty() { + while(this.element.childNodes.length > 0) { + this.element.removeChild(this.element.lastChild); + } + return this; + } + + attach(parent) { + unwrap(parent).appendChild(this.element); + return this; + } + + detach() { + this.element.parentNode.removeChild(this.element); + return this; + } + } + + return class DOMWrapper { + constructor(document) { + if(!document) { + throw new Error('Missing document!'); + } + this.document = document; + this.wrap = this.wrap.bind(this); + this.el = this.el.bind(this); + this.txt = this.txt.bind(this); + } + + wrap(element) { + if(element.element) { + return element; + } else { + return new WrappedElement(element); } } - } - function make(type, attrs = {}, children = []) { - const o = document.createElementNS(NS, type); - setAttributes(o, attrs); - for(const c of children) { - o.appendChild(c); + el(tag, namespace = null) { + let element = null; + if(namespace === null) { + element = this.document.createElement(tag); + } else { + element = this.document.createElementNS(namespace, tag); + } + return new WrappedElement(element); } - return o; - } - function makeContainer(attrs = {}) { - return make('svg', Object.assign({ - 'xmlns': NS, - 'version': '1.1', - }, attrs)); - } - - function empty(node) { - while(node.childNodes.length > 0) { - node.removeChild(node.lastChild); + txt(content = '') { + return this.document.createTextNode(content); } - } - - return { - makeText, - make, - makeContainer, - setAttributes, - empty, }; }); -define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { +define('svg/SVGTextBlock',[],() => { 'use strict'; // Thanks, https://stackoverflow.com/a/9851769/1180785 const firefox = (typeof window.InstallTrigger !== 'undefined'); - function fontDetails(attrs) { - const size = Number(attrs['font-size']); - const lineHeight = size * (Number(attrs['line-height']) || 1); - return { - size, - lineHeight, - }; - } - function merge(state, newState) { for(const k in state) { if(state.hasOwnProperty(k)) { @@ -3524,17 +3686,15 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { } } - function populateSvgTextLine(node, formattedLine) { + function populateSvgTextLine(svg, node, formattedLine) { if(!Array.isArray(formattedLine)) { throw new Error('Invalid formatted text line: ' + formattedLine); } formattedLine.forEach(({text, attrs}) => { - const textNode = svg.makeText(text); if(attrs) { - const span = svg.make('tspan', attrs, [textNode]); - node.appendChild(span); + node.add(svg.el('tspan').attrs(attrs).add(text)); } else { - node.appendChild(textNode); + node.add(text); } }); } @@ -3542,8 +3702,9 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { const EMPTY = []; class SVGTextBlock { - constructor(container, initialState = {}) { + constructor(container, svg, initialState = {}) { this.container = container; + this.svg = svg; this.state = { attrs: {}, formatted: EMPTY, @@ -3556,19 +3717,18 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { _rebuildLines(count) { if(count > this.lines.length) { - const attrs = Object.assign({ - 'x': this.state.x, - }, this.state.attrs); - while(this.lines.length < count) { - const node = svg.make('text', attrs); - this.container.appendChild(node); - this.lines.push({node, latest: ''}); + this.lines.push({ + node: this.svg.el('text') + .attr('x', this.state.x) + .attrs(this.state.attrs) + .attach(this.container), + latest: '', + }); } } else { while(this.lines.length > count) { - const {node} = this.lines.pop(); - this.container.removeChild(node); + this.lines.pop().node.detach(); } } } @@ -3593,8 +3753,8 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { this.lines.forEach((ln, i) => { const id = JSON.stringify(formatted[i]); if(id !== ln.latest) { - svg.empty(ln.node); - populateSvgTextLine(ln.node, formatted[i]); + ln.node.empty(); + populateSvgTextLine(this.svg, ln.node, formatted[i]); ln.latest = id; } }); @@ -3602,22 +3762,18 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { _updateX() { this.lines.forEach(({node}) => { - node.setAttribute('x', this.state.x); + node.attr('x', this.state.x); }); } _updateY() { - const {size, lineHeight} = fontDetails(this.state.attrs); - this.lines.forEach(({node}, i) => { - node.setAttribute('y', this.state.y + i * lineHeight + size); - }); - } - - firstLine() { - if(this.lines.length > 0) { - return this.lines[0].node; - } else { - return null; + const sizer = this.svg.textSizer; + let y = this.state.y; + for(let i = 0; i < this.lines.length; ++ i) { + const line = [this.state.formatted[i]]; + const baseline = sizer.baseline(this.state.attrs, line); + this.lines[i].node.attr('y', y + baseline); + y += sizer.measureHeight(this.state.attrs, line); } } @@ -3646,160 +3802,47 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { } } - class SizeTester { - constructor(container) { - this.testers = svg.make('g', { + SVGTextBlock.TextSizer = class TextSizer { + constructor(svg) { + this.svg = svg; + this.testers = this.svg.el('g').attrs({ // Firefox fails to measure non-displayed text 'display': firefox ? 'block' : 'none', 'visibility': 'hidden', }); - this.container = container; - this.cache = new Map(); - this.nodes = null; + this.container = svg.body; } - _expectMeasure({attrs, formatted}) { - if(!formatted.length) { - return; - } - - const attrKey = JSON.stringify(attrs); - let attrCache = this.cache.get(attrKey); - if(!attrCache) { - attrCache = { - attrs, - lines: new Map(), - }; - this.cache.set(attrKey, attrCache); - } - - formatted.forEach((line) => { - if(!line.length) { - return; - } - - const labelKey = JSON.stringify(line); - if(!attrCache.lines.has(labelKey)) { - attrCache.lines.set(labelKey, { - formatted: line, - width: null, - }); - } - }); - - return attrCache; + baseline({attrs}) { + return Number(attrs['font-size']); } - _measureHeight({attrs, formatted}) { - return formatted.length * fontDetails(attrs).lineHeight; + measureHeight({attrs, formatted}) { + const size = this.baseline({attrs, formatted}); + const lineHeight = size * (Number(attrs['line-height']) || 1); + return formatted.length * lineHeight; } - _measureLine(attrCache, line) { - if(!line.length) { - return 0; - } - - const labelKey = JSON.stringify(line); - const cache = attrCache.lines.get(labelKey); - if(cache.width === null) { - window.console.warn('Performing unexpected measurement', line); - this.performMeasurements(); - } - return cache.width; + prepMeasurement(attrs, formatted) { + const node = this.svg.el('text') + .attrs(attrs) + .attach(this.testers); + populateSvgTextLine(this.svg, node, formatted); + return node; } - _measureWidth(opts) { - if(!opts.formatted.length) { - return 0; - } - - const attrCache = this._expectMeasure(opts); - - return (opts.formatted - .map((line) => this._measureLine(attrCache, line)) - .reduce((a, b) => Math.max(a, b), 0) - ); + prepComplete() { + this.container.add(this.testers); } - _getMeasurementOpts(attrs, formatted) { - if(!formatted) { - if(typeof attrs === 'object' && attrs.state) { - formatted = attrs.state.formatted || []; - attrs = attrs.state.attrs; - } else { - formatted = []; - } - } else if(!Array.isArray(formatted)) { - throw new Error('Invalid formatted text: ' + formatted); - } - return {attrs, formatted}; + performMeasurement(node) { + return node.element.getComputedTextLength(); } - expectMeasure(attrs, formatted) { - const opts = this._getMeasurementOpts(attrs, formatted); - this._expectMeasure(opts); + teardown() { + this.container.del(this.testers.empty()); } - - performMeasurementsPre() { - this.nodes = []; - this.cache.forEach(({attrs, lines}) => { - lines.forEach((cacheLine) => { - if(cacheLine.width === null) { - const node = svg.make('text', attrs); - populateSvgTextLine(node, cacheLine.formatted); - this.testers.appendChild(node); - this.nodes.push({node, cacheLine}); - } - }); - }); - - if(this.nodes.length) { - this.container.appendChild(this.testers); - } - } - - performMeasurementsAct() { - this.nodes.forEach(({node, cacheLine}) => { - cacheLine.width = node.getComputedTextLength(); - }); - } - - performMeasurementsPost() { - if(this.nodes.length) { - this.container.removeChild(this.testers); - svg.empty(this.testers); - } - this.nodes = null; - } - - performMeasurements() { - // getComputedTextLength forces a reflow, so we try to batch as - // many measurements as possible into a single DOM change - - this.performMeasurementsPre(); - this.performMeasurementsAct(); - this.performMeasurementsPost(); - } - - measure(attrs, formatted) { - const opts = this._getMeasurementOpts(attrs, formatted); - return { - width: this._measureWidth(opts), - height: this._measureHeight(opts), - }; - } - - measureHeight(attrs, formatted) { - const opts = this._getMeasurementOpts(attrs, formatted); - return this._measureHeight(opts); - } - - resetCache() { - this.cache.clear(); - } - } - - SVGTextBlock.SizeTester = SizeTester; + }; return SVGTextBlock; }); @@ -3919,51 +3962,16 @@ define('svg/PatternedLine',[],() => { }; }); -define('svg/SVGShapes',[ - './SVGUtilities', +define('svg/SVG',[ './SVGTextBlock', './PatternedLine', ], ( - svg, SVGTextBlock, PatternedLine ) => { 'use strict'; - function renderBox(attrs, position) { - return svg.make('rect', Object.assign({}, position, attrs)); - } - - function renderLine(attrs, position) { - return svg.make('line', Object.assign({}, position, attrs)); - } - - function renderNote(attrs, flickAttrs, position) { - const x0 = position.x; - const x1 = position.x + position.width; - const y0 = position.y; - const y1 = position.y + position.height; - const flick = 7; - - return svg.make('g', {}, [ - svg.make('polygon', Object.assign({ - 'points': ( - x0 + ' ' + y0 + ' ' + - (x1 - flick) + ' ' + y0 + ' ' + - x1 + ' ' + (y0 + flick) + ' ' + - x1 + ' ' + y1 + ' ' + - x0 + ' ' + y1 - ), - }, attrs)), - svg.make('polyline', Object.assign({ - 'points': ( - (x1 - flick) + ' ' + y0 + ' ' + - (x1 - flick) + ' ' + (y0 + flick) + ' ' + - x1 + ' ' + (y0 + flick) - ), - }, flickAttrs)), - ]); - } + const NS = 'http://www.w3.org/2000/svg'; function calculateAnchor(x, attrs, padding) { let shift = 0; @@ -3985,68 +3993,333 @@ define('svg/SVGShapes',[ return {shift, anchorX}; } - function renderBoxedText(formatted, { - x, - y, - padding, - boxAttrs, - labelAttrs, - boxLayer, - labelLayer, - boxRenderer = null, - SVGTextBlockClass = SVGTextBlock, - textSizer, - }) { - if(!formatted || !formatted.length) { - return {width: 0, height: 0, label: null, box: null}; + const defaultTextSizerFactory = (svg) => new SVGTextBlock.TextSizer(svg); + + class TextSizerWrapper { + constructor(sizer) { + this.sizer = sizer; + this.cache = new Map(); + this.active = null; } - const {shift, anchorX} = calculateAnchor(x, labelAttrs, padding); + _expectMeasure({attrs, formatted}) { + if(!formatted.length) { + return; + } - const label = new SVGTextBlockClass(labelLayer, { - attrs: labelAttrs, - formatted, - x: anchorX, - y: y + padding.top, - }); + const attrKey = JSON.stringify(attrs); + let attrCache = this.cache.get(attrKey); + if(!attrCache) { + attrCache = { + attrs, + lines: new Map(), + }; + this.cache.set(attrKey, attrCache); + } - const size = textSizer.measure(label); - const width = (size.width + padding.left + padding.right); - const height = (size.height + padding.top + padding.bottom); + formatted.forEach((line) => { + if(!line.length) { + return; + } - let box = null; - if(boxRenderer) { - box = boxRenderer({ - 'x': anchorX - size.width * shift - padding.left, - 'y': y, - 'width': width, - 'height': height, + const labelKey = JSON.stringify(line); + if(!attrCache.lines.has(labelKey)) { + attrCache.lines.set(labelKey, { + formatted: line, + width: null, + }); + } }); - } else { - box = renderBox(boxAttrs, { - 'x': anchorX - size.width * shift - padding.left, - 'y': y, - 'width': width, - 'height': height, + + return attrCache; + } + + _measureLine(attrCache, line) { + if(!line.length) { + return 0; + } + + const labelKey = JSON.stringify(line); + const cache = attrCache.lines.get(labelKey); + if(cache.width === null) { + window.console.warn('Performing unexpected measurement', line); + this.performMeasurements(); + } + return cache.width; + } + + _measureWidth(opts) { + if(!opts.formatted.length) { + return 0; + } + + const attrCache = this._expectMeasure(opts); + + return (opts.formatted + .map((line) => this._measureLine(attrCache, line)) + .reduce((a, b) => Math.max(a, b), 0) + ); + } + + _getMeasurementOpts(attrs, formatted) { + if(!formatted) { + formatted = []; + if(attrs.textBlock) { + attrs = attrs.textBlock; + } + if(attrs.state) { + formatted = attrs.state.formatted || []; + attrs = attrs.state.attrs; + } + } + if(!Array.isArray(formatted)) { + throw new Error('Invalid formatted text: ' + formatted); + } + return {attrs, formatted}; + } + + expectMeasure(attrs, formatted) { + const opts = this._getMeasurementOpts(attrs, formatted); + this._expectMeasure(opts); + } + + performMeasurementsPre() { + this.active = []; + this.cache.forEach(({attrs, lines}) => { + lines.forEach((cacheLine) => { + if(cacheLine.width === null) { + this.active.push({ + data: this.sizer.prepMeasurement( + attrs, + cacheLine.formatted + ), + cacheLine, + }); + } + }); + }); + + if(this.active.length) { + this.sizer.prepComplete(); + } + } + + performMeasurementsAct() { + this.active.forEach(({data, cacheLine}) => { + cacheLine.width = this.sizer.performMeasurement(data); }); } - if(boxLayer === labelLayer) { - boxLayer.insertBefore(box, label.firstLine()); - } else { - boxLayer.appendChild(box); + performMeasurementsPost() { + if(this.active.length) { + this.sizer.teardown(); + } + this.active = null; } - return {width, height, label, box}; + performMeasurements() { + // getComputedTextLength forces a reflow, so we try to batch as + // many measurements as possible into a single DOM change + + try { + this.performMeasurementsPre(); + this.performMeasurementsAct(); + } finally { + this.performMeasurementsPost(); + } + } + + measure(attrs, formatted) { + const opts = this._getMeasurementOpts(attrs, formatted); + return { + width: this._measureWidth(opts), + height: this.sizer.measureHeight(opts), + }; + } + + baseline(attrs, formatted) { + const opts = this._getMeasurementOpts(attrs, formatted); + return this.sizer.baseline(opts); + } + + measureHeight(attrs, formatted) { + const opts = this._getMeasurementOpts(attrs, formatted); + return this.sizer.measureHeight(opts); + } + + resetCache() { + this.cache.clear(); + } } - return { - renderBox, - renderLine, - renderNote, - renderBoxedText, - TextBlock: SVGTextBlock, - PatternedLine, + return class SVG { + constructor(domWrapper, textSizerFactory = null) { + this.dom = domWrapper; + this.body = this.el('svg').attrs({'xmlns': NS, 'version': '1.1'}); + const fn = (textSizerFactory || defaultTextSizerFactory); + this.textSizer = new TextSizerWrapper(fn(this)); + + this.txt = this.txt.bind(this); + this.el = this.el.bind(this); + } + + linearGradient(attrs, stops) { + return this.el('linearGradient') + .attrs(attrs) + .add(stops.map((stop) => this.el('stop').attrs(stop))); + } + + patternedLine(pattern = null, phase = 0) { + return new PatternedLine(pattern, phase); + } + + txt(content) { + return this.dom.txt(content); + } + + el(tag, namespace = NS) { + return this.dom.el(tag, namespace); + } + + box(attrs, position) { + return this.el('rect').attrs(attrs).attrs(position); + } + + boxFactory(attrs) { + return this.box.bind(this, attrs); + } + + line(attrs, position) { + return this.el('line').attrs(attrs).attrs(position); + } + + lineFactory(attrs) { + return this.line.bind(this, attrs); + } + + circle(attrs, {x, y, radius}) { + return this.el('circle') + .attrs({ + 'cx': x, + 'cy': y, + 'r': radius, + }) + .attrs(attrs); + } + + circleFactory(attrs) { + return this.circle.bind(this, attrs); + } + + cross(attrs, {x, y, radius}) { + return this.el('path') + .attr('d', ( + 'M' + (x - radius) + ' ' + (y - radius) + + 'l' + (radius * 2) + ' ' + (radius * 2) + + 'm0 ' + (-radius * 2) + + 'l' + (-radius * 2) + ' ' + (radius * 2) + )) + .attrs(attrs); + } + + crossFactory(attrs) { + return this.cross.bind(this, attrs); + } + + note(attrs, flickAttrs, position) { + const x0 = position.x; + const x1 = position.x + position.width; + const y0 = position.y; + const y1 = position.y + position.height; + const flick = 7; + + return this.el('g').add( + this.el('polygon') + .attr('points', ( + x0 + ' ' + y0 + ' ' + + (x1 - flick) + ' ' + y0 + ' ' + + x1 + ' ' + (y0 + flick) + ' ' + + x1 + ' ' + y1 + ' ' + + x0 + ' ' + y1 + )) + .attrs(attrs), + this.el('polyline') + .attr('points', ( + (x1 - flick) + ' ' + y0 + ' ' + + (x1 - flick) + ' ' + (y0 + flick) + ' ' + + x1 + ' ' + (y0 + flick) + )) + .attrs(flickAttrs) + ); + } + + noteFactory(attrs, flickAttrs) { + return this.note.bind(this, attrs, flickAttrs); + } + + formattedText(attrs = {}, formatted = [], position = {}) { + const container = this.el('g'); + const txt = new SVGTextBlock(container, this, { + attrs, + formatted, + x: position.x, + y: position.y, + }); + return Object.assign(container, { + set: (state) => txt.set(state), + textBlock: txt, + }); + } + + formattedTextFactory(attrs) { + return this.formattedText.bind(this, attrs); + } + + boxedText({ + padding, + labelAttrs, + boxAttrs = {}, + boxRenderer = null, + }, formatted, {x, y}) { + if(!formatted || !formatted.length) { + return Object.assign(this.el('g'), { + width: 0, + height: 0, + box: null, + label: null, + }); + } + + const {shift, anchorX} = calculateAnchor(x, labelAttrs, padding); + + const label = this.formattedText(labelAttrs, formatted, { + x: anchorX, + y: y + padding.top, + }); + + const size = this.textSizer.measure(label); + const width = (size.width + padding.left + padding.right); + const height = (size.height + padding.top + padding.bottom); + + const boxFn = boxRenderer || this.boxFactory(boxAttrs); + const box = boxFn({ + 'x': anchorX - size.width * shift - padding.left, + 'y': y, + 'width': width, + 'height': height, + }); + + return Object.assign(this.el('g').add(box, label), { + width, + height, + box, + label, + }); + } + + boxedTextFactory(options) { + return this.boxedText.bind(this, options); + } }; }); @@ -4118,6 +4391,7 @@ define('sequence/components/BaseComponent',[],() => { blockLayer, theme, agentInfos, + svg, textSizer, SVGTextBlockClass, addDef, @@ -4178,16 +4452,17 @@ define('sequence/components/BaseComponent',[],() => { define('sequence/components/Block',[ './BaseComponent', 'core/ArrayUtilities', - 'svg/SVGUtilities', - 'svg/SVGShapes', ], ( BaseComponent, - array, - svg, - SVGShapes + array ) => { 'use strict'; + const OUTLINE_ATTRS = { + 'fill': 'transparent', + 'class': 'outline', + }; + class BlockSplit extends BaseComponent { prepareMeasurements({left, tag, label}, env) { const blockInfo = env.state.blocks.get(left); @@ -4230,58 +4505,46 @@ define('sequence/components/Block',[ const clickable = env.makeRegion(); - const tagRender = SVGShapes.renderBoxedText(tag, { - x: agentInfoL.x, - y, + const tagRender = env.svg.boxedText({ padding: config.section.tag.padding, boxAttrs: config.section.tag.boxAttrs, boxRenderer: config.section.tag.boxRenderer, labelAttrs: config.section.tag.labelAttrs, - boxLayer: blockInfo.hold, - labelLayer: clickable, - SVGTextBlockClass: env.SVGTextBlockClass, - textSizer: env.textSizer, - }); + }, tag, {x: agentInfoL.x, y}); - const labelRender = SVGShapes.renderBoxedText(label, { - x: agentInfoL.x + tagRender.width, - y, + const labelRender = env.svg.boxedText({ padding: config.section.label.padding, boxAttrs: {'fill': '#000000'}, labelAttrs: config.section.label.labelAttrs, - boxLayer: env.lineMaskLayer, - labelLayer: clickable, - SVGTextBlockClass: env.SVGTextBlockClass, - textSizer: env.textSizer, - }); + }, label, {x: agentInfoL.x + tagRender.width, y}); const labelHeight = Math.max( Math.max(tagRender.height, labelRender.height), config.section.label.minHeight ); - clickable.insertBefore(svg.make('rect', { - 'x': agentInfoL.x, - 'y': y, - 'width': agentInfoR.x - agentInfoL.x, - 'height': labelHeight, - 'fill': 'transparent', - 'class': 'outline', - }), clickable.firstChild); + blockInfo.hold.add(tagRender.box); + env.lineMaskLayer.add(labelRender.box); + clickable.add( + env.svg.box(OUTLINE_ATTRS, { + 'x': agentInfoL.x, + 'y': y, + 'width': agentInfoR.x - agentInfoL.x, + 'height': labelHeight, + }), + tagRender.label, + labelRender.label + ); if(!first) { - blockInfo.hold.appendChild(config.sepRenderer({ + blockInfo.hold.add(config.sepRenderer({ 'x1': agentInfoL.x, 'y1': y, 'x2': agentInfoR.x, 'y2': y, })); } else if(blockInfo.canHide) { - clickable.setAttribute( - 'class', - clickable.getAttribute('class') + - (blockInfo.hide ? ' collapsed' : ' expanded') - ); + clickable.addClass(blockInfo.hide ? 'collapsed' : 'expanded'); } return y + labelHeight + config.section.padding.top; @@ -4337,8 +4600,8 @@ define('sequence/components/Block',[ } render(stage, env) { - const hold = svg.make('g'); - env.blockLayer.appendChild(hold); + const hold = env.svg.el('g'); + env.blockLayer.add(hold); const blockInfo = env.state.blocks.get(stage.left); blockInfo.hold = hold; @@ -4392,13 +4655,9 @@ define('sequence/components/Block',[ shapes = {shape: shapes}; } - blockInfo.hold.appendChild(shapes.shape); - if(shapes.fill) { - env.fillLayer.appendChild(shapes.fill); - } - if(shapes.mask) { - env.lineMaskLayer.appendChild(shapes.mask); - } + blockInfo.hold.add(shapes.shape); + env.fillLayer.add(shapes.fill); + env.lineMaskLayer.add(shapes.mask); return env.primaryY + config.margin.bottom + env.theme.actionMargin; } @@ -4569,16 +4828,17 @@ define('sequence/components/Marker',['./BaseComponent'], (BaseComponent) => { define('sequence/components/AgentCap',[ './BaseComponent', 'core/ArrayUtilities', - 'svg/SVGUtilities', - 'svg/SVGShapes', ], ( BaseComponent, - array, - svg, - SVGShapes + array ) => { 'use strict'; + const OUTLINE_ATTRS = { + 'fill': 'transparent', + 'class': 'outline', + }; + class CapBox { getConfig(options, env) { let config = null; @@ -4620,27 +4880,18 @@ define('sequence/components/AgentCap',[ render(y, {x, formattedLabel, options}, env) { const config = this.getConfig(options, env); - const clickable = env.makeRegion(); - const text = SVGShapes.renderBoxedText(formattedLabel, { - x, - y, - padding: config.padding, - boxAttrs: config.boxAttrs, - boxRenderer: config.boxRenderer, - labelAttrs: config.labelAttrs, - boxLayer: clickable, - labelLayer: clickable, - SVGTextBlockClass: env.SVGTextBlockClass, - textSizer: env.textSizer, - }); - clickable.insertBefore(svg.make('rect', { - 'x': x - text.width / 2, - 'y': y, - 'width': text.width, - 'height': text.height, - 'fill': 'transparent', - 'class': 'outline', - }), text.label.firstLine()); + + const text = env.svg.boxedText(config, formattedLabel, {x, y}); + + env.makeRegion().add( + text, + env.svg.box(OUTLINE_ATTRS, { + 'x': x - text.width / 2, + 'y': y, + 'width': text.width, + 'height': text.height, + }) + ); return { lineTop: 0, @@ -4672,22 +4923,20 @@ define('sequence/components/AgentCap',[ const config = env.theme.agentCap.cross; const d = config.size / 2; - const clickable = env.makeRegion(); - - clickable.appendChild(config.render({ - x, - y: y + d, - radius: d, - options, - })); - clickable.appendChild(svg.make('rect', { - 'x': x - d, - 'y': y, - 'width': d * 2, - 'height': d * 2, - 'fill': 'transparent', - 'class': 'outline', - })); + env.makeRegion().add( + config.render({ + x, + y: y + d, + radius: d, + options, + }), + env.svg.box(OUTLINE_ATTRS, { + 'x': x - d, + 'y': y, + 'width': d * 2, + 'height': d * 2, + }) + ); return { lineTop: d, @@ -4733,22 +4982,21 @@ define('sequence/components/AgentCap',[ ); const height = barCfg.height; - const clickable = env.makeRegion(); - clickable.appendChild(barCfg.render({ - x: x - width / 2, - y, - width, - height, - options, - })); - clickable.appendChild(svg.make('rect', { - 'x': x - width / 2, - 'y': y, - 'width': width, - 'height': height, - 'fill': 'transparent', - 'class': 'outline', - })); + env.makeRegion().add( + barCfg.render({ + x: x - width / 2, + y, + width, + height, + options, + }), + env.svg.box(OUTLINE_ATTRS, { + 'x': x - width / 2, + 'y': y, + 'width': width, + 'height': height, + }) + ); return { lineTop: 0, @@ -4780,38 +5028,37 @@ define('sequence/components/AgentCap',[ const ratio = config.height / (config.height + config.extend); const gradID = env.addDef(isBegin ? 'FadeIn' : 'FadeOut', () => { - return svg.make('linearGradient', { + return env.svg.linearGradient({ 'x1': '0%', 'y1': isBegin ? '100%' : '0%', 'x2': '0%', 'y2': isBegin ? '0%' : '100%', }, [ - svg.make('stop', { + { 'offset': '0%', 'stop-color': '#FFFFFF', - }), - svg.make('stop', { + }, + { 'offset': (100 * ratio).toFixed(3) + '%', 'stop-color': '#000000', - }), + }, ]); }); - env.lineMaskLayer.appendChild(svg.make('rect', { + env.lineMaskLayer.add(env.svg.box({ + 'fill': 'url(#' + gradID + ')', + }, { 'x': x - config.width / 2, 'y': y - (isBegin ? config.extend : 0), 'width': config.width, 'height': config.height + config.extend, - 'fill': 'url(#' + gradID + ')', })); - env.makeRegion().appendChild(svg.make('rect', { + env.makeRegion().add(env.svg.box(OUTLINE_ATTRS, { 'x': x - config.width / 2, 'y': y, 'width': config.width, 'height': config.height, - 'fill': 'transparent', - 'class': 'outline', })); return { @@ -4843,13 +5090,11 @@ define('sequence/components/AgentCap',[ const config = env.theme.agentCap.none; const w = 10; - env.makeRegion().appendChild(svg.make('rect', { + env.makeRegion().add(env.svg.box(OUTLINE_ATTRS, { 'x': x - w / 2, 'y': y, 'width': w, 'height': config.height, - 'fill': 'transparent', - 'class': 'outline', })); return { @@ -4927,12 +5172,7 @@ define('sequence/components/AgentCap',[ const cap = AGENT_CAPS[mode]; const topShift = cap.topShift(agentInfo, env, this.begin); const y0 = env.primaryY - topShift; - const shifts = cap.render( - y0, - agentInfo, - env, - this.begin - ); + const shifts = cap.render(y0, agentInfo, env, this.begin); maxEnd = Math.max(maxEnd, y0 + shifts.height); if(this.begin) { env.drawAgentLine(id, y0 + shifts.lineBottom); @@ -5003,16 +5243,17 @@ define('sequence/components/AgentHighlight',['./BaseComponent'], (BaseComponent) define('sequence/components/Connect',[ 'core/ArrayUtilities', './BaseComponent', - 'svg/SVGUtilities', - 'svg/SVGShapes', ], ( array, - BaseComponent, - svg, - SVGShapes + BaseComponent ) => { 'use strict'; + const OUTLINE_ATTRS = { + 'fill': 'transparent', + 'class': 'outline', + }; + class Arrowhead { constructor(propName) { this.propName = propName; @@ -5040,7 +5281,7 @@ define('sequence/components/Connect',[ render(layer, theme, pt, dir) { const config = this.getConfig(theme); const short = this.short(theme); - layer.appendChild(config.render(config.attrs, { + layer.add(config.render(config.attrs, { x: pt.x + short * dir.dx, y: pt.y + short * dir.dy, width: config.width, @@ -5078,7 +5319,7 @@ define('sequence/components/Connect',[ render(layer, theme, pt, dir) { const config = this.getConfig(theme); - layer.appendChild(config.render({ + layer.add(config.render({ x: pt.x + config.short * dir.dx, y: pt.y + config.short * dir.dy, radius: config.radius, @@ -5196,7 +5437,7 @@ define('sequence/components/Connect',[ xR, rad: config.loopbackRadius, }); - clickable.appendChild(rendered.shape); + clickable.add(rendered.shape); lArrow.render(clickable, env.theme, { x: rendered.p1.x - dx1, @@ -5229,19 +5470,15 @@ define('sequence/components/Connect',[ (label ? config.label.padding : 0) ); - const clickable = env.makeRegion(); - - const renderedText = SVGShapes.renderBoxedText(label, { - x: xL - config.mask.padding.left, - y: yBegin - height + config.label.margin.top, + const renderedText = env.svg.boxedText({ padding: config.mask.padding, boxAttrs: {'fill': '#000000'}, labelAttrs: config.label.loopbackAttrs, - boxLayer: env.lineMaskLayer, - labelLayer: clickable, - SVGTextBlockClass: env.SVGTextBlockClass, - textSizer: env.textSizer, + }, label, { + x: xL - config.mask.padding.left, + y: yBegin - height + config.label.margin.top, }); + const labelW = (label ? ( renderedText.width + config.label.padding - @@ -5254,6 +5491,20 @@ define('sequence/components/Connect',[ xL + labelW ); + const raise = Math.max(height, lArrow.height(env.theme) / 2); + const arrowDip = rArrow.height(env.theme) / 2; + + env.lineMaskLayer.add(renderedText.box); + const clickable = env.makeRegion().add( + env.svg.box(OUTLINE_ATTRS, { + 'x': from.x, + 'y': yBegin - raise, + 'width': xR + config.loopbackRadius - from.x, + 'height': raise + env.primaryY - yBegin + arrowDip, + }), + renderedText.label + ); + this.renderRevArrowLine({ x1: from.x + from.currentMaxRad, y1: yBegin, @@ -5262,18 +5513,6 @@ define('sequence/components/Connect',[ xR, }, options, env, clickable); - const raise = Math.max(height, lArrow.height(env.theme) / 2); - const arrowDip = rArrow.height(env.theme) / 2; - - clickable.insertBefore(svg.make('rect', { - 'x': from.x, - 'y': yBegin - raise, - 'width': xR + config.loopbackRadius - from.x, - 'height': raise + env.primaryY - yBegin + arrowDip, - 'fill': 'transparent', - 'class': 'outline', - }), clickable.firstChild); - return ( env.primaryY + Math.max(arrowDip, 0) + @@ -5302,7 +5541,7 @@ define('sequence/components/Connect',[ x2: x2 - d2 * dx, y2: y2 - d2 * dy, }); - clickable.appendChild(rendered.shape); + clickable.add(rendered.shape); const p1 = {x: rendered.p1.x - d1 * dx, y: rendered.p1.y - d1 * dy}; const p2 = {x: rendered.p2.x + d2 * dx, y: rendered.p2.y + d2 * dy}; @@ -5322,14 +5561,14 @@ define('sequence/components/Connect',[ const config = env.theme.connect.source; if(from.isVirtualSource) { - clickable.appendChild(config.render({ + clickable.add(config.render({ x: rendered.p1.x - config.radius, y: rendered.p1.y, radius: config.radius, })); } if(to.isVirtualSource) { - clickable.appendChild(config.render({ + clickable.add(config.render({ x: rendered.p2.x + config.radius, y: rendered.p2.y, radius: config.radius, @@ -5354,21 +5593,20 @@ define('sequence/components/Connect',[ ')' ); boxAttrs.transform = transform; - labelLayer = svg.make('g', {'transform': transform}); - layer.appendChild(labelLayer); + labelLayer = env.svg.el('g').attrs({'transform': transform}); + layer.add(labelLayer); } - SVGShapes.renderBoxedText(label, { - x: midX, - y: midY + config.label.margin.top - height, + const text = env.svg.boxedText({ padding: config.mask.padding, boxAttrs, labelAttrs: config.label.attrs, - boxLayer: env.lineMaskLayer, - labelLayer, - SVGTextBlockClass: env.SVGTextBlockClass, - textSizer: env.textSizer, + }, label, { + x: midX, + y: midY + config.label.margin.top - height, }); + env.lineMaskLayer.add(text.box); + labelLayer.add(text.label); } renderSimpleConnect({label, agentIDs, options}, env, from, yBegin) { @@ -5404,17 +5642,16 @@ define('sequence/components/Connect',[ this.renderVirtualSources({from, to, rendered}, env, clickable); - clickable.appendChild(svg.make('path', { - 'd': ( + clickable.add(env.svg.el('path') + .attrs(OUTLINE_ATTRS) + .attr('d', ( 'M' + x1 + ',' + (yBegin - lift) + 'L' + x2 + ',' + (env.primaryY - lift) + 'L' + x2 + ',' + (env.primaryY + arrowSpread) + 'L' + x1 + ',' + (yBegin + arrowSpread) + 'Z' - ), - 'fill': 'transparent', - 'class': 'outline', - })); + )) + ); this.renderSimpleLabel(label, { layer: clickable, @@ -5545,9 +5782,14 @@ define('sequence/components/Connect',[ }; }); -define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => { +define('sequence/components/Note',['./BaseComponent'], (BaseComponent) => { 'use strict'; + const OUTLINE_ATTRS = { + 'fill': 'transparent', + 'class': 'outline', + }; + function findExtremes(agentInfos, agentIDs) { let min = null; let max = null; @@ -5586,14 +5828,8 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base }, env) { const config = env.theme.getNote(mode); - const clickable = env.makeRegion(); - const y = env.topY + config.margin.top + config.padding.top; - const labelNode = new env.SVGTextBlockClass(clickable, { - attrs: config.labelAttrs, - formatted: label, - y, - }); + const labelNode = env.svg.formattedText(config.labelAttrs, label); const size = env.textSizer.measure(labelNode); const fullW = ( @@ -5632,21 +5868,21 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base break; } - clickable.insertBefore(svg.make('rect', { - 'x': x0, - 'y': env.topY + config.margin.top, - 'width': x1 - x0, - 'height': fullH, - 'fill': 'transparent', - 'class': 'outline', - }), clickable.firstChild); - - clickable.insertBefore(config.boxRenderer({ - x: x0, - y: env.topY + config.margin.top, - width: x1 - x0, - height: fullH, - }), clickable.firstChild); + env.makeRegion().add( + config.boxRenderer({ + x: x0, + y: env.topY + config.margin.top, + width: x1 - x0, + height: fullH, + }), + env.svg.box(OUTLINE_ATTRS, { + 'x': x0, + 'y': env.topY + config.margin.top, + 'width': x1 - x0, + 'height': fullH, + }), + labelNode + ); return ( env.topY + @@ -5822,15 +6058,16 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base define('sequence/components/Divider',[ './BaseComponent', - 'svg/SVGUtilities', - 'svg/SVGShapes', ], ( - BaseComponent, - svg, - SVGShapes + BaseComponent ) => { 'use strict'; + const OUTLINE_ATTRS = { + 'fill': 'transparent', + 'class': 'outline', + }; + class Divider extends BaseComponent { prepareMeasurements({mode, formattedLabel}, env) { const config = env.theme.getDivider(mode); @@ -5863,8 +6100,6 @@ define('sequence/components/Divider',[ const left = env.agentInfos.get('['); const right = env.agentInfos.get(']'); - const clickable = env.makeRegion({unmasked: true}); - let labelWidth = 0; let labelHeight = 0; if(formattedLabel) { @@ -5876,22 +6111,22 @@ define('sequence/components/Divider',[ const fullHeight = Math.max(height, labelHeight) + config.margin; + let labelText = null; if(formattedLabel) { - const boxed = SVGShapes.renderBoxedText(formattedLabel, { + const boxed = env.svg.boxedText({ + padding: config.padding, + boxAttrs: {'fill': '#000000'}, + labelAttrs: config.labelAttrs, + }, formattedLabel, { x: (left.x + right.x) / 2, y: ( env.primaryY + (fullHeight - labelHeight) / 2 - config.padding.top ), - padding: config.padding, - boxAttrs: {'fill': '#000000'}, - labelAttrs: config.labelAttrs, - boxLayer: env.fullMaskLayer, - labelLayer: clickable, - SVGTextBlockClass: env.SVGTextBlockClass, - textSizer: env.textSizer, }); + env.fullMaskLayer.add(boxed.box); + labelText = boxed.label; labelWidth = boxed.width; } @@ -5904,20 +6139,18 @@ define('sequence/components/Divider',[ height, env, }); - if(shape) { - clickable.insertBefore(shape, clickable.firstChild); - } - if(mask) { - env.fullMaskLayer.appendChild(mask); - } - clickable.insertBefore(svg.make('rect', { - 'x': left.x - config.extend, - 'y': env.primaryY, - 'width': right.x - left.x + config.extend * 2, - 'height': fullHeight, - 'fill': 'transparent', - 'class': 'outline', - }), clickable.firstChild); + env.fullMaskLayer.add(mask); + + env.makeRegion({unmasked: true}).add( + env.svg.box(OUTLINE_ATTRS, { + 'x': left.x - config.extend, + 'y': env.primaryY, + 'width': right.x - left.x + config.extend * 2, + 'height': fullHeight, + }), + shape, + labelText + ); return env.primaryY + fullHeight + env.theme.actionMargin; } @@ -5932,8 +6165,8 @@ define('sequence/components/Divider',[ define('sequence/Renderer',[ 'core/ArrayUtilities', 'core/EventObject', - 'svg/SVGUtilities', - 'svg/SVGShapes', + 'core/DOMWrapper', + 'svg/SVG', './components/BaseComponent', './components/Block', './components/Parallel', @@ -5946,8 +6179,8 @@ define('sequence/Renderer',[ ], ( array, EventObject, - svg, - SVGShapes, + DOMWrapper, + SVG, BaseComponent ) => { /* jshint +W072 */ @@ -5998,7 +6231,8 @@ define('sequence/Renderer',[ themes = [], namespace = null, components = null, - SVGTextBlockClass = SVGShapes.TextBlock, + document, + textSizerFactory = null, } = {}) { super(); @@ -6012,10 +6246,11 @@ define('sequence/Renderer',[ this.width = 0; this.height = 0; this.themes = makeThemes(themes); + this.themeBuilder = null; this.theme = null; this.namespace = parseNamespace(namespace); this.components = components; - this.SVGTextBlockClass = SVGTextBlockClass; + this.svg = new SVG(new DOMWrapper(document), textSizerFactory); this.knownThemeDefs = new Set(); this.knownDefs = new Set(); this.highlights = new Map(); @@ -6040,61 +6275,54 @@ define('sequence/Renderer',[ this.themes.set(theme.name, theme); } - buildMetadata() { - this.metaCode = svg.makeText(); - return svg.make('metadata', {}, [this.metaCode]); - } - buildStaticElements() { - this.base = svg.makeContainer(); + const el = this.svg.el; - this.themeDefs = svg.make('defs'); - this.defs = svg.make('defs'); - this.fullMask = svg.make('mask', { + this.metaCode = this.svg.txt(); + this.themeDefs = el('defs'); + this.defs = el('defs'); + this.fullMask = el('mask').attrs({ 'id': this.namespace + 'FullMask', 'maskUnits': 'userSpaceOnUse', }); - this.lineMask = svg.make('mask', { + this.lineMask = el('mask').attrs({ 'id': this.namespace + 'LineMask', 'maskUnits': 'userSpaceOnUse', }); - this.fullMaskReveal = svg.make('rect', {'fill': '#FFFFFF'}); - this.lineMaskReveal = svg.make('rect', {'fill': '#FFFFFF'}); - this.backgroundFills = svg.make('g'); - this.agentLines = svg.make('g', { - 'mask': 'url(#' + this.namespace + 'LineMask)', - }); - this.blocks = svg.make('g'); - this.shapes = svg.make('g'); - this.unmaskedShapes = svg.make('g'); - this.base.appendChild(this.buildMetadata()); - this.base.appendChild(this.themeDefs); - this.base.appendChild(this.defs); - this.base.appendChild(this.backgroundFills); - this.base.appendChild( - svg.make('g', { - 'mask': 'url(#' + this.namespace + 'FullMask)', - }, [ - this.agentLines, - this.blocks, - this.shapes, - ]) - ); - this.base.appendChild(this.unmaskedShapes); - this.title = new this.SVGTextBlockClass(this.base); + this.fullMaskReveal = el('rect').attr('fill', '#FFFFFF'); + this.lineMaskReveal = el('rect').attr('fill', '#FFFFFF'); + this.backgroundFills = el('g'); + this.agentLines = el('g') + .attr('mask', 'url(#' + this.namespace + 'LineMask)'); + this.blocks = el('g'); + this.shapes = el('g'); + this.unmaskedShapes = el('g'); + this.title = this.svg.formattedText(); - this.sizer = new this.SVGTextBlockClass.SizeTester(this.base); + this.svg.body.add( + this.svg.el('metadata') + .add(this.metaCode), + this.themeDefs, + this.defs, + this.backgroundFills, + el('g') + .attr('mask', 'url(#' + this.namespace + 'FullMask)') + .add( + this.agentLines, + this.blocks, + this.shapes + ), + this.unmaskedShapes, + this.title + ); } addThemeDef(name, generator) { const namespacedName = this.namespace + name; - if(this.knownThemeDefs.has(name)) { - return namespacedName; + if(!this.knownThemeDefs.has(name)) { + this.knownThemeDefs.add(name); + this.themeDefs.add(generator().attr('id', namespacedName)); } - this.knownThemeDefs.add(name); - const def = generator(); - def.setAttribute('id', namespacedName); - this.themeDefs.appendChild(def); return namespacedName; } @@ -6106,13 +6334,10 @@ define('sequence/Renderer',[ } const namespacedName = this.namespace + name; - if(this.knownDefs.has(name)) { - return namespacedName; + if(!this.knownDefs.has(name)) { + this.knownDefs.add(name); + this.defs.add(generator().attr('id', namespacedName)); } - this.knownDefs.add(name); - const def = generator(); - def.setAttribute('id', namespacedName); - this.defs.appendChild(def); return namespacedName; } @@ -6133,7 +6358,7 @@ define('sequence/Renderer',[ renderer: this, theme: this.theme, agentInfos: this.agentInfos, - textSizer: this.sizer, + textSizer: this.svg.textSizer, state: this.state, components: this.components, }; @@ -6181,7 +6406,7 @@ define('sequence/Renderer',[ agentInfos: this.agentInfos, visibleAgentIDs: this.visibleAgentIDs, momentaryAgentIDs: agentIDs, - textSizer: this.sizer, + textSizer: this.svg.textSizer, addSpacing, addSeparation, state: this.state, @@ -6231,7 +6456,7 @@ define('sequence/Renderer',[ renderer: this, theme: this.theme, agentInfos: this.agentInfos, - textSizer: this.sizer, + textSizer: this.svg.textSizer, state: this.state, components: this.components, }; @@ -6276,20 +6501,18 @@ define('sequence/Renderer',[ drawAgentLine(agentInfo, toY) { if( - agentInfo.latestYStart === null || - toY <= agentInfo.latestYStart + agentInfo.latestYStart !== null && + toY > agentInfo.latestYStart ) { - return; + this.agentLines.add(this.theme.renderAgentLine({ + x: agentInfo.x, + y0: agentInfo.latestYStart, + y1: toY, + width: agentInfo.currentRad * 2, + className: 'agent-' + agentInfo.index + '-line', + options: agentInfo.options, + })); } - - this.agentLines.appendChild(this.theme.renderAgentLine({ - x: agentInfo.x, - y0: agentInfo.latestYStart, - y1: toY, - width: agentInfo.currentRad * 2, - className: 'agent-' + agentInfo.index + '-line', - options: agentInfo.options, - })); } addHighlightObject(line, o) { @@ -6302,7 +6525,7 @@ define('sequence/Renderer',[ } forwardEvent(source, sourceEvent, forwardEvent, forwardArgs) { - source.addEventListener( + source.on( sourceEvent, this.trigger.bind(this, forwardEvent, forwardArgs) ); @@ -6318,7 +6541,7 @@ define('sequence/Renderer',[ renderer: this, theme: this.theme, agentInfos: this.agentInfos, - textSizer: this.sizer, + textSizer: this.svg.textSizer, state: this.state, components: this.components, }; @@ -6333,16 +6556,14 @@ define('sequence/Renderer',[ stageOverride = null, unmasked = false, } = {}) => { - const o = svg.make('g'); + const o = this.svg.el('g').setClass('region'); const targetStage = (stageOverride || stage); this.addHighlightObject(targetStage.ln, o); - o.setAttribute('class', 'region'); this.forwardEvent(o, 'mouseenter', 'mouseover', [targetStage]); this.forwardEvent(o, 'mouseleave', 'mouseout', [targetStage]); this.forwardEvent(o, 'click', 'click', [targetStage]); this.forwardEvent(o, 'dblclick', 'dblclick', [targetStage]); - (unmasked ? this.unmaskedShapes : this.shapes).appendChild(o); - return o; + return o.attach(unmasked ? this.unmaskedShapes : this.shapes); }; const env = { @@ -6355,8 +6576,7 @@ define('sequence/Renderer',[ lineMaskLayer: this.lineMask, theme: this.theme, agentInfos: this.agentInfos, - textSizer: this.sizer, - SVGTextBlockClass: this.SVGTextBlockClass, + textSizer: this.svg.textSizer, state: this.state, drawAgentLine: (agentID, toY, andStop = false) => { const agentInfo = this.agentInfos.get(agentID); @@ -6366,6 +6586,7 @@ define('sequence/Renderer',[ addDef: this.addDef, makeRegion, components: this.components, + svg: this.svg, }; let bottomY = topY; @@ -6441,7 +6662,7 @@ define('sequence/Renderer',[ updateBounds(stagesHeight) { const cx = (this.minX + this.maxX) / 2; - const titleSize = this.sizer.measure(this.title); + const titleSize = this.svg.textSizer.measure(this.title); const titleY = ((titleSize.height > 0) ? (-this.theme.titleMargin - titleSize.height) : 0 ); @@ -6464,10 +6685,10 @@ define('sequence/Renderer',[ 'height': this.height, }; - svg.setAttributes(this.fullMaskReveal, fullSize); - svg.setAttributes(this.lineMaskReveal, fullSize); + this.fullMaskReveal.attrs(fullSize); + this.lineMaskReveal.attrs(fullSize); - this.base.setAttribute('viewBox', ( + this.svg.body.attr('viewBox', ( x0 + ' ' + y0 + ' ' + this.width + ' ' + this.height )); @@ -6484,23 +6705,23 @@ define('sequence/Renderer',[ _reset(theme) { if(theme) { this.knownThemeDefs.clear(); - svg.empty(this.themeDefs); + this.themeDefs.empty(); } this.knownDefs.clear(); this.highlights.clear(); - svg.empty(this.defs); - svg.empty(this.fullMask); - svg.empty(this.lineMask); - svg.empty(this.backgroundFills); - svg.empty(this.agentLines); - svg.empty(this.blocks); - svg.empty(this.shapes); - svg.empty(this.unmaskedShapes); - this.fullMask.appendChild(this.fullMaskReveal); - this.lineMask.appendChild(this.lineMaskReveal); - this.defs.appendChild(this.fullMask); - this.defs.appendChild(this.lineMask); + this.defs.empty(); + this.fullMask.empty(); + this.lineMask.empty(); + this.backgroundFills.empty(); + this.agentLines.empty(); + this.blocks.empty(); + this.shapes.empty(); + this.unmaskedShapes.empty(); + this.defs.add( + this.fullMask.add(this.fullMaskReveal), + this.lineMask.add(this.lineMaskReveal) + ); this._resetState(); } @@ -6513,12 +6734,12 @@ define('sequence/Renderer',[ } if(this.highlights.has(this.currentHighlight)) { this.highlights.get(this.currentHighlight).forEach((o) => { - o.setAttribute('class', 'region'); + o.delClass('focus'); }); } if(this.highlights.has(line)) { this.highlights.get(line).forEach((o) => { - o.setAttribute('class', 'region focus'); + o.addClass('focus'); }); } this.currentHighlight = line; @@ -6568,11 +6789,14 @@ define('sequence/Renderer',[ } _switchTheme(name) { - const oldTheme = this.theme; - this.theme = this.getThemeNamed(name); + const oldThemeBuilder = this.themeBuilder; + this.themeBuilder = this.getThemeNamed(name); + if(this.themeBuilder !== oldThemeBuilder) { + this.theme = this.themeBuilder.build(this.svg); + } this.theme.reset(); - return (this.theme !== oldTheme); + return (this.themeBuilder !== oldThemeBuilder); } optimisedRenderPreReflow(sequence) { @@ -6586,7 +6810,7 @@ define('sequence/Renderer',[ attrs: this.theme.titleAttrs, formatted: sequence.meta.title, }); - this.sizer.expectMeasure(this.title); + this.svg.textSizer.expectMeasure(this.title); this.minX = 0; this.maxX = 0; @@ -6595,11 +6819,11 @@ define('sequence/Renderer',[ sequence.stages.forEach(this.prepareMeasurementsStage); this._resetState(); - this.sizer.performMeasurementsPre(); + this.svg.textSizer.performMeasurementsPre(); } optimisedRenderReflow() { - this.sizer.performMeasurementsAct(); + this.svg.textSizer.performMeasurementsAct(); } optimisedRenderPostReflow(sequence) { @@ -6619,8 +6843,8 @@ define('sequence/Renderer',[ this.currentHighlight = -1; this.setHighlight(prevHighlight); - this.sizer.performMeasurementsPost(); - this.sizer.resetCache(); + this.svg.textSizer.performMeasurementsPost(); + this.svg.textSizer.resetCache(); } render(sequence) { @@ -6651,8 +6875,8 @@ define('sequence/Renderer',[ return this.agentInfos.get(id).x; } - svg() { - return this.base; + dom() { + return this.svg.body.element; } }; }); @@ -6675,7 +6899,7 @@ define('sequence/Exporter',[],() => { } getSVGContent(renderer) { - let code = renderer.svg().outerHTML; + let code = renderer.dom().outerHTML; // Firefox fails to render SVGs as unless they have size // attributes on the tag, so we must set this when @@ -7017,28 +7241,9 @@ define('sequence/CodeMirrorHints',['core/ArrayUtilities'], (array) => { }; }); -define('sequence/themes/BaseTheme',[ - 'svg/SVGUtilities', - 'svg/SVGShapes', -], ( - svg, - SVGShapes -) => { +define('sequence/themes/BaseTheme',[],() => { 'use strict'; - function deepCopy(o) { - if(typeof o !== 'object' || !o) { - return o; - } - const r = {}; - for(const k in o) { - if(o.hasOwnProperty(k)) { - r[k] = deepCopy(o[k]); - } - } - return r; - } - function optionsAttributes(attributes, options) { let attrs = Object.assign({}, attributes['']); options.forEach((opt) => { @@ -7048,14 +7253,12 @@ define('sequence/themes/BaseTheme',[ } class BaseTheme { - constructor({name, settings, blocks, notes, dividers}) { - this.name = name; - this.blocks = deepCopy(blocks); - this.notes = deepCopy(notes); - this.dividers = deepCopy(dividers); - Object.assign(this, deepCopy(settings)); + constructor(svg) { + this.svg = svg; } + // PUBLIC API + reset() { } @@ -7081,116 +7284,297 @@ define('sequence/themes/BaseTheme',[ renderAgentLine({x, y0, y1, width, className, options}) { const attrs = this.optionsAttributes(this.agentLineAttrs, options); if(width > 0) { - return svg.make('rect', Object.assign({ + return this.svg.box(attrs, { 'x': x - width / 2, 'y': y0, 'width': width, 'height': y1 - y0, - 'class': className, - }, attrs)); + }).addClass(className); } else { - return svg.make('line', Object.assign({ + return this.svg.line(attrs, { 'x1': x, 'y1': y0, 'x2': x, 'y2': y1, - 'class': className, - }, attrs)); + }).addClass(className); } } - } - BaseTheme.renderArrowHead = (attrs, {x, y, width, height, dir}) => { - const wx = width * dir.dx; - const wy = width * dir.dy; - const hy = height * 0.5 * dir.dx; - const hx = -height * 0.5 * dir.dy; - return svg.make( - attrs.fill === 'none' ? 'polyline' : 'polygon', - Object.assign({ - 'points': ( + // INTERNAL HELPERS + + renderArrowHead(attrs, {x, y, width, height, dir}) { + const wx = width * dir.dx; + const wy = width * dir.dy; + const hy = height * 0.5 * dir.dx; + const hx = -height * 0.5 * dir.dy; + return this.svg.el(attrs.fill === 'none' ? 'polyline' : 'polygon') + .attr('points', ( (x + wx - hx) + ' ' + (y + wy - hy) + ' ' + x + ' ' + y + ' ' + (x + wx + hx) + ' ' + (y + wy + hy) - ), - }, attrs) - ); - }; - - BaseTheme.renderTag = (attrs, {x, y, width, height}) => { - const {rx, ry} = attrs; - const x2 = x + width; - const y2 = y + height; - - const line = ( - 'M' + x2 + ' ' + y + - 'L' + x2 + ' ' + (y2 - ry) + - 'L' + (x2 - rx) + ' ' + y2 + - 'L' + x + ' ' + y2 - ); - - const g = svg.make('g'); - if(attrs.fill !== 'none') { - g.appendChild(svg.make('path', Object.assign({ - 'd': line + 'L' + x + ' ' + y, - }, attrs, {'stroke': 'none'}))); + )) + .attrs(attrs); } - if(attrs.stroke !== 'none') { - g.appendChild(svg.make('path', Object.assign({ - 'd': line, - }, attrs, {'fill': 'none'}))); + renderTag(attrs, {x, y, width, height}) { + const {rx, ry} = attrs; + const x2 = x + width; + const y2 = y + height; + + const line = ( + 'M' + x2 + ' ' + y + + 'L' + x2 + ' ' + (y2 - ry) + + 'L' + (x2 - rx) + ' ' + y2 + + 'L' + x + ' ' + y2 + ); + + const g = this.svg.el('g'); + + if(attrs.fill !== 'none') { + g.add(this.svg.el('path') + .attr('d', line + 'L' + x + ' ' + y) + .attrs(attrs) + .attr('stroke', 'none') + ); + } + if(attrs.stroke !== 'none') { + g.add(this.svg.el('path') + .attr('d', line) + .attrs(attrs) + .attr('fill', 'none') + ); + } + + return g; } - return g; - }; + renderDB(attrs, position) { + const z = attrs['db-z']; + return this.svg.el('g').add( + this.svg.box({ + 'rx': position.width / 2, + 'ry': z, + }, position).attrs(attrs), + this.svg.el('path') + .attr('d', ( + 'M' + position.x + ' ' + (position.y + z) + + 'a' + (position.width / 2) + ' ' + z + + ' 0 0 0 ' + position.width + ' 0' + )) + .attrs(attrs) + .attr('fill', 'none') + ); + } - BaseTheme.renderDB = (attrs, {x, y, width, height}) => { - const z = attrs['db-z']; - return svg.make('g', {}, [ - svg.make('rect', Object.assign({ - 'x': x, - 'y': y, - 'width': width, - 'height': height, - 'rx': width / 2, - 'ry': z, - }, attrs)), - svg.make('path', Object.assign({ - 'd': ( - 'M' + x + ' ' + (y + z) + - 'a' + (width / 2) + ' ' + z + - ' 0 0 0 ' + width + ' 0' - ), - }, attrs, {'fill': 'none'})), - ]); - }; + renderRef(options, position) { + return { + shape: this.svg.box(options, position).attrs({'fill': 'none'}), + mask: this.svg.box(options, position).attrs({ + 'fill': '#000000', + 'stroke': 'none', + }), + fill: this.svg.box(options, position).attrs({'stroke': 'none'}), + }; + } - BaseTheme.renderCross = (attrs, {x, y, radius}) => { - return svg.make('path', Object.assign({ - 'd': ( - 'M' + (x - radius) + ' ' + (y - radius) + - 'l' + (radius * 2) + ' ' + (radius * 2) + - 'm0 ' + (-radius * 2) + - 'l' + (-radius * 2) + ' ' + (radius * 2) - ), - }, attrs)); - }; + renderFlatConnect( + pattern, + attrs, + {x1, y1, x2, y2} + ) { + return { + shape: this.svg.el('path') + .attr('d', this.svg.patternedLine(pattern) + .move(x1, y1) + .line(x2, y2) + .cap() + .asPath() + ) + .attrs(attrs), + p1: {x: x1, y: y1}, + p2: {x: x2, y: y2}, + }; + } - BaseTheme.renderRef = (options, position) => { - return { - shape: SVGShapes.renderBox(Object.assign({}, options, { - 'fill': 'none', - }), position), - mask: SVGShapes.renderBox(Object.assign({}, options, { - 'fill': '#000000', - 'stroke': 'none', - }), position), - fill: SVGShapes.renderBox(Object.assign({}, options, { - 'stroke': 'none', - }), position), - }; - }; + renderRevConnect( + pattern, + attrs, + {x1, y1, x2, y2, xR, rad} + ) { + const maxRad = (y2 - y1) / 2; + const line = this.svg.patternedLine(pattern) + .move(x1, y1) + .line(xR, y1); + if(rad < maxRad) { + line + .arc(xR, y1 + rad, Math.PI / 2) + .line(xR + rad, y2 - rad) + .arc(xR, y2 - rad, Math.PI / 2); + } else { + line.arc(xR, (y1 + y2) / 2, Math.PI); + } + return { + shape: this.svg.el('path') + .attr('d', line + .line(x2, y2) + .cap() + .asPath() + ) + .attrs(attrs), + p1: {x: x1, y: y1}, + p2: {x: x2, y: y2}, + }; + } + + renderLineDivider( + {lineAttrs}, + {x, y, labelWidth, width, height} + ) { + let shape = null; + const yPos = y + height / 2; + if(labelWidth > 0) { + shape = this.svg.el('g').add( + this.svg.line({'fill': 'none'}, { + 'x1': x, + 'y1': yPos, + 'x2': x + (width - labelWidth) / 2, + 'y2': yPos, + }).attrs(lineAttrs), + this.svg.line({'fill': 'none'}, { + 'x1': x + (width + labelWidth) / 2, + 'y1': yPos, + 'x2': x + width, + 'y2': yPos, + }).attrs(lineAttrs) + ); + } else { + shape = this.svg.line({'fill': 'none'}, { + 'x1': x, + 'y1': yPos, + 'x2': x + width, + 'y2': yPos, + }).attrs(lineAttrs); + } + return {shape}; + } + + renderDelayDivider( + {dotSize, gapSize}, + {x, y, width, height} + ) { + const mask = this.svg.el('g'); + for(let i = 0; i + gapSize <= height; i += dotSize + gapSize) { + mask.add(this.svg.box({ + 'fill': '#000000', + }, { + 'x': x, + 'y': y + i, + 'width': width, + 'height': gapSize, + })); + } + return {mask}; + } + + renderTearDivider( + {fadeBegin, fadeSize, pattern, zigWidth, zigHeight, lineAttrs}, + {x, y, labelWidth, labelHeight, width, height, env} + ) { + const maskGradID = env.addDef('tear-grad', () => { + const px = 100 / width; + return this.svg.linearGradient({}, [ + { + 'offset': (fadeBegin * px) + '%', + 'stop-color': '#000000', + }, + { + 'offset': ((fadeBegin + fadeSize) * px) + '%', + 'stop-color': '#FFFFFF', + }, + { + 'offset': (100 - (fadeBegin + fadeSize) * px) + '%', + 'stop-color': '#FFFFFF', + }, + { + 'offset': (100 - fadeBegin * px) + '%', + 'stop-color': '#000000', + }, + ]); + }); + const shapeMask = this.svg.el('mask') + .attr('maskUnits', 'userSpaceOnUse') + .add( + this.svg.box({ + 'fill': 'url(#' + maskGradID + ')', + }, { + 'x': x, + 'y': y - 5, + 'width': width, + 'height': height + 10, + }) + ); + const shapeMaskID = env.addDef(shapeMask); + + if(labelWidth > 0) { + shapeMask.add(this.svg.box({ + 'rx': 2, + 'ry': 2, + 'fill': '#000000', + }, { + 'x': x + (width - labelWidth) / 2, + 'y': y + (height - labelHeight) / 2 - 1, + 'width': labelWidth, + 'height': labelHeight + 2, + })); + } + + if(!pattern) { + pattern = new BaseTheme.WavePattern( + zigWidth, + [zigHeight, -zigHeight] + ); + } + let mask = null; + + const pathTop = this.svg.patternedLine(pattern) + .move(x, y) + .line(x + width, y); + + const shape = this.svg.el('g') + .attr('mask', 'url(#' + shapeMaskID + ')') + .add( + this.svg.el('path') + .attrs({ + 'd': pathTop.asPath(), + 'fill': 'none', + }) + .attrs(lineAttrs) + ); + + if(height > 0) { + const pathBase = this.svg.patternedLine(pattern) + .move(x, y + height) + .line(x + width, y + height); + shape.add( + this.svg.el('path') + .attrs({ + 'd': pathBase.asPath(), + 'fill': 'none', + }) + .attrs(lineAttrs) + ); + pathTop + .line(pathBase.x, pathBase.y, {patterned: false}) + .cap(); + pathTop.points.push(...pathBase.points.reverse()); + mask = this.svg.el('path').attrs({ + 'd': pathTop.asPath(), + 'fill': '#000000', + }); + } + return {shape, mask}; + } + } BaseTheme.WavePattern = class WavePattern { constructor(width, height) { @@ -7216,208 +7600,10 @@ define('sequence/themes/BaseTheme',[ } }; - BaseTheme.renderFlatConnector = ( - pattern, - attrs, - {x1, y1, x2, y2} - ) => { - return { - shape: svg.make('path', Object.assign({ - d: new SVGShapes.PatternedLine(pattern) - .move(x1, y1) - .line(x2, y2) - .cap() - .asPath(), - }, attrs)), - p1: {x: x1, y: y1}, - p2: {x: x2, y: y2}, - }; - }; - - BaseTheme.renderRevConnector = ( - pattern, - attrs, - {x1, y1, x2, y2, xR, rad} - ) => { - const maxRad = (y2 - y1) / 2; - const line = new SVGShapes.PatternedLine(pattern) - .move(x1, y1) - .line(xR, y1); - if(rad < maxRad) { - line - .arc(xR, y1 + rad, Math.PI / 2) - .line(xR + rad, y2 - rad) - .arc(xR, y2 - rad, Math.PI / 2); - } else { - line.arc(xR, (y1 + y2) / 2, Math.PI); - } - return { - shape: svg.make('path', Object.assign({ - d: line - .line(x2, y2) - .cap() - .asPath(), - }, attrs)), - p1: {x: x1, y: y1}, - p2: {x: x2, y: y2}, - }; - }; - - BaseTheme.renderLineDivider = ( - {lineAttrs}, - {x, y, labelWidth, width, height} - ) => { - let shape = null; - const yPos = y + height / 2; - if(labelWidth > 0) { - shape = svg.make('g', {}, [ - svg.make('line', Object.assign({ - 'x1': x, - 'y1': yPos, - 'x2': x + (width - labelWidth) / 2, - 'y2': yPos, - 'fill': 'none', - }, lineAttrs)), - svg.make('line', Object.assign({ - 'x1': x + (width + labelWidth) / 2, - 'y1': yPos, - 'x2': x + width, - 'y2': yPos, - 'fill': 'none', - }, lineAttrs)), - ]); - } else { - shape = svg.make('line', Object.assign({ - 'x1': x, - 'y1': yPos, - 'x2': x + width, - 'y2': yPos, - 'fill': 'none', - }, lineAttrs)); - } - return {shape}; - }; - - BaseTheme.renderDelayDivider = ( - {dotSize, gapSize}, - {x, y, width, height} - ) => { - const mask = svg.make('g'); - for(let i = 0; i + gapSize <= height; i += dotSize + gapSize) { - mask.appendChild(svg.make('rect', { - 'x': x, - 'y': y + i, - 'width': width, - 'height': gapSize, - 'fill': '#000000', - })); - } - return {mask}; - }; - - BaseTheme.renderTearDivider = ( - {fadeBegin, fadeSize, pattern, zigWidth, zigHeight, lineAttrs}, - {x, y, labelWidth, labelHeight, width, height, env} - ) => { - const maskGradID = env.addDef('tear-grad', () => { - const px = 100 / width; - return svg.make('linearGradient', {}, [ - svg.make('stop', { - 'offset': (fadeBegin * px) + '%', - 'stop-color': '#000000', - }), - svg.make('stop', { - 'offset': ((fadeBegin + fadeSize) * px) + '%', - 'stop-color': '#FFFFFF', - }), - svg.make('stop', { - 'offset': (100 - (fadeBegin + fadeSize) * px) + '%', - 'stop-color': '#FFFFFF', - }), - svg.make('stop', { - 'offset': (100 - fadeBegin * px) + '%', - 'stop-color': '#000000', - }), - ]); - }); - const shapeMask = svg.make('mask', { - 'maskUnits': 'userSpaceOnUse', - }, [ - svg.make('rect', { - 'x': x, - 'y': y - 5, - 'width': width, - 'height': height + 10, - 'fill': 'url(#' + maskGradID + ')', - }), - ]); - const shapeMaskID = env.addDef(shapeMask); - - if(labelWidth > 0) { - shapeMask.appendChild(svg.make('rect', { - 'x': x + (width - labelWidth) / 2, - 'y': y + (height - labelHeight) / 2 - 1, - 'width': labelWidth, - 'height': labelHeight + 2, - 'rx': 2, - 'ry': 2, - 'fill': '#000000', - })); - } - - if(!pattern) { - pattern = new BaseTheme.WavePattern( - zigWidth, - [zigHeight, -zigHeight] - ); - } - let mask = null; - - const pathTop = new SVGShapes.PatternedLine(pattern) - .move(x, y) - .line(x + width, y); - - const shape = svg.make('g', { - 'mask': 'url(#' + shapeMaskID + ')', - }, [ - svg.make('path', Object.assign({ - 'd': pathTop.asPath(), - 'fill': 'none', - }, lineAttrs)), - ]); - - if(height > 0) { - const pathBase = new SVGShapes.PatternedLine(pattern) - .move(x, y + height) - .line(x + width, y + height); - shape.appendChild(svg.make('path', Object.assign({ - 'd': pathBase.asPath(), - 'fill': 'none', - }, lineAttrs))); - pathTop - .line(pathBase.x, pathBase.y, {patterned: false}) - .cap(); - pathTop.points.push(...pathBase.points.reverse()); - mask = svg.make('path', { - 'd': pathTop.asPath(), - 'fill': '#000000', - }); - } - return {shape, mask}; - }; - return BaseTheme; }); -define('sequence/themes/Basic',[ - './BaseTheme', - 'svg/SVGUtilities', - 'svg/SVGShapes', -], ( - BaseTheme, - svg, - SVGShapes -) => { +define('sequence/themes/Basic',['./BaseTheme'], (BaseTheme) => { 'use strict'; const FONT = 'sans-serif'; @@ -7425,339 +7611,12 @@ define('sequence/themes/Basic',[ const WAVE = new BaseTheme.WavePattern(6, 0.5); - const SETTINGS = { - titleMargin: 10, - outerMargin: 5, - agentMargin: 10, - actionMargin: 10, - minActionMargin: 3, - agentLineHighlightRadius: 4, - - agentCap: { - box: { - padding: { - top: 5, - left: 10, - right: 10, - bottom: 5, - }, - arrowBottom: 5 + 12 * 1.3 / 2, - boxAttrs: { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - }, - labelAttrs: { - 'font-family': FONT, - 'font-size': 12, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - }, - database: { - padding: { - top: 12, - left: 10, - right: 10, - bottom: 3, - }, - arrowBottom: 5 + 12 * 1.3 / 2, - boxRenderer: BaseTheme.renderDB.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - 'db-z': 5, - }), - labelAttrs: { - 'font-family': FONT, - 'font-size': 12, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - }, - cross: { - size: 20, - render: BaseTheme.renderCross.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }), - }, - bar: { - height: 4, - render: SVGShapes.renderBox.bind(null, { - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 1, - }), - }, - fade: { - width: 5, - height: 6, - extend: 1, - }, - none: { - height: 10, - }, - }, - - connect: { - loopbackRadius: 6, - line: { - 'solid': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, null), - renderRev: BaseTheme.renderRevConnector.bind(null, null), - }, - 'dash': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - 'stroke-dasharray': '4, 2', - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, null), - renderRev: BaseTheme.renderRevConnector.bind(null, null), - }, - 'wave': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - 'stroke-linejoin': 'round', - 'stroke-linecap': 'round', - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, WAVE), - renderRev: BaseTheme.renderRevConnector.bind(null, WAVE), - }, - }, - arrow: { - 'single': { - width: 5, - height: 10, - render: BaseTheme.renderArrowHead, - attrs: { - 'fill': '#000000', - 'stroke-width': 0, - 'stroke-linejoin': 'miter', - }, - }, - 'double': { - width: 4, - height: 6, - render: BaseTheme.renderArrowHead, - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - 'stroke-linejoin': 'miter', - }, - }, - 'cross': { - short: 7, - radius: 3, - render: BaseTheme.renderCross.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }), - }, - }, - label: { - padding: 6, - margin: {top: 2, bottom: 1}, - attrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - loopbackAttrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - }, - }, - source: { - radius: 2, - render: ({x, y, radius}) => { - return svg.make('circle', { - 'cx': x, - 'cy': y, - 'r': radius, - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 1, - }); - }, - }, - mask: { - padding: { - top: 0, - left: 3, - right: 3, - bottom: 1, - }, - }, - }, - - titleAttrs: { - 'font-family': FONT, - 'font-size': 20, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - 'class': 'title', - }, - - agentLineAttrs: { - '': { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }, - 'red': { - 'stroke': '#CC0000', - }, - }, - }; - - const SHARED_BLOCK_SECTION = { - padding: { - top: 3, - bottom: 2, - }, - tag: { - padding: { - top: 1, - left: 3, - right: 3, - bottom: 0, - }, - boxRenderer: BaseTheme.renderTag.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - 'rx': 2, - 'ry': 2, - }), - labelAttrs: { - 'font-family': FONT, - 'font-weight': 'bold', - 'font-size': 9, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'left', - }, - }, - label: { - minHeight: 4, - padding: { - top: 1, - left: 5, - right: 3, - bottom: 1, - }, - labelAttrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'left', - }, - }, - }; - - const BLOCKS = { - 'ref': { - margin: { - top: 0, - bottom: 0, - }, - boxRenderer: BaseTheme.renderRef.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1.5, - 'rx': 2, - 'ry': 2, - }), - section: SHARED_BLOCK_SECTION, - }, - '': { - margin: { - top: 0, - bottom: 0, - }, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1.5, - 'rx': 2, - 'ry': 2, - }), - collapsedBoxRenderer: BaseTheme.renderRef.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1.5, - 'rx': 2, - 'ry': 2, - }), - section: SHARED_BLOCK_SECTION, - sepRenderer: SVGShapes.renderLine.bind(null, { - 'stroke': '#000000', - 'stroke-width': 1.5, - 'stroke-dasharray': '4, 2', - }), - }, - }; - const NOTE_ATTRS = { 'font-family': FONT, 'font-size': 8, 'line-height': LINE_HEIGHT, }; - const NOTES = { - 'text': { - margin: {top: 0, left: 2, right: 2, bottom: 0}, - padding: {top: 2, left: 2, right: 2, bottom: 2}, - overlap: {left: 10, right: 10}, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': '#FFFFFF', - }), - labelAttrs: NOTE_ATTRS, - }, - 'note': { - margin: {top: 0, left: 5, right: 5, bottom: 0}, - padding: {top: 5, left: 5, right: 10, bottom: 5}, - overlap: {left: 10, right: 10}, - boxRenderer: SVGShapes.renderNote.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - }, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }), - labelAttrs: NOTE_ATTRS, - }, - 'state': { - margin: {top: 0, left: 5, right: 5, bottom: 0}, - padding: {top: 7, left: 7, right: 7, bottom: 7}, - overlap: {left: 10, right: 10}, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - 'rx': 10, - 'ry': 10, - }), - labelAttrs: NOTE_ATTRS, - }, - }; - const DIVIDER_LABEL_ATTRS = { 'font-family': FONT, 'font-size': 8, @@ -7765,74 +7624,391 @@ define('sequence/themes/Basic',[ 'text-anchor': 'middle', }; - const DIVIDERS = { - '': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 0, - margin: 0, - render: () => ({}), - }, - 'line': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 10, - margin: 0, - render: BaseTheme.renderLineDivider.bind(null, { - lineAttrs: { - 'stroke': '#000000', - }, - }), - }, - 'delay': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 0, - margin: 0, - render: BaseTheme.renderDelayDivider.bind(null, { - dotSize: 1, - gapSize: 2, - }), - }, - 'tear': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 10, - margin: 10, - render: BaseTheme.renderTearDivider.bind(null, { - fadeBegin: 5, - fadeSize: 10, - zigWidth: 6, - zigHeight: 1, - lineAttrs: { - 'stroke': '#000000', - }, - }), - }, - }; + class BasicTheme extends BaseTheme { + constructor(svg) { + super(svg); - return class BasicTheme extends BaseTheme { - constructor() { - super({ - name: 'basic', - settings: SETTINGS, - blocks: BLOCKS, - notes: NOTES, - dividers: DIVIDERS, + const sharedBlockSection = { + padding: { + top: 3, + bottom: 2, + }, + tag: { + padding: { + top: 1, + left: 3, + right: 3, + bottom: 0, + }, + boxRenderer: this.renderTag.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + 'rx': 2, + 'ry': 2, + }), + labelAttrs: { + 'font-family': FONT, + 'font-weight': 'bold', + 'font-size': 9, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'left', + }, + }, + label: { + minHeight: 4, + padding: { + top: 1, + left: 5, + right: 3, + bottom: 1, + }, + labelAttrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'left', + }, + }, + }; + + Object.assign(this, { + titleMargin: 10, + outerMargin: 5, + agentMargin: 10, + actionMargin: 10, + minActionMargin: 3, + agentLineHighlightRadius: 4, + + agentCap: { + box: { + padding: { + top: 5, + left: 10, + right: 10, + bottom: 5, + }, + arrowBottom: 5 + 12 * 1.3 / 2, + boxAttrs: { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + }, + labelAttrs: { + 'font-family': FONT, + 'font-size': 12, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + }, + database: { + padding: { + top: 12, + left: 10, + right: 10, + bottom: 3, + }, + arrowBottom: 5 + 12 * 1.3 / 2, + boxRenderer: this.renderDB.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + 'db-z': 5, + }), + labelAttrs: { + 'font-family': FONT, + 'font-size': 12, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + }, + cross: { + size: 20, + render: svg.crossFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + bar: { + height: 4, + render: svg.boxFactory({ + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + fade: { + width: 5, + height: 6, + extend: 1, + }, + none: { + height: 10, + }, + }, + + connect: { + loopbackRadius: 6, + line: { + 'solid': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }, + renderFlat: this.renderFlatConnect.bind(this, null), + renderRev: this.renderRevConnect.bind(this, null), + }, + 'dash': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + 'stroke-dasharray': '4, 2', + }, + renderFlat: this.renderFlatConnect.bind(this, null), + renderRev: this.renderRevConnect.bind(this, null), + }, + 'wave': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round', + }, + renderFlat: this.renderFlatConnect.bind(this, WAVE), + renderRev: this.renderRevConnect.bind(this, WAVE), + }, + }, + arrow: { + 'single': { + width: 5, + height: 10, + render: this.renderArrowHead.bind(this), + attrs: { + 'fill': '#000000', + 'stroke-width': 0, + 'stroke-linejoin': 'miter', + }, + }, + 'double': { + width: 4, + height: 6, + render: this.renderArrowHead.bind(this), + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + 'stroke-linejoin': 'miter', + }, + }, + 'cross': { + short: 7, + radius: 3, + render: svg.crossFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + }, + label: { + padding: 6, + margin: {top: 2, bottom: 1}, + attrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + loopbackAttrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + }, + }, + source: { + radius: 2, + render: svg.circleFactory({ + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + mask: { + padding: { + top: 0, + left: 3, + right: 3, + bottom: 1, + }, + }, + }, + + titleAttrs: { + 'font-family': FONT, + 'font-size': 20, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + 'class': 'title', + }, + + agentLineAttrs: { + '': { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }, + 'red': { + 'stroke': '#CC0000', + }, + }, + blocks: { + 'ref': { + margin: { + top: 0, + bottom: 0, + }, + boxRenderer: this.renderRef.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1.5, + 'rx': 2, + 'ry': 2, + }), + section: sharedBlockSection, + }, + '': { + margin: { + top: 0, + bottom: 0, + }, + boxRenderer: svg.boxFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1.5, + 'rx': 2, + 'ry': 2, + }), + collapsedBoxRenderer: this.renderRef.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1.5, + 'rx': 2, + 'ry': 2, + }), + section: sharedBlockSection, + sepRenderer: svg.lineFactory({ + 'stroke': '#000000', + 'stroke-width': 1.5, + 'stroke-dasharray': '4, 2', + }), + }, + }, + notes: { + 'text': { + margin: {top: 0, left: 2, right: 2, bottom: 0}, + padding: {top: 2, left: 2, right: 2, bottom: 2}, + overlap: {left: 10, right: 10}, + boxRenderer: svg.boxFactory({ + 'fill': '#FFFFFF', + }), + labelAttrs: NOTE_ATTRS, + }, + 'note': { + margin: {top: 0, left: 5, right: 5, bottom: 0}, + padding: {top: 5, left: 5, right: 10, bottom: 5}, + overlap: {left: 10, right: 10}, + boxRenderer: svg.noteFactory({ + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + }, { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }), + labelAttrs: NOTE_ATTRS, + }, + 'state': { + margin: {top: 0, left: 5, right: 5, bottom: 0}, + padding: {top: 7, left: 7, right: 7, bottom: 7}, + overlap: {left: 10, right: 10}, + boxRenderer: svg.boxFactory({ + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + 'rx': 10, + 'ry': 10, + }), + labelAttrs: NOTE_ATTRS, + }, + }, + dividers: { + '': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 0, + margin: 0, + render: () => ({}), + }, + 'line': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 10, + margin: 0, + render: this.renderLineDivider.bind(this, { + lineAttrs: { + 'stroke': '#000000', + }, + }), + }, + 'delay': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 0, + margin: 0, + render: this.renderDelayDivider.bind(this, { + dotSize: 1, + gapSize: 2, + }), + }, + 'tear': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 10, + margin: 10, + render: this.renderTearDivider.bind(this, { + fadeBegin: 5, + fadeSize: 10, + zigWidth: 6, + zigHeight: 1, + lineAttrs: { + 'stroke': '#000000', + }, + }), + }, + }, }); } + } + + BasicTheme.Factory = class { + constructor() { + this.name = 'basic'; + } + + build(svg) { + return new BasicTheme(svg); + } }; + + return BasicTheme; }); -define('sequence/themes/Monospace',[ - './BaseTheme', - 'svg/SVGUtilities', - 'svg/SVGShapes', -], ( - BaseTheme, - svg, - SVGShapes -) => { +define('sequence/themes/Monospace',['./BaseTheme'], (BaseTheme) => { 'use strict'; const FONT = 'monospace'; @@ -7849,331 +8025,12 @@ define('sequence/themes/Monospace',[ +0.25, ]); - const SETTINGS = { - titleMargin: 8, - outerMargin: 4, - agentMargin: 12, - actionMargin: 12, - minActionMargin: 4, - agentLineHighlightRadius: 4, - - agentCap: { - box: { - padding: { - top: 4, - left: 8, - right: 8, - bottom: 4, - }, - arrowBottom: 12, - boxAttrs: { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - }, - labelAttrs: { - 'font-family': FONT, - 'font-size': 12, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - }, - database: { - padding: { - top: 9, - left: 8, - right: 8, - bottom: 3, - }, - arrowBottom: 12, - boxRenderer: BaseTheme.renderDB.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - 'db-z': 4, - }), - labelAttrs: { - 'font-family': FONT, - 'font-size': 12, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - }, - cross: { - size: 16, - render: BaseTheme.renderCross.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }), - }, - bar: { - height: 4, - render: SVGShapes.renderBox.bind(null, { - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 1, - }), - }, - fade: { - width: 5, - height: 8, - extend: 1, - }, - none: { - height: 8, - }, - }, - - connect: { - loopbackRadius: 4, - line: { - 'solid': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, null), - renderRev: BaseTheme.renderRevConnector.bind(null, null), - }, - 'dash': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - 'stroke-dasharray': '4, 4', - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, null), - renderRev: BaseTheme.renderRevConnector.bind(null, null), - }, - 'wave': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, WAVE), - renderRev: BaseTheme.renderRevConnector.bind(null, WAVE), - }, - }, - arrow: { - 'single': { - width: 4, - height: 8, - render: BaseTheme.renderArrowHead, - attrs: { - 'fill': '#000000', - 'stroke-width': 0, - 'stroke-linejoin': 'miter', - }, - }, - 'double': { - width: 3, - height: 6, - render: BaseTheme.renderArrowHead, - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - 'stroke-linejoin': 'miter', - }, - }, - 'cross': { - short: 8, - radius: 4, - render: BaseTheme.renderCross.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }), - }, - }, - label: { - padding: 4, - margin: {top: 2, bottom: 1}, - attrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - loopbackAttrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - }, - }, - source: { - radius: 2, - render: ({x, y, radius}) => { - return svg.make('circle', { - 'cx': x, - 'cy': y, - 'r': radius, - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 1, - }); - }, - }, - mask: { - padding: { - top: 0, - left: 3, - right: 3, - bottom: 1, - }, - }, - }, - - titleAttrs: { - 'font-family': FONT, - 'font-size': 20, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - 'class': 'title', - }, - - agentLineAttrs: { - '': { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }, - 'red': { - 'stroke': '#AA0000', - }, - }, - }; - - const SHARED_BLOCK_SECTION = { - padding: { - top: 3, - bottom: 2, - }, - tag: { - padding: { - top: 2, - left: 4, - right: 4, - bottom: 2, - }, - boxRenderer: BaseTheme.renderTag.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - 'rx': 3, - 'ry': 3, - }), - labelAttrs: { - 'font-family': FONT, - 'font-weight': 'bold', - 'font-size': 9, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'left', - }, - }, - label: { - minHeight: 8, - padding: { - top: 2, - left: 8, - right: 8, - bottom: 2, - }, - labelAttrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'left', - }, - }, - }; - - const BLOCKS = { - 'ref': { - margin: { - top: 0, - bottom: 0, - }, - boxRenderer: BaseTheme.renderRef.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 2, - }), - section: SHARED_BLOCK_SECTION, - }, - '': { - margin: { - top: 0, - bottom: 0, - }, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 2, - }), - collapsedBoxRenderer: BaseTheme.renderRef.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 2, - }), - section: SHARED_BLOCK_SECTION, - sepRenderer: SVGShapes.renderLine.bind(null, { - 'stroke': '#000000', - 'stroke-width': 2, - 'stroke-dasharray': '8, 4', - }), - }, - }; - const NOTE_ATTRS = { 'font-family': FONT, 'font-size': 8, 'line-height': LINE_HEIGHT, }; - const NOTES = { - 'text': { - margin: {top: 0, left: 8, right: 8, bottom: 0}, - padding: {top: 4, left: 4, right: 4, bottom: 4}, - overlap: {left: 8, right: 8}, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': '#FFFFFF', - }), - labelAttrs: NOTE_ATTRS, - }, - 'note': { - margin: {top: 0, left: 8, right: 8, bottom: 0}, - padding: {top: 8, left: 8, right: 8, bottom: 8}, - overlap: {left: 8, right: 8}, - boxRenderer: SVGShapes.renderNote.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - }, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }), - labelAttrs: NOTE_ATTRS, - }, - 'state': { - margin: {top: 0, left: 8, right: 8, bottom: 0}, - padding: {top: 8, left: 8, right: 8, bottom: 8}, - overlap: {left: 8, right: 8}, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 1, - 'rx': 8, - 'ry': 8, - }), - labelAttrs: NOTE_ATTRS, - }, - }; - const DIVIDER_LABEL_ATTRS = { 'font-family': FONT, 'font-size': 8, @@ -8181,74 +8038,383 @@ define('sequence/themes/Monospace',[ 'text-anchor': 'middle', }; - const DIVIDERS = { - '': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 0, - margin: 0, - render: () => ({}), - }, - 'line': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 8, - margin: 0, - render: BaseTheme.renderLineDivider.bind(null, { - lineAttrs: { - 'stroke': '#000000', - }, - }), - }, - 'delay': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 0, - margin: 0, - render: BaseTheme.renderDelayDivider.bind(null, { - dotSize: 2, - gapSize: 2, - }), - }, - 'tear': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 8, - margin: 8, - render: BaseTheme.renderTearDivider.bind(null, { - fadeBegin: 4, - fadeSize: 4, - zigWidth: 4, - zigHeight: 1, - lineAttrs: { - 'stroke': '#000000', - }, - }), - }, - }; + class MonospaceTheme extends BaseTheme { + constructor(svg) { + super(svg); - return class MonospaceTheme extends BaseTheme { - constructor() { - super({ - name: 'monospace', - settings: SETTINGS, - blocks: BLOCKS, - notes: NOTES, - dividers: DIVIDERS, + const sharedBlockSection = { + padding: { + top: 3, + bottom: 2, + }, + tag: { + padding: { + top: 2, + left: 4, + right: 4, + bottom: 2, + }, + boxRenderer: this.renderTag.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + 'rx': 3, + 'ry': 3, + }), + labelAttrs: { + 'font-family': FONT, + 'font-weight': 'bold', + 'font-size': 9, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'left', + }, + }, + label: { + minHeight: 8, + padding: { + top: 2, + left: 8, + right: 8, + bottom: 2, + }, + labelAttrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'left', + }, + }, + }; + + Object.assign(this, { + titleMargin: 8, + outerMargin: 4, + agentMargin: 12, + actionMargin: 12, + minActionMargin: 4, + agentLineHighlightRadius: 4, + + agentCap: { + box: { + padding: { + top: 4, + left: 8, + right: 8, + bottom: 4, + }, + arrowBottom: 12, + boxAttrs: { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + }, + labelAttrs: { + 'font-family': FONT, + 'font-size': 12, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + }, + database: { + padding: { + top: 9, + left: 8, + right: 8, + bottom: 3, + }, + arrowBottom: 12, + boxRenderer: this.renderDB.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + 'db-z': 4, + }), + labelAttrs: { + 'font-family': FONT, + 'font-size': 12, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + }, + cross: { + size: 16, + render: svg.crossFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + bar: { + height: 4, + render: svg.boxFactory({ + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + fade: { + width: 5, + height: 8, + extend: 1, + }, + none: { + height: 8, + }, + }, + + connect: { + loopbackRadius: 4, + line: { + 'solid': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }, + renderFlat: this.renderFlatConnect.bind(this, null), + renderRev: this.renderRevConnect.bind(this, null), + }, + 'dash': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + 'stroke-dasharray': '4, 4', + }, + renderFlat: this.renderFlatConnect.bind(this, null), + renderRev: this.renderRevConnect.bind(this, null), + }, + 'wave': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }, + renderFlat: this.renderFlatConnect.bind(this, WAVE), + renderRev: this.renderRevConnect.bind(this, WAVE), + }, + }, + arrow: { + 'single': { + width: 4, + height: 8, + render: this.renderArrowHead.bind(this), + attrs: { + 'fill': '#000000', + 'stroke-width': 0, + 'stroke-linejoin': 'miter', + }, + }, + 'double': { + width: 3, + height: 6, + render: this.renderArrowHead.bind(this), + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + 'stroke-linejoin': 'miter', + }, + }, + 'cross': { + short: 8, + radius: 4, + render: svg.crossFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + }, + label: { + padding: 4, + margin: {top: 2, bottom: 1}, + attrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + loopbackAttrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + }, + }, + source: { + radius: 2, + render: svg.circleFactory({ + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + mask: { + padding: { + top: 0, + left: 3, + right: 3, + bottom: 1, + }, + }, + }, + + titleAttrs: { + 'font-family': FONT, + 'font-size': 20, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + 'class': 'title', + }, + + agentLineAttrs: { + '': { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }, + 'red': { + 'stroke': '#AA0000', + }, + }, + blocks: { + 'ref': { + margin: { + top: 0, + bottom: 0, + }, + boxRenderer: this.renderRef.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 2, + }), + section: sharedBlockSection, + }, + '': { + margin: { + top: 0, + bottom: 0, + }, + boxRenderer: svg.boxFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 2, + }), + collapsedBoxRenderer: this.renderRef.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 2, + }), + section: sharedBlockSection, + sepRenderer: svg.lineFactory({ + 'stroke': '#000000', + 'stroke-width': 2, + 'stroke-dasharray': '8, 4', + }), + }, + }, + notes: { + 'text': { + margin: {top: 0, left: 8, right: 8, bottom: 0}, + padding: {top: 4, left: 4, right: 4, bottom: 4}, + overlap: {left: 8, right: 8}, + boxRenderer: svg.boxFactory({ + 'fill': '#FFFFFF', + }), + labelAttrs: NOTE_ATTRS, + }, + 'note': { + margin: {top: 0, left: 8, right: 8, bottom: 0}, + padding: {top: 8, left: 8, right: 8, bottom: 8}, + overlap: {left: 8, right: 8}, + boxRenderer: svg.noteFactory({ + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + }, { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }), + labelAttrs: NOTE_ATTRS, + }, + 'state': { + margin: {top: 0, left: 8, right: 8, bottom: 0}, + padding: {top: 8, left: 8, right: 8, bottom: 8}, + overlap: {left: 8, right: 8}, + boxRenderer: svg.boxFactory({ + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 1, + 'rx': 8, + 'ry': 8, + }), + labelAttrs: NOTE_ATTRS, + }, + }, + dividers: { + '': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 0, + margin: 0, + render: () => ({}), + }, + 'line': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 8, + margin: 0, + render: this.renderLineDivider.bind(this, { + lineAttrs: { + 'stroke': '#000000', + }, + }), + }, + 'delay': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 0, + margin: 0, + render: this.renderDelayDivider.bind(this, { + dotSize: 2, + gapSize: 2, + }), + }, + 'tear': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 8, + margin: 8, + render: this.renderTearDivider.bind(this, { + fadeBegin: 4, + fadeSize: 4, + zigWidth: 4, + zigHeight: 1, + lineAttrs: { + 'stroke': '#000000', + }, + }), + }, + }, }); } + } + + MonospaceTheme.Factory = class { + constructor() { + this.name = 'monospace'; + } + + build(svg) { + return new MonospaceTheme(svg); + } }; + + return MonospaceTheme; }); -define('sequence/themes/Chunky',[ - './BaseTheme', - 'svg/SVGUtilities', - 'svg/SVGShapes', -], ( - BaseTheme, - svg, - SVGShapes -) => { +define('sequence/themes/Chunky',['./BaseTheme'], (BaseTheme) => { 'use strict'; const FONT = 'sans-serif'; @@ -8256,352 +8422,12 @@ define('sequence/themes/Chunky',[ const WAVE = new BaseTheme.WavePattern(10, 1); - const SETTINGS = { - titleMargin: 12, - outerMargin: 5, - agentMargin: 8, - actionMargin: 5, - minActionMargin: 5, - agentLineHighlightRadius: 4, - - agentCap: { - box: { - padding: { - top: 1, - left: 3, - right: 3, - bottom: 1, - }, - arrowBottom: 2 + 14 * 1.3 / 2, - boxAttrs: { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 3, - 'rx': 4, - 'ry': 4, - }, - labelAttrs: { - 'font-family': FONT, - 'font-weight': 'bold', - 'font-size': 14, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - }, - database: { - padding: { - top: 4, - left: 3, - right: 3, - bottom: 0, - }, - arrowBottom: 2 + 14 * 1.3 / 2, - boxRenderer: BaseTheme.renderDB.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 3, - 'db-z': 2, - }), - labelAttrs: { - 'font-family': FONT, - 'font-weight': 'bold', - 'font-size': 14, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - }, - cross: { - size: 20, - render: BaseTheme.renderCross.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 3, - 'stroke-linecap': 'round', - }), - }, - bar: { - height: 4, - render: SVGShapes.renderBox.bind(null, { - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 3, - 'rx': 2, - 'ry': 2, - }), - }, - fade: { - width: 5, - height: 10, - extend: 1, - }, - none: { - height: 10, - }, - }, - - connect: { - loopbackRadius: 8, - line: { - 'solid': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 3, - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, null), - renderRev: BaseTheme.renderRevConnector.bind(null, null), - }, - 'dash': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 3, - 'stroke-dasharray': '10, 4', - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, null), - renderRev: BaseTheme.renderRevConnector.bind(null, null), - }, - 'wave': { - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 3, - 'stroke-linejoin': 'round', - 'stroke-linecap': 'round', - }, - renderFlat: BaseTheme.renderFlatConnector.bind(null, WAVE), - renderRev: BaseTheme.renderRevConnector.bind(null, WAVE), - }, - }, - arrow: { - 'single': { - width: 10, - height: 12, - render: BaseTheme.renderArrowHead, - attrs: { - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 3, - 'stroke-linejoin': 'round', - }, - }, - 'double': { - width: 10, - height: 12, - render: BaseTheme.renderArrowHead, - attrs: { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 3, - 'stroke-linejoin': 'round', - 'stroke-linecap': 'round', - }, - }, - 'cross': { - short: 10, - radius: 5, - render: BaseTheme.renderCross.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 3, - 'stroke-linejoin': 'round', - 'stroke-linecap': 'round', - }), - }, - }, - label: { - padding: 7, - margin: {top: 2, bottom: 3}, - attrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - loopbackAttrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - }, - }, - source: { - radius: 5, - render: ({x, y, radius}) => { - return svg.make('circle', { - 'cx': x, - 'cy': y, - 'r': radius, - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 3, - }); - }, - }, - mask: { - padding: { - top: 1, - left: 5, - right: 5, - bottom: 3, - }, - }, - }, - - titleAttrs: { - 'font-family': FONT, - 'font-weight': 'bolder', - 'font-size': 20, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - 'class': 'title', - }, - - agentLineAttrs: { - '': { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 3, - }, - 'red': { - 'stroke': '#DD0000', - }, - }, - }; - - const SHARED_BLOCK_SECTION = { - padding: { - top: 3, - bottom: 4, - }, - tag: { - padding: { - top: 2, - left: 5, - right: 5, - bottom: 1, - }, - boxRenderer: BaseTheme.renderTag.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 2, - 'rx': 3, - 'ry': 3, - }), - labelAttrs: { - 'font-family': FONT, - 'font-weight': 'bold', - 'font-size': 9, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'left', - }, - }, - label: { - minHeight: 5, - padding: { - top: 2, - left: 5, - right: 3, - bottom: 1, - }, - labelAttrs: { - 'font-family': FONT, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'left', - }, - }, - }; - - const BLOCKS = { - 'ref': { - margin: { - top: 0, - bottom: 0, - }, - boxRenderer: BaseTheme.renderRef.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 4, - 'rx': 5, - 'ry': 5, - }), - section: SHARED_BLOCK_SECTION, - }, - '': { - margin: { - top: 0, - bottom: 0, - }, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 4, - 'rx': 5, - 'ry': 5, - }), - collapsedBoxRenderer: BaseTheme.renderRef.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 4, - 'rx': 5, - 'ry': 5, - }), - section: SHARED_BLOCK_SECTION, - sepRenderer: SVGShapes.renderLine.bind(null, { - 'stroke': '#000000', - 'stroke-width': 2, - 'stroke-dasharray': '5, 3', - }), - }, - }; - const NOTE_ATTRS = { 'font-family': FONT, 'font-size': 8, 'line-height': LINE_HEIGHT, }; - const NOTES = { - 'text': { - margin: {top: 0, left: 2, right: 2, bottom: 0}, - padding: {top: 2, left: 2, right: 2, bottom: 2}, - overlap: {left: 10, right: 10}, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': '#FFFFFF', - }), - labelAttrs: NOTE_ATTRS, - }, - 'note': { - margin: {top: 0, left: 5, right: 5, bottom: 0}, - padding: {top: 3, left: 3, right: 10, bottom: 3}, - overlap: {left: 10, right: 10}, - boxRenderer: SVGShapes.renderNote.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 2, - 'stroke-linejoin': 'round', - }, { - 'fill': 'none', - 'stroke': '#000000', - 'stroke-width': 1, - }), - labelAttrs: NOTE_ATTRS, - }, - 'state': { - margin: {top: 0, left: 5, right: 5, bottom: 0}, - padding: {top: 5, left: 7, right: 7, bottom: 5}, - overlap: {left: 10, right: 10}, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': '#FFFFFF', - 'stroke': '#000000', - 'stroke-width': 3, - 'rx': 10, - 'ry': 10, - }), - labelAttrs: NOTE_ATTRS, - }, - }; - const DIVIDER_LABEL_ATTRS = { 'font-family': FONT, 'font-size': 8, @@ -8609,65 +8435,445 @@ define('sequence/themes/Chunky',[ 'text-anchor': 'middle', }; - const DIVIDERS = { - '': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 0, - margin: 0, - render: () => ({}), - }, - 'line': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 10, - margin: 0, - render: BaseTheme.renderLineDivider.bind(null, { - lineAttrs: { - 'stroke': '#000000', - 'stroke-width': 2, - 'stroke-linecap': 'round', + class ChunkyTheme extends BaseTheme { + constructor(svg) { + super(svg); + + const sharedBlockSection = { + padding: { + top: 3, + bottom: 4, }, - }), - }, - 'delay': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 0, - margin: 0, - render: BaseTheme.renderDelayDivider.bind(null, { - dotSize: 3, - gapSize: 3, - }), - }, - 'tear': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 10, - margin: 10, - render: BaseTheme.renderTearDivider.bind(null, { - fadeBegin: 5, - fadeSize: 10, - zigWidth: 6, - zigHeight: 1, - lineAttrs: { - 'stroke': '#000000', - 'stroke-width': 2, - 'stroke-linejoin': 'round', + tag: { + padding: { + top: 2, + left: 5, + right: 5, + bottom: 1, + }, + boxRenderer: this.renderTag.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 2, + 'rx': 3, + 'ry': 3, + }), + labelAttrs: { + 'font-family': FONT, + 'font-weight': 'bold', + 'font-size': 9, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'left', + }, }, - }), - }, + label: { + minHeight: 5, + padding: { + top: 2, + left: 5, + right: 3, + bottom: 1, + }, + labelAttrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'left', + }, + }, + }; + + Object.assign(this, { + titleMargin: 12, + outerMargin: 5, + agentMargin: 8, + actionMargin: 5, + minActionMargin: 5, + agentLineHighlightRadius: 4, + + agentCap: { + box: { + padding: { + top: 1, + left: 3, + right: 3, + bottom: 1, + }, + arrowBottom: 2 + 14 * 1.3 / 2, + boxAttrs: { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 3, + 'rx': 4, + 'ry': 4, + }, + labelAttrs: { + 'font-family': FONT, + 'font-weight': 'bold', + 'font-size': 14, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + }, + database: { + padding: { + top: 4, + left: 3, + right: 3, + bottom: 0, + }, + arrowBottom: 2 + 14 * 1.3 / 2, + boxRenderer: this.renderDB.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 3, + 'db-z': 2, + }), + labelAttrs: { + 'font-family': FONT, + 'font-weight': 'bold', + 'font-size': 14, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + }, + cross: { + size: 20, + render: svg.crossFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-linecap': 'round', + }), + }, + bar: { + height: 4, + render: svg.boxFactory({ + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 3, + 'rx': 2, + 'ry': 2, + }), + }, + fade: { + width: 5, + height: 10, + extend: 1, + }, + none: { + height: 10, + }, + }, + + connect: { + loopbackRadius: 8, + line: { + 'solid': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + }, + renderFlat: this.renderFlatConnect.bind(this, null), + renderRev: this.renderRevConnect.bind(this, null), + }, + 'dash': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-dasharray': '10, 4', + }, + renderFlat: this.renderFlatConnect.bind(this, null), + renderRev: this.renderRevConnect.bind(this, null), + }, + 'wave': { + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round', + }, + renderFlat: this.renderFlatConnect.bind(this, WAVE), + renderRev: this.renderRevConnect.bind(this, WAVE), + }, + }, + arrow: { + 'single': { + width: 10, + height: 12, + render: this.renderArrowHead.bind(this), + attrs: { + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-linejoin': 'round', + }, + }, + 'double': { + width: 10, + height: 12, + render: this.renderArrowHead.bind(this), + attrs: { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round', + }, + }, + 'cross': { + short: 10, + radius: 5, + render: svg.crossFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round', + }), + }, + }, + label: { + padding: 7, + margin: {top: 2, bottom: 3}, + attrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + loopbackAttrs: { + 'font-family': FONT, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + }, + }, + source: { + radius: 5, + render: svg.circleFactory({ + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 3, + }), + }, + mask: { + padding: { + top: 1, + left: 5, + right: 5, + bottom: 3, + }, + }, + }, + + titleAttrs: { + 'font-family': FONT, + 'font-weight': 'bolder', + 'font-size': 20, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + 'class': 'title', + }, + + agentLineAttrs: { + '': { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 3, + }, + 'red': { + 'stroke': '#DD0000', + }, + }, + blocks: { + 'ref': { + margin: { + top: 0, + bottom: 0, + }, + boxRenderer: this.renderRef.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 4, + 'rx': 5, + 'ry': 5, + }), + section: sharedBlockSection, + }, + '': { + margin: { + top: 0, + bottom: 0, + }, + boxRenderer: svg.boxFactory({ + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 4, + 'rx': 5, + 'ry': 5, + }), + collapsedBoxRenderer: this.renderRef.bind(this, { + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 4, + 'rx': 5, + 'ry': 5, + }), + section: sharedBlockSection, + sepRenderer: svg.lineFactory({ + 'stroke': '#000000', + 'stroke-width': 2, + 'stroke-dasharray': '5, 3', + }), + }, + }, + notes: { + 'text': { + margin: {top: 0, left: 2, right: 2, bottom: 0}, + padding: {top: 2, left: 2, right: 2, bottom: 2}, + overlap: {left: 10, right: 10}, + boxRenderer: svg.boxFactory({ + 'fill': '#FFFFFF', + }), + labelAttrs: NOTE_ATTRS, + }, + 'note': { + margin: {top: 0, left: 5, right: 5, bottom: 0}, + padding: {top: 3, left: 3, right: 10, bottom: 3}, + overlap: {left: 10, right: 10}, + boxRenderer: svg.noteFactory({ + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 2, + 'stroke-linejoin': 'round', + }, { + 'fill': 'none', + 'stroke': '#000000', + 'stroke-width': 1, + }), + labelAttrs: NOTE_ATTRS, + }, + 'state': { + margin: {top: 0, left: 5, right: 5, bottom: 0}, + padding: {top: 5, left: 7, right: 7, bottom: 5}, + overlap: {left: 10, right: 10}, + boxRenderer: svg.boxFactory({ + 'fill': '#FFFFFF', + 'stroke': '#000000', + 'stroke-width': 3, + 'rx': 10, + 'ry': 10, + }), + labelAttrs: NOTE_ATTRS, + }, + }, + dividers: { + '': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 0, + margin: 0, + render: () => ({}), + }, + 'line': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 10, + margin: 0, + render: this.renderLineDivider.bind(this, { + lineAttrs: { + 'stroke': '#000000', + 'stroke-width': 2, + 'stroke-linecap': 'round', + }, + }), + }, + 'delay': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 0, + margin: 0, + render: this.renderDelayDivider.bind(this, { + dotSize: 3, + gapSize: 3, + }), + }, + 'tear': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 10, + margin: 10, + render: this.renderTearDivider.bind(this, { + fadeBegin: 5, + fadeSize: 10, + zigWidth: 6, + zigHeight: 1, + lineAttrs: { + 'stroke': '#000000', + 'stroke-width': 2, + 'stroke-linejoin': 'round', + }, + }), + }, + }, + }); + } + } + + ChunkyTheme.Factory = class { + constructor() { + this.name = 'chunky'; + } + + build(svg) { + return new ChunkyTheme(svg); + } }; - return class ChunkyTheme extends BaseTheme { + return ChunkyTheme; +}); + +define('core/Random',[],() => { + 'use strict'; + + return class Random { + // xorshift+ 64-bit random generator + // https://en.wikipedia.org/wiki/Xorshift + constructor() { - super({ - name: 'chunky', - settings: SETTINGS, - blocks: BLOCKS, - notes: NOTES, - dividers: DIVIDERS, - }); + this.s = new Uint32Array(4); + } + + reset() { + // Arbitrary random seed with roughly balanced 1s / 0s + // (taken from running Math.random a few times) + this.s[0] = 0x177E9C74; + this.s[1] = 0xAE6FFDCE; + this.s[2] = 0x3CF4F32B; + this.s[3] = 0x46449F88; + } + + nextFloat() { + /* jshint -W016 */ // bit-operations are part of the algorithm + const range = 0x100000000; + let x0 = this.s[0]; + let x1 = this.s[1]; + const y0 = this.s[2]; + const y1 = this.s[3]; + this.s[0] = y0; + this.s[1] = y1; + x0 ^= (x0 << 23) | (x1 >>> 9); + x1 ^= (x1 << 23); + this.s[2] = x0 ^ y0 ^ (x0 >>> 17) ^ (y0 >>> 26); + this.s[3] = ( + x1 ^ y1 ^ + (x0 << 15 | x1 >>> 17) ^ + (y0 << 6 | y1 >>> 26) + ); + return (((this.s[3] + y1) >>> 0) % range) / range; } }; }); @@ -8679,6 +8885,10 @@ define('sequence/themes/HandleeFontData',[],() => { // Downloaded from Google Fonts and converted to Base64 for embedding in // generated SVGs // https://fonts.google.com/specimen/Handlee + // base64 -b64 \ + // < *.woff2 \ + // | sed -e "s/^/"$'\t'$'\t'$'\t'"'/" -e "s/$/' +/" \ + // > handlee.woff2.b64 /* License @@ -9108,14 +9318,12 @@ define('sequence/themes/HandleeFontData',[],() => { }); define('sequence/themes/Sketch',[ + 'core/Random', './BaseTheme', - 'svg/SVGUtilities', - 'svg/SVGShapes', './HandleeFontData', ], ( + Random, BaseTheme, - svg, - SVGShapes, Handlee ) => { 'use strict'; @@ -9140,266 +9348,12 @@ define('sequence/themes/Sketch',[ }, }; - const SETTINGS = { - titleMargin: 10, - outerMargin: 5, - agentMargin: 10, - actionMargin: 10, - minActionMargin: 3, - agentLineHighlightRadius: 4, - - agentCap: { - box: { - padding: { - top: 5, - left: 10, - right: 10, - bottom: 5, - }, - arrowBottom: 5 + 12 * 1.3 / 2, - labelAttrs: { - 'font-family': FONT_FAMILY, - 'font-size': 12, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - boxRenderer: null, - }, - database: { - padding: { - top: 12, - left: 10, - right: 10, - bottom: 2, - }, - arrowBottom: 5 + 12 * 1.3 / 2, - boxRenderer: BaseTheme.renderDB.bind(null, Object.assign({ - 'fill': '#FFFFFF', - 'db-z': 5, - }, PENCIL.normal)), - labelAttrs: { - 'font-family': FONT, - 'font-size': 12, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - }, - cross: { - size: 15, - render: null, - }, - bar: { - height: 6, - render: null, - }, - fade: { - width: Math.ceil(MAX_CHAOS * 2 + 2), - height: 6, - extend: Math.ceil(MAX_CHAOS * 0.3 + 1), - }, - none: { - height: 10, - }, - }, - - connect: { - loopbackRadius: 6, - line: { - 'solid': { - attrs: Object.assign({ - 'fill': 'none', - }, PENCIL.normal), - renderFlat: null, - renderRev: null, - }, - 'dash': { - attrs: Object.assign({ - 'fill': 'none', - 'stroke-dasharray': '4, 2', - }, PENCIL.normal), - renderFlat: null, - renderRev: null, - }, - 'wave': { - attrs: Object.assign({ - 'fill': 'none', - 'stroke-linejoin': 'round', - 'stroke-linecap': 'round', - }, PENCIL.normal), - renderFlat: null, - renderRev: null, - }, - }, - arrow: { - 'single': { - width: 5, - height: 6, - attrs: Object.assign({ - 'fill': 'rgba(0,0,0,0.9)', - }, PENCIL.normal), - render: null, - }, - 'double': { - width: 4, - height: 8, - attrs: Object.assign({ - 'fill': 'none', - }, PENCIL.normal), - render: null, - }, - 'cross': { - short: 5, - radius: 3, - render: null, - }, - }, - label: { - padding: 6, - margin: {top: 2, bottom: 1}, - attrs: { - 'font-family': FONT_FAMILY, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - }, - loopbackAttrs: { - 'font-family': FONT_FAMILY, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - }, - }, - source: { - radius: 1, - render: ({x, y, radius}) => { - return svg.make('circle', { - 'cx': x, - 'cy': y, - 'r': radius, - 'fill': '#000000', - 'stroke': '#000000', - 'stroke-width': 1, - }); - }, - }, - mask: { - padding: { - top: 0, - left: 3, - right: 3, - bottom: 1, - }, - }, - }, - - titleAttrs: { - 'font-family': FONT_FAMILY, - 'font-size': 20, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'middle', - 'class': 'title', - }, - - agentLineAttrs: { - '': Object.assign({ - 'fill': 'none', - }, PENCIL.normal), - 'red': { - 'stroke': 'rgba(200,40,0,0.8)', - }, - }, - }; - - const SHARED_BLOCK_SECTION = { - padding: { - top: 3, - bottom: 2, - }, - tag: { - padding: { - top: 2, - left: 3, - right: 5, - bottom: 0, - }, - boxRenderer: null, - labelAttrs: { - 'font-family': FONT_FAMILY, - 'font-weight': 'bold', - 'font-size': 9, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'left', - }, - }, - label: { - minHeight: 6, - padding: { - top: 2, - left: 5, - right: 3, - bottom: 1, - }, - labelAttrs: { - 'font-family': FONT_FAMILY, - 'font-size': 8, - 'line-height': LINE_HEIGHT, - 'text-anchor': 'left', - }, - }, - }; - - const BLOCKS = { - 'ref': { - margin: { - top: 0, - bottom: 0, - }, - boxRenderer: null, - section: SHARED_BLOCK_SECTION, - }, - '': { - margin: { - top: 0, - bottom: 0, - }, - boxRenderer: null, - collapsedBoxRenderer: null, - section: SHARED_BLOCK_SECTION, - sepRenderer: null, - }, - }; - const NOTE_ATTRS = { 'font-family': FONT_FAMILY, 'font-size': 8, 'line-height': LINE_HEIGHT, }; - const NOTES = { - 'text': { - margin: {top: 0, left: 6, right: 6, bottom: 0}, - padding: {top: 2, left: 2, right: 2, bottom: 2}, - overlap: {left: 10, right: 10}, - boxRenderer: SVGShapes.renderBox.bind(null, { - 'fill': '#FFFFFF', - }), - labelAttrs: NOTE_ATTRS, - }, - 'note': { - margin: {top: 0, left: 5, right: 5, bottom: 0}, - padding: {top: 5, left: 5, right: 10, bottom: 5}, - overlap: {left: 10, right: 10}, - boxRenderer: null, - labelAttrs: NOTE_ATTRS, - }, - 'state': { - margin: {top: 0, left: 5, right: 5, bottom: 0}, - padding: {top: 7, left: 7, right: 7, bottom: 7}, - overlap: {left: 10, right: 10}, - boxRenderer: null, - labelAttrs: NOTE_ATTRS, - }, - }; - const DIVIDER_LABEL_ATTRS = { 'font-family': FONT_FAMILY, 'font-size': 8, @@ -9407,78 +9361,6 @@ define('sequence/themes/Sketch',[ 'text-anchor': 'middle', }; - const DIVIDERS = { - '': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 0, - margin: 0, - render: () => ({}), - }, - 'line': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 10, - margin: 0, - render: null, - }, - 'delay': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 0, - margin: 0, - render: BaseTheme.renderDelayDivider.bind(null, { - dotSize: 1, - gapSize: 2, - }), - }, - 'tear': { - labelAttrs: DIVIDER_LABEL_ATTRS, - padding: {top: 2, left: 5, right: 5, bottom: 2}, - extend: 10, - margin: 10, - render: null, - }, - }; - - class Random { - // xorshift+ 64-bit random generator - // https://en.wikipedia.org/wiki/Xorshift - - constructor() { - this.s = new Uint32Array(4); - } - - reset() { - // Arbitrary random seed with roughly balanced 1s / 0s - // (taken from running Math.random a few times) - this.s[0] = 0x177E9C74; - this.s[1] = 0xAE6FFDCE; - this.s[2] = 0x3CF4F32B; - this.s[3] = 0x46449F88; - } - - nextFloat() { - /* jshint -W016 */ // bit-operations are part of the algorithm - const range = 0x100000000; - let x0 = this.s[0]; - let x1 = this.s[1]; - const y0 = this.s[2]; - const y1 = this.s[3]; - this.s[0] = y0; - this.s[1] = y1; - x0 ^= (x0 << 23) | (x1 >>> 9); - x1 ^= (x1 << 23); - this.s[2] = x0 ^ y0 ^ (x0 >>> 17) ^ (y0 >>> 26); - this.s[3] = ( - x1 ^ y1 ^ - (x0 << 15 | x1 >>> 17) ^ - (y0 << 6 | y1 >>> 26) - ); - return (((this.s[3] + y1) >>> 0) % range) / range; - } - } - const RIGHT = {}; const LEFT = {}; @@ -9508,84 +9390,296 @@ define('sequence/themes/Sketch',[ } class SketchTheme extends BaseTheme { - constructor(handedness = RIGHT) { - super({ - name: '', - settings: SETTINGS, - blocks: BLOCKS, - notes: NOTES, - dividers: DIVIDERS, - }); + constructor(svg, handedness = RIGHT) { + super(svg); - if(handedness === RIGHT) { - this.name = 'sketch'; - this.handedness = 1; - } else { - this.name = 'sketch left handed'; - this.handedness = -1; - } + this.handedness = (handedness === RIGHT) ? 1 : -1; this.random = new Random(); this.wave = new SketchWavePattern(4, handedness); - this._assignCapFunctions(); - this._assignConnectFunctions(); - this._assignNoteFunctions(); - this._assignBlockFunctions(); - this._assignDividerFunctions(); - } + const sharedBlockSection = { + padding: { + top: 3, + bottom: 2, + }, + tag: { + padding: { + top: 2, + left: 3, + right: 5, + bottom: 0, + }, + boxRenderer: this.renderTag.bind(this), + labelAttrs: { + 'font-family': FONT_FAMILY, + 'font-weight': 'bold', + 'font-size': 9, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'left', + }, + }, + label: { + minHeight: 6, + padding: { + top: 2, + left: 5, + right: 3, + bottom: 1, + }, + labelAttrs: { + 'font-family': FONT_FAMILY, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'left', + }, + }, + }; - _assignCapFunctions() { - this.renderBar = this.renderBar.bind(this); - this.renderBox = this.renderBox.bind(this); + Object.assign(this, { + titleMargin: 10, + outerMargin: 5, + agentMargin: 10, + actionMargin: 10, + minActionMargin: 3, + agentLineHighlightRadius: 4, - this.agentCap.cross.render = this.renderCross.bind(this); - this.agentCap.bar.render = this.renderBar; - this.agentCap.box.boxRenderer = this.renderBox; - } + agentCap: { + box: { + padding: { + top: 5, + left: 10, + right: 10, + bottom: 5, + }, + arrowBottom: 5 + 12 * 1.3 / 2, + labelAttrs: { + 'font-family': FONT_FAMILY, + 'font-size': 12, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + boxRenderer: this.renderBox.bind(this), + }, + database: { + padding: { + top: 12, + left: 10, + right: 10, + bottom: 2, + }, + arrowBottom: 5 + 12 * 1.3 / 2, + boxRenderer: this.renderDB.bind(this, Object.assign({ + 'fill': '#FFFFFF', + 'db-z': 5, + }, PENCIL.normal)), + labelAttrs: { + 'font-family': FONT, + 'font-size': 12, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + }, + cross: { + size: 15, + render: this.renderCross.bind(this), + }, + bar: { + height: 6, + render: this.renderBar.bind(this), + }, + fade: { + width: Math.ceil(MAX_CHAOS * 2 + 2), + height: 6, + extend: Math.ceil(MAX_CHAOS * 0.3 + 1), + }, + none: { + height: 10, + }, + }, - _assignConnectFunctions() { - this.renderArrowHead = this.renderArrowHead.bind(this); - this.renderFlatConnector = this.renderFlatConnector.bind(this); - this.renderRevConnector = this.renderRevConnector.bind(this); + connect: { + loopbackRadius: 6, + line: { + 'solid': { + attrs: Object.assign({ + 'fill': 'none', + }, PENCIL.normal), + renderFlat: this.renderFlatConnect.bind(this), + renderRev: this.renderRevConnect.bind(this), + }, + 'dash': { + attrs: Object.assign({ + 'fill': 'none', + 'stroke-dasharray': '4, 2', + }, PENCIL.normal), + renderFlat: this.renderFlatConnect.bind(this), + renderRev: this.renderRevConnect.bind(this), + }, + 'wave': { + attrs: Object.assign({ + 'fill': 'none', + 'stroke-linejoin': 'round', + 'stroke-linecap': 'round', + }, PENCIL.normal), + renderFlat: this.renderFlatConnectWave.bind(this), + renderRev: this.renderRevConnectWave.bind(this), + }, + }, + arrow: { + 'single': { + width: 5, + height: 6, + attrs: Object.assign({ + 'fill': 'rgba(0,0,0,0.9)', + }, PENCIL.normal), + render: this.renderArrowHead.bind(this), + }, + 'double': { + width: 4, + height: 8, + attrs: Object.assign({ + 'fill': 'none', + }, PENCIL.normal), + render: this.renderArrowHead.bind(this), + }, + 'cross': { + short: 5, + radius: 3, + render: this.renderCross.bind(this), + }, + }, + label: { + padding: 6, + margin: {top: 2, bottom: 1}, + attrs: { + 'font-family': FONT_FAMILY, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + }, + loopbackAttrs: { + 'font-family': FONT_FAMILY, + 'font-size': 8, + 'line-height': LINE_HEIGHT, + }, + }, + source: { + radius: 1, + render: svg.circleFactory({ + 'fill': '#000000', + 'stroke': '#000000', + 'stroke-width': 1, + }), + }, + mask: { + padding: { + top: 0, + left: 3, + right: 3, + bottom: 1, + }, + }, + }, - this.connect.arrow.single.render = this.renderArrowHead; - this.connect.arrow.double.render = this.renderArrowHead; - this.connect.arrow.cross.render = this.renderCross.bind(this); + titleAttrs: { + 'font-family': FONT_FAMILY, + 'font-size': 20, + 'line-height': LINE_HEIGHT, + 'text-anchor': 'middle', + 'class': 'title', + }, - this.connect.line.solid.renderFlat = this.renderFlatConnector; - this.connect.line.solid.renderRev = this.renderRevConnector; - this.connect.line.dash.renderFlat = this.renderFlatConnector; - this.connect.line.dash.renderRev = this.renderRevConnector; - this.connect.line.wave.renderFlat = - this.renderFlatConnectorWave.bind(this); - this.connect.line.wave.renderRev = - this.renderRevConnectorWave.bind(this); - } - - _assignNoteFunctions() { - this.notes.note.boxRenderer = this.renderNote.bind(this); - this.notes.state.boxRenderer = this.renderState.bind(this); - } - - _assignBlockFunctions() { - this.renderTag = this.renderTag.bind(this); - - this.blocks.ref.boxRenderer = this.renderRefBlock.bind(this); - this.blocks[''].boxRenderer = this.renderBlock.bind(this); - this.blocks[''].collapsedBoxRenderer = - this.renderCollapsedBlock.bind(this); - this.blocks.ref.section.tag.boxRenderer = this.renderTag; - this.blocks[''].section.tag.boxRenderer = this.renderTag; - this.blocks[''].sepRenderer = this.renderSeparator.bind(this); - } - - _assignDividerFunctions() { - this.dividers.line.render = this.renderLineDivider.bind(this); - this.dividers.tear.render = BaseTheme.renderTearDivider.bind(null, { - fadeBegin: 5, - fadeSize: 10, - pattern: this.wave, - lineAttrs: PENCIL.normal, + agentLineAttrs: { + '': Object.assign({ + 'fill': 'none', + }, PENCIL.normal), + 'red': { + 'stroke': 'rgba(200,40,0,0.8)', + }, + }, + blocks: { + 'ref': { + margin: { + top: 0, + bottom: 0, + }, + boxRenderer: this.renderRefBlock.bind(this), + section: sharedBlockSection, + }, + '': { + margin: { + top: 0, + bottom: 0, + }, + boxRenderer: this.renderBlock.bind(this), + collapsedBoxRenderer: this.renderMinBlock.bind(this), + section: sharedBlockSection, + sepRenderer: this.renderSeparator.bind(this), + }, + }, + notes: { + 'text': { + margin: {top: 0, left: 6, right: 6, bottom: 0}, + padding: {top: 2, left: 2, right: 2, bottom: 2}, + overlap: {left: 10, right: 10}, + boxRenderer: svg.boxFactory({ + 'fill': '#FFFFFF', + }), + labelAttrs: NOTE_ATTRS, + }, + 'note': { + margin: {top: 0, left: 5, right: 5, bottom: 0}, + padding: {top: 5, left: 5, right: 10, bottom: 5}, + overlap: {left: 10, right: 10}, + boxRenderer: this.renderNote.bind(this), + labelAttrs: NOTE_ATTRS, + }, + 'state': { + margin: {top: 0, left: 5, right: 5, bottom: 0}, + padding: {top: 7, left: 7, right: 7, bottom: 7}, + overlap: {left: 10, right: 10}, + boxRenderer: this.renderState.bind(this), + labelAttrs: NOTE_ATTRS, + }, + }, + dividers: { + '': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 0, + margin: 0, + render: () => ({}), + }, + 'line': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 10, + margin: 0, + render: this.renderLineDivider.bind(this), + }, + 'delay': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 0, + margin: 0, + render: this.renderDelayDivider.bind(this, { + dotSize: 1, + gapSize: 2, + }), + }, + 'tear': { + labelAttrs: DIVIDER_LABEL_ATTRS, + padding: {top: 2, left: 5, right: 5, bottom: 2}, + extend: 10, + margin: 10, + render: this.renderTearDivider.bind(this, { + fadeBegin: 5, + fadeSize: 10, + pattern: this.wave, + lineAttrs: PENCIL.normal, + }), + }, + }, }); } @@ -9595,13 +9689,13 @@ define('sequence/themes/Sketch',[ addDefs(builder) { builder('sketch_font', () => { - const style = document.createElement('style'); + const style = this.svg.el('style', null); // For some uses, it is fine to load this font externally, // but this fails when exporting as SVG / PNG (svg tags must // have no external dependencies). // const url = 'https://fonts.googleapis.com/css?family=' + FONT; -// style.textContent = '@import url("' + url + '")'; - style.textContent = ( +// style.text('@import url("' + url + '")'); + style.text( '@font-face{' + 'font-family:"' + Handlee.name + '";' + 'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' + @@ -9673,14 +9767,15 @@ define('sequence/themes/Sketch',[ renderLine(p1, p2, lineOptions) { const line = this.lineNodes(p1, p2, lineOptions); - const shape = svg.make('path', Object.assign({ - 'd': line.nodes, - 'fill': 'none', - 'stroke-dasharray': lineOptions.dash ? '6, 5' : 'none', - }, lineOptions.attrs || ( - lineOptions.thick ? PENCIL.thick : PENCIL.normal - ))); - return shape; + return this.svg.el('path') + .attrs({ + 'd': line.nodes, + 'fill': 'none', + 'stroke-dasharray': lineOptions.dash ? '6, 5' : 'none', + }) + .attrs(lineOptions.attrs || ( + lineOptions.thick ? PENCIL.thick : PENCIL.normal + )); } boxNodes({x, y, width, height}) { @@ -9709,10 +9804,12 @@ define('sequence/themes/Sketch',[ } renderBox(position, {fill = null, thick = false, attrs = null} = {}) { - return svg.make('path', Object.assign({ - 'd': this.boxNodes(position), - 'fill': fill || '#FFFFFF', - }, attrs || (thick ? PENCIL.thick : PENCIL.normal))); + return this.svg.el('path') + .attrs({ + 'd': this.boxNodes(position), + 'fill': fill || '#FFFFFF', + }) + .attrs(attrs || (thick ? PENCIL.thick : PENCIL.normal)); } renderNote({x, y, width, height}) { @@ -9753,29 +9850,33 @@ define('sequence/themes/Sketch',[ {var1: 0, move: false} ); - return svg.make('g', {}, [ - svg.make('path', Object.assign({ - 'd': ( - lT.nodes + - lF.nodes + - lR.nodes + - lB.nodes + - lL.nodes - ), - 'fill': '#FFFFFF', - }, PENCIL.normal)), - svg.make('path', Object.assign({ - 'd': lF1.nodes + lF2.nodes, - 'fill': 'none', - }, PENCIL.normal)), - ]); + return this.svg.el('g').add( + this.svg.el('path') + .attrs({ + 'd': ( + lT.nodes + + lF.nodes + + lR.nodes + + lB.nodes + + lL.nodes + ), + 'fill': '#FFFFFF', + }) + .attrs(PENCIL.normal), + this.svg.el('path') + .attrs({ + 'd': lF1.nodes + lF2.nodes, + 'fill': 'none', + }) + .attrs(PENCIL.normal) + ); } renderLineDivider({x, y, labelWidth, width, height}) { let shape = null; const yPos = y + height / 2; if(labelWidth > 0) { - shape = svg.make('g', {}, [ + shape = this.svg.el('g').add( this.renderLine( {x, y: yPos}, {x: x + (width - labelWidth) / 2, y: yPos}, @@ -9785,8 +9886,8 @@ define('sequence/themes/Sketch',[ {x: x + (width + labelWidth) / 2, y: yPos}, {x: x + width, y: yPos}, {} - ), - ]); + ) + ); } else { shape = this.renderLine( {x, y: yPos}, @@ -9797,20 +9898,20 @@ define('sequence/themes/Sketch',[ return {shape}; } - renderFlatConnector(attrs, {x1, y1, x2, y2}) { + renderFlatConnect(attrs, {x1, y1, x2, y2}) { const ln = this.lineNodes( {x: x1, y: y1}, {x: x2, y: y2}, {varX: 0.3} ); return { - shape: svg.make('path', Object.assign({'d': ln.nodes}, attrs)), + shape: this.svg.el('path').attr('d', ln.nodes).attrs(attrs), p1: ln.p1, p2: ln.p2, }; } - renderRevConnector(attrs, {x1, y1, x2, y2, xR}) { + renderRevConnect(attrs, {x1, y1, x2, y2, xR}) { const variance = Math.min((xR - x1) * 0.06, 3); const overshoot = Math.min((xR - x1) * 0.5, 6); const p1x = x1 + this.vary(variance, -1); @@ -9825,54 +9926,56 @@ define('sequence/themes/Sketch',[ const p3y = y2 + this.vary(variance, -1); return { - shape: svg.make('path', Object.assign({ - d: ( + shape: this.svg.el('path') + .attr('d', ( 'M' + p1x + ' ' + p1y + 'C' + p1x + ' ' + p1y + ',' + b1x + ' ' + b1y + ',' + p2x + ' ' + p2y + 'S' + b2x + ' ' + b2y + ',' + p3x + ' ' + p3y - ), - }, attrs)), + )) + .attrs(attrs), p1: {x: p1x, y: p1y}, p2: {x: p3x, y: p3y}, }; } - renderFlatConnectorWave(attrs, {x1, y1, x2, y2}) { + renderFlatConnectWave(attrs, {x1, y1, x2, y2}) { const x1v = x1 + this.vary(0.3); const x2v = x2 + this.vary(0.3); const y1v = y1 + this.vary(1); const y2v = y2 + this.vary(1); return { - shape: svg.make('path', Object.assign({ - d: new SVGShapes.PatternedLine(this.wave) + shape: this.svg.el('path') + .attr('d', this.svg.patternedLine(this.wave) .move(x1v, y1v) .line(x2v, y2v) .cap() - .asPath(), - }, attrs)), + .asPath() + ) + .attrs(attrs), p1: {x: x1v, y: y1v}, p2: {x: x2v, y: y2v}, }; } - renderRevConnectorWave(attrs, {x1, y1, x2, y2, xR}) { + renderRevConnectWave(attrs, {x1, y1, x2, y2, xR}) { const x1v = x1 + this.vary(0.3); const x2v = x2 + this.vary(0.3); const y1v = y1 + this.vary(1); const y2v = y2 + this.vary(1); return { - shape: svg.make('path', Object.assign({ - d: new SVGShapes.PatternedLine(this.wave) + shape: this.svg.el('path') + .attr('d', this.svg.patternedLine(this.wave) .move(x1v, y1v) .line(xR, y1) .arc(xR, (y1 + y2) / 2, Math.PI) .line(x2v, y2v) .cap() - .asPath(), - }, attrs)), + .asPath() + ) + .attrs(attrs), p1: {x: x1v, y: y1v}, p2: {x: x2v, y: y2v}, }; @@ -9900,9 +10003,9 @@ define('sequence/themes/Sketch',[ l1.p1, {var1: 0, var2: 0, move: false} ); - return svg.make('path', Object.assign({ - 'd': l1.nodes + l2.nodes + l3.nodes, - }, attrs)); + return this.svg.el('path') + .attr('d', l1.nodes + l2.nodes + l3.nodes) + .attrs(attrs); } renderState({x, y, width, height}) { @@ -9925,8 +10028,8 @@ define('sequence/themes/Sketch',[ const bentL = x - Math.min(height * 0.01, 2) * this.handedness; const bentR = bentL + width; - return svg.make('path', Object.assign({ - 'd': ( + return this.svg.el('path') + .attr('d', ( 'M' + tlX + ' ' + tlY + 'C' + (tlX + cx) + ' ' + (tlY - cy) + ',' + (mX - width * this.vary(0.03, 0.3)) + ' ' + bentT + @@ -9946,26 +10049,21 @@ define('sequence/themes/Sketch',[ 'S' + (tlX - cx) + ' ' + (tlY + cy) + ',' + tlX + ' ' + tlY + 'Z' - ), - 'fill': '#FFFFFF', - }, PENCIL.normal)); + )) + .attr('fill', '#FFFFFF') + .attrs(PENCIL.normal); } renderRefBlock(position) { const nodes = this.boxNodes(position); return { - shape: svg.make('path', Object.assign({ - 'd': nodes, - 'fill': 'none', - }, PENCIL.thick)), - mask: svg.make('path', { - 'd': nodes, - 'fill': '#000000', - }), - fill: svg.make('path', { - 'd': nodes, - 'fill': '#FFFFFF', - }), + shape: this.svg.el('path') + .attrs({'d': nodes, 'fill': 'none'}) + .attrs(PENCIL.thick), + mask: this.svg.el('path') + .attrs({'d': nodes, 'fill': '#000000'}), + fill: this.svg.el('path') + .attrs({'d': nodes, 'fill': '#FFFFFF'}), }; } @@ -9973,7 +10071,7 @@ define('sequence/themes/Sketch',[ return this.renderBox(position, {fill: 'none', thick: true}); } - renderCollapsedBlock(position) { + renderMinBlock(position) { return this.renderRefBlock(position); } @@ -9994,16 +10092,19 @@ define('sequence/themes/Sketch',[ const line = l1.nodes + l2.nodes; - return svg.make('g', {}, [ - svg.make('path', { - 'd': line + 'L' + x + ' ' + y, - 'fill': '#FFFFFF', - }), - svg.make('path', Object.assign({ - 'd': line, - 'fill': '#FFFFFF', - }, PENCIL.normal)), - ]); + return this.svg.el('g').add( + this.svg.el('path') + .attrs({ + 'd': line + 'L' + x + ' ' + y, + 'fill': '#FFFFFF', + }), + this.svg.el('path') + .attrs({ + 'd': line, + 'fill': '#FFFFFF', + }) + .attrs(PENCIL.normal) + ); } renderSeparator({x1, y1, x2, y2}) { @@ -10032,35 +10133,45 @@ define('sequence/themes/Sketch',[ {} ); - return svg.make('path', Object.assign({ - 'd': l1.nodes + l2.nodes, - 'fill': 'none', - }, PENCIL.normal)); + return this.svg.el('path') + .attrs({ + 'd': l1.nodes + l2.nodes, + 'fill': 'none', + }) + .attrs(PENCIL.normal); } renderAgentLine({x, y0, y1, width, className, options}) { const attrs = this.optionsAttributes(this.agentLineAttrs, options); if(width > 0) { - const shape = this.renderBox({ + return this.renderBox({ x: x - width / 2, y: y0, width, height: y1 - y0, - }, {fill: 'none', attrs}); - shape.setAttribute('class', className); - return shape; + }, {fill: 'none', attrs}).setClass(className); } else { - const shape = this.renderLine( + return this.renderLine( {x, y: y0}, {x, y: y1}, {varY: 0.3, attrs} - ); - shape.setAttribute('class', className); - return shape; + ).setClass(className); } } } + SketchTheme.Factory = class { + constructor(handedness = RIGHT) { + const right = (handedness === RIGHT); + this.name = right ? 'sketch' : 'sketch left handed'; + this.handedness = handedness; + } + + build(svg) { + return new SketchTheme(svg, this.handedness); + } + }; + SketchTheme.RIGHT = RIGHT; SketchTheme.LEFT = LEFT; @@ -10097,11 +10208,11 @@ define('sequence/SequenceDiagram',[ 'use strict'; const themes = [ - new BasicTheme(), - new MonospaceTheme(), - new ChunkyTheme(), - new SketchTheme(SketchTheme.RIGHT), - new SketchTheme(SketchTheme.LEFT), + new BasicTheme.Factory(), + new MonospaceTheme.Factory(), + new ChunkyTheme.Factory(), + new SketchTheme.Factory(SketchTheme.RIGHT), + new SketchTheme.Factory(SketchTheme.LEFT), ]; const SharedParser = new Parser(); @@ -10152,6 +10263,14 @@ define('sequence/SequenceDiagram',[ } } + function pickDocument(container) { + if(container) { + return container.ownerDocument; + } else { + return window.document; + } + } + class SequenceDiagram extends EventObject { constructor(code = null, options = {}) { super(); @@ -10161,16 +10280,24 @@ define('sequence/SequenceDiagram',[ code = options.code; } - this.registerCodeMirrorMode = registerCodeMirrorMode; + Object.assign(this, { + code, + latestProcessed: null, + isInteractive: false, + textSizerFactory: options.textSizerFactory || null, + registerCodeMirrorMode, + + parser: SharedParser, + generator: SharedGenerator, + renderer: new Renderer(Object.assign({ + themes, + document: pickDocument(options.container), + }, options)), + exporter: new Exporter(), + }); - this.code = code; - this.parser = SharedParser; - this.generator = SharedGenerator; - this.renderer = new Renderer(Object.assign({themes}, options)); - this.exporter = new Exporter(); this.renderer.addEventForwarding(this); - this.latestProcessed = null; - this.isInteractive = false; + if(options.container) { options.container.appendChild(this.dom()); } @@ -10183,6 +10310,8 @@ define('sequence/SequenceDiagram',[ } clone(options = {}) { + const reference = (options.container || this.renderer.dom()); + return new SequenceDiagram(Object.assign({ code: this.code, container: null, @@ -10190,7 +10319,8 @@ define('sequence/SequenceDiagram',[ namespace: null, components: this.renderer.components, interactive: this.isInteractive, - SVGTextBlockClass: this.renderer.SVGTextBlockClass, + document: reference.ownerDocument, + textSizerFactory: this.textSizerFactory, }, options)); } @@ -10301,9 +10431,9 @@ define('sequence/SequenceDiagram',[ } _revertParent(state) { - const dom = this.renderer.svg(); + const dom = this.renderer.dom(); if(dom.parentNode !== state.originalParent) { - document.body.removeChild(dom); + dom.parentNode.removeChild(dom); if(state.originalParent) { state.originalParent.appendChild(dom); } @@ -10317,7 +10447,7 @@ define('sequence/SequenceDiagram',[ } optimisedRenderPreReflow(processed = null) { - const dom = this.renderer.svg(); + const dom = this.renderer.dom(); this.renderState = { originalParent: dom.parentNode, processed, @@ -10325,11 +10455,11 @@ define('sequence/SequenceDiagram',[ }; const state = this.renderState; - if(!document.body.contains(dom)) { + if(!dom.isConnected) { if(state.originalParent) { state.originalParent.removeChild(dom); } - document.body.appendChild(dom); + dom.ownerDocument.body.appendChild(dom); } try { @@ -10419,7 +10549,7 @@ define('sequence/SequenceDiagram',[ } dom() { - return this.renderer.svg(); + return this.renderer.dom(); } } @@ -10450,8 +10580,6 @@ define('sequence/SequenceDiagram',[ Object.assign(tagOptions, options) ); const newElement = diagram.dom(); - element.parentNode.insertBefore(newElement, element); - element.parentNode.removeChild(element); const attrs = element.attributes; for(let i = 0; i < attrs.length; ++ i) { newElement.setAttribute( @@ -10459,6 +10587,7 @@ define('sequence/SequenceDiagram',[ attrs[i].nodeValue ); } + element.parentNode.replaceChild(newElement, element); return diagram; } diff --git a/lib/sequence-diagram.min.js b/lib/sequence-diagram.min.js index f169b24..6913b1b 100644 --- a/lib/sequence-diagram.min.js +++ b/lib/sequence-diagram.min.js @@ -1 +1 @@ -!function(){var e,t,n;!function(r){function s(e,t){return y.call(e,t)}function i(e,t){var n,r,s,i,a,o,l,h,d,g,c,u=t&&t.split("/"),p=b.map,f=p&&p["*"]||{};if(e){for(a=(e=e.split("/")).length-1,b.nodeIdCompat&&w.test(e[a])&&(e[a]=e[a].replace(w,"")),"."===e[0].charAt(0)&&u&&(e=u.slice(0,u.length-1).concat(e)),d=0;d0&&(e.splice(d-1,2),d-=2)}e=e.join("/")}if((u||f)&&p){for(d=(n=e.split("/")).length;d>0;d-=1){if(r=n.slice(0,d).join("/"),u)for(g=u.length;g>0;g-=1)if((s=p[u.slice(0,g).join("/")])&&(s=s[r])){i=s,o=d;break}if(i)break;!l&&f&&f[r]&&(l=f[r],h=d)}!i&&l&&(i=l,o=h),i&&(n.splice(0,o,i),e=n.join("/"))}return e}function a(e,t){return function(){var n=k.call(arguments,0);return"string"!=typeof n[0]&&1===n.length&&n.push(null),c.apply(r,n.concat([e,t]))}}function o(e){return function(t){f[e]=t}}function l(e){if(s(m,e)){var t=m[e];delete m[e],x[e]=!0,g.apply(r,t)}if(!s(f,e)&&!s(x,e))throw new Error("No "+e);return f[e]}function h(e){var t,n=e?e.indexOf("!"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function d(e){return e?h(e):[]}var g,c,u,p,f={},m={},b={},x={},y=Object.prototype.hasOwnProperty,k=[].slice,w=/\.js$/;u=function(e,t){var n,r=h(e),s=r[0],a=t[1];return e=r[1],s&&(n=l(s=i(s,a))),s?e=n&&n.normalize?n.normalize(e,function(e){return function(t){return i(t,e)}}(a)):i(e,a):(s=(r=h(e=i(e,a)))[0],e=r[1],s&&(n=l(s))),{f:s?s+"!"+e:e,n:e,pr:s,p:n}},p={require:function(e){return a(e)},exports:function(e){var t=f[e];return void 0!==t?t:f[e]={}},module:function(e){return{id:e,uri:"",exports:f[e],config:function(e){return function(){return b&&b.config&&b.config[e]||{}}}(e)}}},g=function(e,t,n,i){var h,g,c,b,y,k,w,v=[],A=typeof n;if(i=i||e,k=d(i),"undefined"===A||"function"===A){for(t=!t.length&&n.length?["require","exports","module"]:t,y=0;y{"use strict";return class{constructor(){this.listeners=new Map,this.forwards=new Set}addEventListener(e,t){const n=this.listeners.get(e);n?n.push(t):this.listeners.set(e,[t])}removeEventListener(e,t){const n=this.listeners.get(e);if(!n)return;const r=n.indexOf(t);-1!==r&&n.splice(r,1)}countEventListeners(e){return(this.listeners.get(e)||[]).length}removeAllEventListeners(e){e?this.listeners.delete(e):this.listeners.clear()}addEventForwarding(e){this.forwards.add(e)}removeEventForwarding(e){this.forwards.delete(e)}removeAllEventForwardings(){this.forwards.clear()}trigger(e,t=[]){(this.listeners.get(e)||[]).forEach(e=>e.apply(null,t)),this.forwards.forEach(n=>n.trigger(e,t))}}}),n("core/ArrayUtilities",[],()=>{"use strict";function e(e,t,n=null){if(null===n)return e.indexOf(t);for(let r=0;r=e.length)return void s.push(r.slice());const i=e[n];if(!Array.isArray(i))return r.push(i),t(e,n+1,r,s),void r.pop();for(let a=0;a{n.push(...t(e))}),n}}}),n("sequence/CodeMirrorMode",["core/ArrayUtilities"],e=>{"use strict";function t(e,t=!1){return{type:"string",suggest:t,then:Object.assign({"":0},e)}}function n(e,t){return e.v===t.v&&e.prefix===t.prefix&&e.suffix===t.suffix&&e.q===t.q}function r(t,n,r){let s=r.suggest;return Array.isArray(s)||(s=[s]),e.flatMap(s,e=>!1===e?[]:"object"==typeof e?e.known?t["known"+e.known]||[]:[e]:"string"==typeof e&&e?[{v:e,q:""===n}]:[function(e,t){return Object.keys(t.then).length>0?{v:e,suffix:" ",q:!1}:{v:e,suffix:"\n",q:!1}}(n,r)])}function s(t,s){const i=[],a=e.last(s);return Object.keys(a.then).forEach(o=>{let l=a.then[o];"number"==typeof l&&(l=s[s.length-l-1]),e.mergeSets(i,r(t,o,l),n)}),i}function i(t,r,s,{suggest:i,override:a}){let o=null;"object"==typeof i&&i.known&&(o=i.known),r.type&&o!==r.type&&(a&&(r.type=a),e.mergeSets(t["known"+r.type],[{v:r.value,suffix:" ",q:!0}],n),r.type="",r.value=""),o&&(r.type=o,r.value&&(r.value+=s.s),r.value+=s.v)}function a(t,n,r){const a={type:"",value:""};let o=r;const h=[o];return t.line.forEach((n,r)=>{r===t.line.length-1&&(t.completions=s(t,h));const d=n.q?"":n.v;let g=o.then[d];void 0===g?(g=o.then[""],t.isVar=!0):t.isVar=n.q,"number"==typeof g?h.length-=g:h.push(g||l),o=e.last(h),i(t,a,n,o)}),n&&i(t,a,null,{}),t.nextCompletions=s(t,h),t.valid=Boolean(o.then["\n"])||0===Object.keys(o.then).length,o.type}function o(e){const t=e.baseToken||{};return{value:t.v||"",quoted:t.q||!1}}const l={type:"error line-error",suggest:!1,then:{"":0}},h=["database","red"],d=(()=>{function e(e,t=1){return{type:"variable",suggest:{known:"Agent"},then:Object.assign({},e,{"":0,",":{type:"operator",then:{"":t}}})}}function n(e){return{type:"keyword",suggest:[e+" of ",e+": "],then:{of:{type:"keyword",then:{"":g}},":":{type:"operator",then:{"":a}},"":g}}}function r({exit:e,sourceExit:t,blankExit:n}){const r={type:"operator",then:{"+":l,"-":l,"*":l,"!":l,"":e}};return{"+":{type:"operator",then:{"+":l,"-":l,"*":r,"!":l,"":e}},"-":{type:"operator",then:{"+":l,"-":l,"*":r,"!":{type:"operator",then:{"+":l,"-":l,"*":l,"!":l,"":e}},"":e}},"*":{type:"operator",then:Object.assign({"+":r,"-":r,"*":l,"!":l,"":e},t||e)},"!":r,"":n||e}}const s={type:"",suggest:"\n",then:{}},i={type:"",suggest:!1,then:{}},a=t({"\n":s}),o={type:"operator",then:{"":a,"\n":i}},d=e({"\n":s,as:{type:"keyword",then:{"":{type:"variable",suggest:{known:"Agent"},then:{"":0,",":{type:"operator",then:{"":3}},"\n":s}}}}}),g=e({":":o}),c={type:"variable",suggest:{known:"Agent"},then:{"":0,":":{type:"operator",then:{"":a,"\n":i}},"\n":s}},u={":":{type:"operator",then:{"":t({as:{type:"keyword",then:{"":{type:"variable",suggest:{known:"Agent"},then:{"":0,"\n":s}}}}})}}},p={type:"keyword",then:Object.assign({over:{type:"keyword",then:{"":e(u)}}},u)},f={"\n":s,":":{type:"operator",then:{"":a,"\n":i}},with:{type:"keyword",suggest:["with height "],then:{height:{type:"keyword",then:{"":{type:"number",suggest:["6 ","30 "],then:{"\n":s,":":{type:"operator",then:{"":a,"\n":i}}}}}}}}},m=function(e,t,n){const r=Object.assign({},n);return t.forEach(t=>{r[t]={type:e,then:n}}),r}("keyword",["a","an"],function(e,t,n){const r={},s=Object.assign({},n);return t.forEach(t=>{r[t]={type:e,then:s},s[t]=0}),r}("keyword",h,{"\n":s})),b={type:"keyword",then:{"":a,":":{type:"operator",then:{"":a}},"\n":s}},x={title:{type:"keyword",then:{"":a}},theme:{type:"keyword",then:{"":{type:"string",suggest:{global:"themes",suffix:"\n"},then:{"":0,"\n":s}}}},headers:{type:"keyword",then:{none:{type:"keyword",then:{}},cross:{type:"keyword",then:{}},box:{type:"keyword",then:{}},fade:{type:"keyword",then:{}},bar:{type:"keyword",then:{}}}},terminators:{type:"keyword",then:{none:{type:"keyword",then:{}},cross:{type:"keyword",then:{}},box:{type:"keyword",then:{}},fade:{type:"keyword",then:{}},bar:{type:"keyword",then:{}}}},divider:{type:"keyword",then:Object.assign({line:{type:"keyword",then:f},space:{type:"keyword",then:f},delay:{type:"keyword",then:f},tear:{type:"keyword",then:f}},f)},define:{type:"keyword",then:{"":d,as:l}},begin:{type:"keyword",then:{"":d,reference:p,as:l}},end:{type:"keyword",then:{"":d,as:l,"\n":s}},if:b,else:{type:"keyword",suggest:["else\n","else if: "],then:{if:{type:"keyword",suggest:"if: ",then:{"":a,":":{type:"operator",then:{"":a}}}},"\n":s}},repeat:b,group:b,note:{type:"keyword",then:{over:{type:"keyword",then:{"":g}},left:n("left"),right:n("right"),between:{type:"keyword",then:{"":e({":":l},g)}}}},state:{type:"keyword",suggest:"state over ",then:{over:{type:"keyword",then:{"":{type:"variable",suggest:{known:"Agent"},then:{"":0,",":l,":":o}}}}}},text:{type:"keyword",then:{left:n("left"),right:n("right")}},autolabel:{type:"keyword",then:{off:{type:"keyword",then:{}},"":t({"\n":s},[{v:"
  • container: DOM node to append the diagram to (defaults to null).
  • +
  • document: Document object to base the diagram in (defaults to +container's document, or window.document).
  • +
  • textSizerFactory: Function which returns an object capable of +measuring text (defaults to wrapping getComputedTextLength).
  • themes: List of themes to make available to the diagram (defaults to globally registered themes).
  • namespace: Each diagram on a page must have a unique namespace. diff --git a/scripts/core/DOMWrapper.js b/scripts/core/DOMWrapper.js new file mode 100644 index 0000000..e6b7d35 --- /dev/null +++ b/scripts/core/DOMWrapper.js @@ -0,0 +1,208 @@ +define(() => { + 'use strict'; + + function make(value, document) { + if(typeof value === 'string') { + return document.createTextNode(value); + } else if(typeof value === 'number') { + return document.createTextNode(value.toString(10)); + } else if(typeof value === 'object' && value.element) { + return value.element; + } else { + return value; + } + } + + function unwrap(node) { + if(node === null) { + return null; + } else if(node.element) { + return node.element; + } else { + return node; + } + } + + class WrappedElement { + constructor(element) { + this.element = element; + } + + addBefore(child = null, before = null) { + if(child === null) { + return this; + } else if(Array.isArray(child)) { + for(const c of child) { + this.addBefore(c, before); + } + } else { + const childElement = make(child, this.element.ownerDocument); + this.element.insertBefore(childElement, unwrap(before)); + } + return this; + } + + add(...child) { + return this.addBefore(child, null); + } + + del(child = null) { + if(child !== null) { + this.element.removeChild(unwrap(child)); + } + return this; + } + + attr(key, value) { + this.element.setAttribute(key, value); + return this; + } + + attrs(attrs) { + for(const k in attrs) { + if(attrs.hasOwnProperty(k)) { + this.element.setAttribute(k, attrs[k]); + } + } + return this; + } + + styles(styles) { + for(const k in styles) { + if(styles.hasOwnProperty(k)) { + this.element.style[k] = styles[k]; + } + } + return this; + } + + setClass(cls) { + return this.attr('class', cls); + } + + addClass(cls) { + const classes = this.element.getAttribute('class'); + if(!classes) { + return this.setClass(cls); + } + const list = classes.split(' '); + if(list.includes(cls)) { + return this; + } + list.push(cls); + return this.attr('class', list.join(' ')); + } + + delClass(cls) { + const classes = this.element.getAttribute('class'); + if(!classes) { + return this; + } + const list = classes.split(' '); + const p = list.indexOf(cls); + if(p !== -1) { + list.splice(p, 1); + this.attr('class', list.join(' ')); + } + return this; + } + + text(text) { + this.element.textContent = text; + return this; + } + + on(event, callback, options = {}) { + if(Array.isArray(event)) { + for(const e of event) { + this.on(e, callback, options); + } + } else { + this.element.addEventListener(event, callback, options); + } + return this; + } + + off(event, callback, options = {}) { + if(Array.isArray(event)) { + for(const e of event) { + this.off(e, callback, options); + } + } else { + this.element.removeEventListener(event, callback, options); + } + return this; + } + + val(value) { + this.element.value = value; + return this; + } + + select(start, end = null) { + this.element.selectionStart = start; + this.element.selectionEnd = (end === null) ? start : end; + return this; + } + + focus() { + this.element.focus(); + return this; + } + + focussed() { + return this.element === this.element.ownerDocument.activeElement; + } + + empty() { + while(this.element.childNodes.length > 0) { + this.element.removeChild(this.element.lastChild); + } + return this; + } + + attach(parent) { + unwrap(parent).appendChild(this.element); + return this; + } + + detach() { + this.element.parentNode.removeChild(this.element); + return this; + } + } + + return class DOMWrapper { + constructor(document) { + if(!document) { + throw new Error('Missing document!'); + } + this.document = document; + this.wrap = this.wrap.bind(this); + this.el = this.el.bind(this); + this.txt = this.txt.bind(this); + } + + wrap(element) { + if(element.element) { + return element; + } else { + return new WrappedElement(element); + } + } + + el(tag, namespace = null) { + let element = null; + if(namespace === null) { + element = this.document.createElement(tag); + } else { + element = this.document.createElementNS(namespace, tag); + } + return new WrappedElement(element); + } + + txt(content = '') { + return this.document.createTextNode(content); + } + }; +}); diff --git a/scripts/core/EventObject.js b/scripts/core/EventObject.js index 28f1182..2eef07c 100644 --- a/scripts/core/EventObject.js +++ b/scripts/core/EventObject.js @@ -27,6 +27,16 @@ define(() => { } } + on(type, fn) { + this.addEventListener(type, fn); + return this; + } + + off(type, fn) { + this.removeEventListener(type, fn); + return this; + } + countEventListeners(type) { return (this.listeners.get(type) || []).length; } diff --git a/scripts/core/Random.js b/scripts/core/Random.js new file mode 100644 index 0000000..c584897 --- /dev/null +++ b/scripts/core/Random.js @@ -0,0 +1,41 @@ +define(() => { + 'use strict'; + + return class Random { + // xorshift+ 64-bit random generator + // https://en.wikipedia.org/wiki/Xorshift + + constructor() { + this.s = new Uint32Array(4); + } + + reset() { + // Arbitrary random seed with roughly balanced 1s / 0s + // (taken from running Math.random a few times) + this.s[0] = 0x177E9C74; + this.s[1] = 0xAE6FFDCE; + this.s[2] = 0x3CF4F32B; + this.s[3] = 0x46449F88; + } + + nextFloat() { + /* jshint -W016 */ // bit-operations are part of the algorithm + const range = 0x100000000; + let x0 = this.s[0]; + let x1 = this.s[1]; + const y0 = this.s[2]; + const y1 = this.s[3]; + this.s[0] = y0; + this.s[1] = y1; + x0 ^= (x0 << 23) | (x1 >>> 9); + x1 ^= (x1 << 23); + this.s[2] = x0 ^ y0 ^ (x0 >>> 17) ^ (y0 >>> 26); + this.s[3] = ( + x1 ^ y1 ^ + (x0 << 15 | x1 >>> 17) ^ + (y0 << 6 | y1 >>> 26) + ); + return (((this.s[3] + y1) >>> 0) % range) / range; + } + }; +}); diff --git a/scripts/core/Random_spec.js b/scripts/core/Random_spec.js new file mode 100644 index 0000000..e5f4b8e --- /dev/null +++ b/scripts/core/Random_spec.js @@ -0,0 +1,47 @@ +defineDescribe('Random', ['./Random'], (Random) => { + 'use strict'; + + let random = null; + + beforeEach(() => { + random = new Random(); + random.reset(); + }); + + describe('.nextFloat', () => { + it('produces values between 0 and 1', () => { + for(let i = 0; i < 1000; ++ i) { + const v = random.nextFloat(); + expect(v).not.toBeLessThan(0); + expect(v).toBeLessThan(1); + } + }); + + it('produces the same sequence when reset', () => { + const values = []; + for(let i = 0; i < 1000; ++ i) { + values.push(random.nextFloat()); + } + random.reset(); + for(let i = 0; i < 1000; ++ i) { + expect(random.nextFloat()).toEqual(values[i]); + } + }); + + it('produces a roughly uniform range of values', () => { + const samples = 10000; + const granularity = 10; + const buckets = []; + buckets.length = granularity; + buckets.fill(0); + for(let i = 0; i < samples; ++ i) { + const v = random.nextFloat() * granularity; + ++ buckets[Math.floor(v)]; + } + const threshold = (samples / granularity) * 0.9; + for(let i = 0; i < granularity; ++ i) { + expect(buckets[i]).not.toBeLessThan(threshold); + } + }); + }); +}); diff --git a/scripts/core/documents/VirtualDocument.js b/scripts/core/documents/VirtualDocument.js new file mode 100644 index 0000000..0bc5cc4 --- /dev/null +++ b/scripts/core/documents/VirtualDocument.js @@ -0,0 +1,211 @@ +define(() => { + 'use strict'; + + function encodeChar(c) { + return '&#' + c.charCodeAt(0).toString(10) + ';'; + } + + function escapeHTML(text) { + return text.replace(/[^\r\n\t -%'-;=?-~]/g, encodeChar); + } + + function escapeQuoted(text) { + return text.replace(/[^\r\n\t !#$%(-;=?-~]/g, encodeChar); + } + + class TextNode { + constructor(content) { + this.parentNode = null; + this.nodeValue = content; + } + + contains() { + return false; + } + + get textContent() { + return this.nodeValue; + } + + set textContent(value) { + this.nodeValue = value; + } + + get isConnected() { + if(this.parentNode !== null) { + return this.parentNode.isConnected; + } + return false; + } + + get innerHTML() { + return escapeHTML(this.nodeValue); + } + + get outerHTML() { + return this.innerHTML; + } + } + + class ElementNode { + constructor(ownerDocument, tag, namespace) { + this.ownerDocument = ownerDocument; + this.tagName = tag; + this.namespaceURI = namespace; + this.parentNode = null; + this.childNodes = []; + this.attributes = new Map(); + this.listeners = new Map(); + } + + setAttribute(key, value) { + if(typeof value === 'number') { + value = value.toString(10); + } else if(typeof value !== 'string') { + throw new Error('Bad value ' + value + ' for attribute ' + key); + } + this.attributes.set(key, value); + } + + getAttribute(key) { + return this.attributes.get(key); + } + + addEventListener(event, fn) { + let list = this.listeners.get(event); + if(!list) { + list = []; + this.listeners.set(event, list); + } + list.push(fn); + } + + removeEventListener(event, fn) { + const list = this.listeners.get(event) || []; + const index = list.indexOf(fn); + if(index !== -1) { + list.splice(index, 1); + } + } + + dispatchEvent(e) { + const list = this.listeners.get(e.type) || []; + list.forEach((fn) => fn(e)); + } + + contains(descendant) { + let check = descendant; + while(check) { + if(check === this) { + return true; + } + check = check.parentNode; + } + return false; + } + + get firstChild() { + return this.childNodes[0] || null; + } + + get lastChild() { + return this.childNodes[this.childNodes.length - 1] || null; + } + + indexOf(child) { + const index = this.childNodes.indexOf(child); + if(index === -1) { + throw new Error(child + ' is not a child of ' + this); + } + return index; + } + + insertBefore(child, existingChild) { + if(child.contains(this)) { + throw new Error('Cyclic node structures are not permitted'); + } + if(child.parentNode !== null) { + child.parentNode.removeChild(child); + } + if(existingChild === null) { + this.childNodes.push(child); + } else { + this.childNodes.splice(this.indexOf(existingChild), 0, child); + } + child.parentNode = this; + return child; + } + + appendChild(child) { + return this.insertBefore(child, null); + } + + removeChild(child) { + this.childNodes.splice(this.indexOf(child), 1); + child.parentNode = null; + return child; + } + + replaceChild(newChild, oldChild) { + if(newChild === oldChild) { + return oldChild; + } + this.insertBefore(newChild, oldChild); + return this.removeChild(oldChild); + } + + get isConnected() { + return true; + } + + get textContent() { + let text = ''; + for(const child of this.childNodes) { + text += child.textContent; + } + return text; + } + + set textContent(value) { + for(const child of this.childNodes) { + child.parentNode = null; + } + this.childNodes.length = 0; + this.appendChild(new TextNode(value)); + } + + get innerHTML() { + let html = ''; + for(const child of this.childNodes) { + html += child.outerHTML; + } + return html; + } + + get outerHTML() { + let attrs = ''; + for(const [key, value] of this.attributes) { + attrs += ' ' + key + '="' + escapeQuoted(value) + '"'; + } + return ( + '<' + this.tagName + attrs + '>' + + this.innerHTML + + '' + ); + } + } + + return class VirtualDocument { + createElement(tag) { + return new ElementNode(this, tag, ''); + } + + createElementNS(ns, tag) { + return new ElementNode(this, tag, ns || ''); + } + + createTextNode(content) { + return new TextNode(content); + } + }; +}); diff --git a/scripts/core/documents/VirtualDocument_spec.js b/scripts/core/documents/VirtualDocument_spec.js new file mode 100644 index 0000000..38b6a47 --- /dev/null +++ b/scripts/core/documents/VirtualDocument_spec.js @@ -0,0 +1,260 @@ +defineDescribe('VirtualDocument', ['./VirtualDocument'], (VirtualDocument) => { + 'use strict'; + + const doc = new VirtualDocument(); + + describe('createElement', () => { + it('creates elements which conform to the DOM API', () => { + const o = doc.createElement('div'); + expect(o.ownerDocument).toEqual(doc); + expect(o.tagName).toEqual('div'); + expect(o.namespaceURI).toEqual(''); + expect(o.parentNode).toEqual(null); + expect(o.childNodes.length).toEqual(0); + }); + + it('claims all elements are always connected', () => { + const o = doc.createElement('div'); + expect(o.isConnected).toEqual(true); + }); + }); + + describe('appendChild', () => { + it('adds a child to the element', () => { + const o = doc.createElement('div'); + o.appendChild(doc.createElement('span')); + + expect(o.childNodes.length).toEqual(1); + expect(o.childNodes[0].tagName).toEqual('span'); + }); + + it('removes the child from its old parent', () => { + const o = doc.createElement('div'); + const oldParent = doc.createElement('div'); + const child = doc.createElement('span'); + oldParent.appendChild(child); + o.appendChild(child); + + expect(oldParent.childNodes.length).toEqual(0); + }); + + it('rejects loops', () => { + const o1 = doc.createElement('div'); + const o2 = doc.createElement('div'); + const o3 = doc.createElement('div'); + o1.appendChild(o2); + o2.appendChild(o3); + + expect(() => o3.appendChild(o1)).toThrow(); + }); + }); + + describe('removeChild', () => { + it('removes a child from the element', () => { + const o = doc.createElement('div'); + const child = doc.createElement('span'); + o.appendChild(child); + o.removeChild(child); + + expect(o.childNodes.length).toEqual(0); + }); + + it('rejects nodes which are not children', () => { + const o = doc.createElement('div'); + const child = doc.createElement('span'); + + expect(() => o.removeChild(child)).toThrow(); + }); + }); + + describe('firstChild', () => { + it('returns the first child of the node', () => { + const o = doc.createElement('div'); + o.appendChild(doc.createElement('a')); + o.appendChild(doc.createElement('b')); + + expect(o.firstChild.tagName).toEqual('a'); + }); + + it('returns null if there are no children', () => { + const o = doc.createElement('div'); + + expect(o.firstChild).toEqual(null); + }); + }); + + describe('lastChild', () => { + it('returns the last child of the node', () => { + const o = doc.createElement('div'); + o.appendChild(doc.createElement('a')); + o.appendChild(doc.createElement('b')); + + expect(o.lastChild.tagName).toEqual('b'); + }); + + it('returns null if there are no children', () => { + const o = doc.createElement('div'); + + expect(o.lastChild).toEqual(null); + }); + }); + + describe('contains', () => { + it('returns true if the given node is within the current node', () => { + const o = doc.createElement('div'); + const child = doc.createElement('div'); + o.appendChild(child); + + expect(o.contains(child)).toEqual(true); + }); + + it('performs a deep check', () => { + const o = doc.createElement('div'); + const middle = doc.createElement('div'); + const child = doc.createElement('div'); + o.appendChild(middle); + middle.appendChild(child); + + expect(o.contains(child)).toEqual(true); + }); + + it('returns true if the nodes are the same', () => { + const o = doc.createElement('div'); + + expect(o.contains(o)).toEqual(true); + }); + + it('returns false if the node is not within the current node', () => { + const o = doc.createElement('div'); + const o2 = doc.createElement('div'); + + expect(o.contains(o2)).toEqual(false); + }); + }); + + describe('textContent', () => { + it('replaces the content of the element', () => { + const o = doc.createElement('div'); + o.appendChild(doc.createElement('span')); + o.textContent = 'foo'; + + expect(o.innerHTML).toEqual('foo'); + }); + + it('returns the text content of all child nodes', () => { + const o = doc.createElement('div'); + const child = doc.createElement('span'); + o.appendChild(doc.createTextNode('abc')); + o.appendChild(child); + child.appendChild(doc.createTextNode('def')); + o.appendChild(doc.createTextNode('ghi')); + + expect(o.textContent).toEqual('abcdefghi'); + }); + }); + + describe('attributes', () => { + it('keeps a key/value map of attributes', () => { + const o = doc.createElement('div'); + o.setAttribute('foo', 'bar'); + o.setAttribute('zig', 'zag'); + o.setAttribute('foo', 'baz'); + + expect(o.getAttribute('foo')).toEqual('baz'); + expect(o.getAttribute('zig')).toEqual('zag'); + expect(o.getAttribute('nope')).toEqual(undefined); + }); + }); + + describe('events', () => { + let o = null; + let called = null; + let fn = null; + + beforeEach(() => { + o = doc.createElement('div'); + called = 0; + fn = () => { + ++ called; + }; + }); + + it('stores and triggers event listeners', () => { + o.addEventListener('foo', fn); + + o.dispatchEvent(new Event('foo')); + + expect(called).toEqual(1); + }); + + it('removes listeners when removeEventListener is called', () => { + o.addEventListener('foo', fn); + o.removeEventListener('foo', fn); + + o.dispatchEvent(new Event('foo')); + + expect(called).toEqual(0); + }); + + it('stores multiple event listeners', () => { + const fn2 = () => { + called += 10; + }; + o.addEventListener('foo', fn); + o.addEventListener('foo', fn2); + + o.dispatchEvent(new Event('foo')); + + expect(called).toEqual(11); + }); + + it('invokes listeners according to their type', () => { + o.addEventListener('foo', fn); + + o.dispatchEvent(new Event('bar')); + + expect(called).toEqual(0); + }); + }); + + describe('outerHTML', () => { + it('returns the tag in HTML form', () => { + const o = doc.createElement('div'); + + expect(o.outerHTML).toEqual('
    '); + }); + + it('includes attributes', () => { + const o = doc.createElement('div'); + o.setAttribute('foo', 'bar'); + o.setAttribute('zig', 'zag'); + + expect(o.outerHTML).toEqual('
    '); + }); + + it('escapes attributes', () => { + const o = doc.createElement('div'); + o.setAttribute('foo', 'b&a"r'); + + expect(o.outerHTML).toEqual('
    '); + }); + + it('includes all children', () => { + const o = doc.createElement('div'); + const child = doc.createElement('span'); + o.appendChild(doc.createTextNode('abc')); + o.appendChild(child); + child.appendChild(doc.createTextNode('def')); + o.appendChild(doc.createTextNode('ghi')); + + expect(o.outerHTML).toEqual('
    abcdefghi
    '); + }); + + it('escapes text content', () => { + const o = doc.createElement('div'); + o.appendChild(doc.createTextNode('ac')); + + expect(o.outerHTML).toEqual('
    a<b>c
    '); + }); + }); +}); diff --git a/scripts/interface/Interface.js b/scripts/interface/Interface.js index 8f7d240..ff671f9 100644 --- a/scripts/interface/Interface.js +++ b/scripts/interface/Interface.js @@ -1,26 +1,11 @@ -define(['require'], (require) => { +define(['require', 'core/DOMWrapper'], (require, DOMWrapper) => { 'use strict'; const DELAY_AGENTCHANGE = 500; const DELAY_STAGECHANGE = 250; const PNG_RESOLUTION = 4; - function makeText(text = '') { - return document.createTextNode(text); - } - - function makeNode(type, attrs = {}, children = []) { - const o = document.createElement(type); - for(const k in attrs) { - if(attrs.hasOwnProperty(k)) { - o.setAttribute(k, attrs[k]); - } - } - for(const c of children) { - o.appendChild(c); - } - return o; - } + const dom = new DOMWrapper(document); function addNewline(value) { if(value.length > 0 && value.charAt(value.length - 1) !== '\n') { @@ -29,10 +14,6 @@ define(['require'], (require) => { return value; } - function on(element, events, fn) { - events.forEach((event) => element.addEventListener(event, fn)); - } - function findPos(content, index) { let p = 0; let line = 0; @@ -166,196 +147,140 @@ define(['require'], (require) => { this._showDropStyle = this._showDropStyle.bind(this); this._hideDropStyle = this._hideDropStyle.bind(this); - this._enhanceEditor(); - } - - buildOptionsLinks() { - const options = makeNode('div', {'class': 'options links'}); - this.links.forEach((link) => { - options.appendChild(makeNode('a', { - 'href': link.href, - 'target': '_blank', - }, [makeText(link.label)])); - }); - return options; + this.diagram + .on('render', () => { + this.updateMinSize(this.diagram.getSize()); + this.pngDirty = true; + }) + .on('mouseover', (element) => { + if(this.marker) { + this.marker.clear(); + } + if(element.ln !== undefined && this.code.markText) { + this.marker = this.code.markText( + {line: element.ln, ch: 0}, + {line: element.ln + 1, ch: 0}, + { + className: 'hover', + inclusiveLeft: false, + inclusiveRight: false, + clearOnEnter: true, + } + ); + } + }) + .on('mouseout', () => { + if(this.marker) { + this.marker.clear(); + this.marker = null; + } + }) + .on('click', (element) => { + if(this.marker) { + this.marker.clear(); + this.marker = null; + } + if(element.ln !== undefined && this.code.setSelection) { + this.code.setSelection( + {line: element.ln, ch: 0}, + {line: element.ln + 1, ch: 0}, + {origin: '+focus', bias: -1} + ); + this.code.focus(); + } + }) + .on('dblclick', (element) => { + this.diagram.toggleCollapsed(element.ln); + }); } buildOptionsDownloads() { - this.downloadPNG = makeNode('a', { - 'href': '#', - 'download': 'SequenceDiagram.png', - }, [makeText('Download PNG')]); - on(this.downloadPNG, [ - 'focus', - 'mouseover', - 'mousedown', - ], this._downloadPNGFocus); - on(this.downloadPNG, ['click'], this._downloadPNGClick); + this.downloadPNG = dom.el('a') + .text('Download PNG') + .attrs({ + 'href': '#', + 'download': 'SequenceDiagram.png', + }) + .on(['focus', 'mouseover', 'mousedown'], this._downloadPNGFocus) + .on('click', this._downloadPNGClick); - this.downloadSVG = makeNode('a', { - 'href': '#', - 'download': 'SequenceDiagram.svg', - }, [makeText('SVG')]); - on(this.downloadSVG, ['click'], this._downloadSVGClick); + this.downloadSVG = dom.el('a') + .text('SVG') + .attrs({ + 'href': '#', + 'download': 'SequenceDiagram.svg', + }) + .on('click', this._downloadSVGClick); - return makeNode('div', {'class': 'options downloads'}, [ - this.downloadPNG, - this.downloadSVG, - ]); - } - - buildEditor(container) { - const value = this.loadCode() || this.defaultCode; - const code = makeNode('textarea', {'class': 'editor-simple'}); - code.value = value; - container.appendChild(code); - - return code; - } - - registerListeners() { - this.code.addEventListener('input', () => this.update(false)); - - this.diagram.addEventListener('render', () => { - this.updateMinSize(this.diagram.getSize()); - this.pngDirty = true; - }); - - this.diagram.addEventListener('mouseover', (element) => { - if(this.marker) { - this.marker.clear(); - } - if(element.ln !== undefined && this.code.markText) { - this.marker = this.code.markText( - {line: element.ln, ch: 0}, - {line: element.ln + 1, ch: 0}, - { - className: 'hover', - inclusiveLeft: false, - inclusiveRight: false, - clearOnEnter: true, - } - ); - } - }); - - this.diagram.addEventListener('mouseout', () => { - if(this.marker) { - this.marker.clear(); - this.marker = null; - } - }); - - this.diagram.addEventListener('click', (element) => { - if(this.marker) { - this.marker.clear(); - this.marker = null; - } - if(element.ln !== undefined && this.code.setSelection) { - this.code.setSelection( - {line: element.ln, ch: 0}, - {line: element.ln + 1, ch: 0}, - {origin: '+focus', bias: -1} - ); - this.code.focus(); - } - }); - - this.diagram.addEventListener('dblclick', (element) => { - this.diagram.toggleCollapsed(element.ln); - }); - - this.container.addEventListener('dragover', (event) => { - event.preventDefault(); - if(hasDroppedFile(event, 'image/svg+xml')) { - event.dataTransfer.dropEffect = 'copy'; - this._showDropStyle(); - } else { - event.dataTransfer.dropEffect = 'none'; - } - }); - - this.container.addEventListener('dragleave', this._hideDropStyle); - this.container.addEventListener('dragend', this._hideDropStyle); - - this.container.addEventListener('drop', (event) => { - event.preventDefault(); - this._hideDropStyle(); - const file = getDroppedFile(event, 'image/svg+xml'); - if(file) { - this.loadFile(file); - } - }); + return dom.el('div').setClass('options downloads') + .add(this.downloadPNG, this.downloadSVG); } buildLibrary(container) { const diagrams = this.library.map((lib) => { - const holdInner = makeNode('div', { - 'title': lib.title || lib.code, - }); - const hold = makeNode('div', { - 'class': 'library-item', - }, [holdInner]); - hold.addEventListener( - 'click', - this.addCodeBlock.bind(this, lib.code) - ); - container.appendChild(hold); - const diagram = this.diagram.clone({ + const holdInner = dom.el('div') + .attr('title', lib.title || lib.code); + + const hold = dom.el('div') + .setClass('library-item') + .add(holdInner) + .on('click', this.addCodeBlock.bind(this, lib.code)) + .attach(container); + + return this.diagram.clone({ code: simplifyPreview(lib.preview || lib.code), - container: holdInner, + container: holdInner.element, render: false, - }); - diagram.addEventListener('error', (sd, e) => { + }).on('error', (sd, e) => { window.console.warn('Failed to render preview', e); - hold.setAttribute('class', 'library-item broken'); - holdInner.textContent = lib.code; + hold.attr('class', 'library-item broken'); + holdInner.text(lib.code); }); - return diagram; }); try { this.diagram.renderAll(diagrams); } catch(e) {} - } - buildErrorReport() { - this.errorText = makeText(); - this.errorMsg = makeNode('div', {'class': 'msg-error'}, [ - this.errorText, - ]); - return this.errorMsg; + return container; } buildViewPane() { - this.viewPaneInner = makeNode('div', {'class': 'pane-view-inner'}); + this.viewPaneInner = dom.el('div').setClass('pane-view-inner') + .add(this.diagram.dom()); - return makeNode('div', {'class': 'pane-view'}, [ - makeNode('div', {'class': 'pane-view-scroller'}, [ - this.viewPaneInner, - ]), - this.buildErrorReport(), - ]); + this.errorMsg = dom.el('div').setClass('msg-error'); + + return dom.el('div').setClass('pane-view') + .add( + dom.el('div').setClass('pane-view-scroller') + .add(this.viewPaneInner), + this.errorMsg + ); } - buildLeftPanes(container) { - const codePane = makeNode('div', {'class': 'pane-code'}); - container.appendChild(codePane); - let libPane = null; + buildLeftPanes() { + const container = dom.el('div').setClass('pane-side'); + + this.code = dom.el('textarea') + .setClass('editor-simple') + .val(this.loadCode() || this.defaultCode) + .on('input', () => this.update(false)); + + const codePane = dom.el('div').setClass('pane-code') + .add(this.code) + .attach(container); if(this.library.length > 0) { - const libPaneInner = makeNode('div', { - 'class': 'pane-library-inner', - }); - libPane = makeNode('div', {'class': 'pane-library'}, [ - makeNode('div', {'class': 'pane-library-scroller'}, [ - libPaneInner, - ]), - ]); - container.appendChild(libPane); - this.buildLibrary(libPaneInner); + const libPane = dom.el('div').setClass('pane-library') + .add(dom.el('div').setClass('pane-library-scroller') + .add(this.buildLibrary( + dom.el('div').setClass('pane-library-inner') + )) + ) + .attach(container); - makeSplit([codePane, libPane], { + makeSplit([codePane.element, libPane.element], { direction: 'vertical', snapOffset: 5, sizes: [70, 30], @@ -363,39 +288,58 @@ define(['require'], (require) => { }); } - return {codePane, libPane}; + return container; } build(container) { - this.container = container; - + const lPane = this.buildLeftPanes(); const viewPane = this.buildViewPane(); - const lPane = makeNode('div', {'class': 'pane-side'}); - const hold = makeNode('div', {'class': 'pane-hold'}, [ - lPane, - viewPane, - this.buildOptionsLinks(), - this.buildOptionsDownloads(), - ]); - container.appendChild(hold); + this.container = dom.wrap(container) + .add(dom.el('div').setClass('pane-hold') + .add( + lPane, + viewPane, + dom.el('div').setClass('options links') + .add(this.links.map((link) => dom.el('a') + .attrs({'href': link.href, 'target': '_blank'}) + .text(link.label) + )), + this.buildOptionsDownloads() + ) + ) + .on('dragover', (event) => { + event.preventDefault(); + if(hasDroppedFile(event, 'image/svg+xml')) { + event.dataTransfer.dropEffect = 'copy'; + this._showDropStyle(); + } else { + event.dataTransfer.dropEffect = 'none'; + } + }) + .on('dragleave', this._hideDropStyle) + .on('dragend', this._hideDropStyle) + .on('drop', (event) => { + event.preventDefault(); + this._hideDropStyle(); + const file = getDroppedFile(event, 'image/svg+xml'); + if(file) { + this.loadFile(file); + } + }); - makeSplit([lPane, viewPane], { + makeSplit([lPane.element, viewPane.element], { direction: 'horizontal', snapOffset: 70, sizes: [30, 70], minSize: [10, 10], }); - const {codePane} = this.buildLeftPanes(lPane); - this.code = this.buildEditor(codePane); - this.viewPaneInner.appendChild(this.diagram.dom()); - - this.registerListeners(); - // Delay first update 1 frame to ensure render target is ready // (prevents initial incorrect font calculations for custom fonts) setTimeout(this.update.bind(this), 0); + + this._enhanceEditor(); } addCodeBlock(block) { @@ -413,15 +357,15 @@ define(['require'], (require) => { this.code.setCursor({line: pos.line + lines, ch: 0}); } else { const value = this.value(); - const cur = this.code.selectionStart; + const cur = this.code.element.selectionStart; const pos = ('\n' + value + '\n').indexOf('\n', cur); const replaced = ( addNewline(value.substr(0, pos)) + addNewline(block) ); - this.code.value = replaced + value.substr(pos); - this.code.selectionStart = replaced.length; - this.code.selectionEnd = replaced.length; + this.code + .val(replaced + value.substr(pos)) + .select(replaced.length); this.update(false); } @@ -429,9 +373,10 @@ define(['require'], (require) => { } updateMinSize({width, height}) { - const style = this.viewPaneInner.style; - style.minWidth = Math.ceil(width * this.minScale) + 'px'; - style.minHeight = Math.ceil(height * this.minScale) + 'px'; + this.viewPaneInner.styles({ + 'minWidth': Math.ceil(width * this.minScale) + 'px', + 'minHeight': Math.ceil(height * this.minScale) + 'px', + }); } redraw(sequence) { @@ -465,23 +410,22 @@ define(['require'], (require) => { markError(error) { if(typeof error === 'object' && error.message) { - this.errorText.nodeValue = error.message; + this.errorMsg.text(error.message); } else { - this.errorText.nodeValue = error; + this.errorMsg.text(error); } - this.errorMsg.setAttribute('class', 'msg-error error'); + this.errorMsg.addClass('error'); } markOK() { - this.errorText.nodeValue = ''; - this.errorMsg.setAttribute('class', 'msg-error'); + this.errorMsg.text('').delClass('error'); } value() { if(this.code.getDoc) { return this.code.getDoc().getValue(); } else { - return this.code.value; + return this.code.element.value; } } @@ -491,7 +435,7 @@ define(['require'], (require) => { doc.setValue(code); doc.clearHistory(); } else { - this.code.value = code; + this.code.val(code); } this.diagram.expandAll({render: false}); this.update(true); @@ -555,7 +499,7 @@ define(['require'], (require) => { this.diagram.getPNG({resolution: PNG_RESOLUTION}) .then(({url, latest}) => { if(latest) { - this.downloadPNG.setAttribute('href', url); + this.downloadPNG.attr('href', url); this.updatingPNG = false; } }); @@ -563,11 +507,11 @@ define(['require'], (require) => { } _showDropStyle() { - this.container.setAttribute('class', 'drop-target'); + this.container.addClass('drop-target'); } _hideDropStyle() { - this.container.setAttribute('class', ''); + this.container.delClass('drop-target'); } _downloadPNGFocus() { @@ -585,7 +529,7 @@ define(['require'], (require) => { _downloadSVGClick() { this.forceRender(); const url = this.diagram.getSVGSynchronous(); - this.downloadSVG.setAttribute('href', url); + this.downloadSVG.attr('href', url); } _enhanceEditor() { @@ -599,12 +543,12 @@ define(['require'], (require) => { ], (CodeMirror) => { this.diagram.registerCodeMirrorMode(CodeMirror); - const selBegin = this.code.selectionStart; - const selEnd = this.code.selectionEnd; - const value = this.code.value; - const focussed = this.code === document.activeElement; + const selBegin = this.code.element.selectionStart; + const selEnd = this.code.element.selectionEnd; + const value = this.code.element.value; + const focussed = this.code.focussed(); - const code = new CodeMirror(this.code.parentNode, { + const code = new CodeMirror(this.code.element.parentNode, { value, mode: 'sequence', globals: { @@ -622,7 +566,7 @@ define(['require'], (require) => { 'Cmd-Enter': 'autocomplete', }, }); - this.code.parentNode.removeChild(this.code); + this.code.detach(); code.getDoc().setSelection( findPos(value, selBegin), findPos(value, selEnd) diff --git a/scripts/interface/Interface_spec.js b/scripts/interface/Interface_spec.js index 97a2e61..a9403b7 100644 --- a/scripts/interface/Interface_spec.js +++ b/scripts/interface/Interface_spec.js @@ -17,7 +17,7 @@ defineDescribe('Interface', ['./Interface'], (Interface) => { 'getSize', 'process', 'getThemeNames', - 'addEventListener', + 'on', 'registerCodeMirrorMode', 'getSVGSynchronous', ]); @@ -26,10 +26,11 @@ defineDescribe('Interface', ['./Interface'], (Interface) => { agents: [], stages: [], }); + sequenceDiagram.on.and.returnValue(sequenceDiagram); sequenceDiagram.getSize.and.returnValue({width: 10, height: 20}); sequenceDiagram.dom.and.returnValue(document.createElement('svg')); container = jasmine.createSpyObj('container', [ - 'appendChild', + 'insertBefore', 'addEventListener', ]); @@ -42,7 +43,7 @@ defineDescribe('Interface', ['./Interface'], (Interface) => { describe('build', () => { it('adds elements to the given container', () => { ui.build(container); - expect(container.appendChild).toHaveBeenCalled(); + expect(container.insertBefore).toHaveBeenCalled(); }); it('creates a code mirror instance with the given code', (done) => { @@ -64,14 +65,16 @@ defineDescribe('Interface', ['./Interface'], (Interface) => { sequenceDiagram.getSVGSynchronous.and.returnValue('mySVGURL'); ui.build(container); - expect(ui.downloadSVG.getAttribute('href')).toEqual('#'); + const el = ui.downloadSVG.element; + + expect(el.getAttribute('href')).toEqual('#'); if(safari) { // Safari actually starts a download if we do this, which // doesn't seem to fit its usual security vibe return; } - ui.downloadSVG.dispatchEvent(new Event('click')); - expect(ui.downloadSVG.getAttribute('href')).toEqual('mySVGURL'); + el.dispatchEvent(new Event('click')); + expect(el.getAttribute('href')).toEqual('mySVGURL'); }); }); }); diff --git a/scripts/sequence/Exporter.js b/scripts/sequence/Exporter.js index 1ffc5cc..c6c826f 100644 --- a/scripts/sequence/Exporter.js +++ b/scripts/sequence/Exporter.js @@ -16,7 +16,7 @@ define(() => { } getSVGContent(renderer) { - let code = renderer.svg().outerHTML; + let code = renderer.dom().outerHTML; // Firefox fails to render SVGs as unless they have size // attributes on the tag, so we must set this when diff --git a/scripts/sequence/MarkdownParser_spec.js b/scripts/sequence/MarkdownParser_spec.js index 9ba91f9..1feeef5 100644 --- a/scripts/sequence/MarkdownParser_spec.js +++ b/scripts/sequence/MarkdownParser_spec.js @@ -1,11 +1,11 @@ defineDescribe('Markdown Parser', [ './MarkdownParser', - 'svg/SVGTextBlock', - 'svg/SVGUtilities', + 'svg/SVG', + 'stubs/TestDOM', ], ( parser, - SVGTextBlock, - svg + SVG, + TestDOM ) => { 'use strict'; @@ -122,25 +122,12 @@ defineDescribe('Markdown Parser', [ ]]); }); - describe('SVGTextBlock interaction', () => { - let hold = null; - let block = null; - - beforeEach(() => { - hold = svg.makeContainer(); - document.body.appendChild(hold); - block = new SVGTextBlock(hold, {attrs: {'font-size': 12}}); - }); - - afterEach(() => { - document.body.removeChild(hold); - }); - - it('produces a format compatible with SVGTextBlock', () => { - const formatted = parser('hello everybody'); - block.set({formatted}); - expect(hold.children.length).toEqual(1); - expect(hold.children[0].innerHTML).toEqual('hello everybody'); - }); + it('produces a format compatible with SVG.formattedText', () => { + const formatted = parser('hello everybody'); + const svg = new SVG(TestDOM.dom, TestDOM.textSizerFactory); + const block = svg.formattedText({}, formatted).element; + expect(block.outerHTML).toEqual( + 'hello everybody' + ); }); }); diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index 2dcbcbd..1a47fe3 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -2,8 +2,8 @@ define([ 'core/ArrayUtilities', 'core/EventObject', - 'svg/SVGUtilities', - 'svg/SVGShapes', + 'core/DOMWrapper', + 'svg/SVG', './components/BaseComponent', './components/Block', './components/Parallel', @@ -16,8 +16,8 @@ define([ ], ( array, EventObject, - svg, - SVGShapes, + DOMWrapper, + SVG, BaseComponent ) => { /* jshint +W072 */ @@ -68,7 +68,8 @@ define([ themes = [], namespace = null, components = null, - SVGTextBlockClass = SVGShapes.TextBlock, + document, + textSizerFactory = null, } = {}) { super(); @@ -82,10 +83,11 @@ define([ this.width = 0; this.height = 0; this.themes = makeThemes(themes); + this.themeBuilder = null; this.theme = null; this.namespace = parseNamespace(namespace); this.components = components; - this.SVGTextBlockClass = SVGTextBlockClass; + this.svg = new SVG(new DOMWrapper(document), textSizerFactory); this.knownThemeDefs = new Set(); this.knownDefs = new Set(); this.highlights = new Map(); @@ -110,61 +112,54 @@ define([ this.themes.set(theme.name, theme); } - buildMetadata() { - this.metaCode = svg.makeText(); - return svg.make('metadata', {}, [this.metaCode]); - } - buildStaticElements() { - this.base = svg.makeContainer(); + const el = this.svg.el; - this.themeDefs = svg.make('defs'); - this.defs = svg.make('defs'); - this.fullMask = svg.make('mask', { + this.metaCode = this.svg.txt(); + this.themeDefs = el('defs'); + this.defs = el('defs'); + this.fullMask = el('mask').attrs({ 'id': this.namespace + 'FullMask', 'maskUnits': 'userSpaceOnUse', }); - this.lineMask = svg.make('mask', { + this.lineMask = el('mask').attrs({ 'id': this.namespace + 'LineMask', 'maskUnits': 'userSpaceOnUse', }); - this.fullMaskReveal = svg.make('rect', {'fill': '#FFFFFF'}); - this.lineMaskReveal = svg.make('rect', {'fill': '#FFFFFF'}); - this.backgroundFills = svg.make('g'); - this.agentLines = svg.make('g', { - 'mask': 'url(#' + this.namespace + 'LineMask)', - }); - this.blocks = svg.make('g'); - this.shapes = svg.make('g'); - this.unmaskedShapes = svg.make('g'); - this.base.appendChild(this.buildMetadata()); - this.base.appendChild(this.themeDefs); - this.base.appendChild(this.defs); - this.base.appendChild(this.backgroundFills); - this.base.appendChild( - svg.make('g', { - 'mask': 'url(#' + this.namespace + 'FullMask)', - }, [ - this.agentLines, - this.blocks, - this.shapes, - ]) - ); - this.base.appendChild(this.unmaskedShapes); - this.title = new this.SVGTextBlockClass(this.base); + this.fullMaskReveal = el('rect').attr('fill', '#FFFFFF'); + this.lineMaskReveal = el('rect').attr('fill', '#FFFFFF'); + this.backgroundFills = el('g'); + this.agentLines = el('g') + .attr('mask', 'url(#' + this.namespace + 'LineMask)'); + this.blocks = el('g'); + this.shapes = el('g'); + this.unmaskedShapes = el('g'); + this.title = this.svg.formattedText(); - this.sizer = new this.SVGTextBlockClass.SizeTester(this.base); + this.svg.body.add( + this.svg.el('metadata') + .add(this.metaCode), + this.themeDefs, + this.defs, + this.backgroundFills, + el('g') + .attr('mask', 'url(#' + this.namespace + 'FullMask)') + .add( + this.agentLines, + this.blocks, + this.shapes + ), + this.unmaskedShapes, + this.title + ); } addThemeDef(name, generator) { const namespacedName = this.namespace + name; - if(this.knownThemeDefs.has(name)) { - return namespacedName; + if(!this.knownThemeDefs.has(name)) { + this.knownThemeDefs.add(name); + this.themeDefs.add(generator().attr('id', namespacedName)); } - this.knownThemeDefs.add(name); - const def = generator(); - def.setAttribute('id', namespacedName); - this.themeDefs.appendChild(def); return namespacedName; } @@ -176,13 +171,10 @@ define([ } const namespacedName = this.namespace + name; - if(this.knownDefs.has(name)) { - return namespacedName; + if(!this.knownDefs.has(name)) { + this.knownDefs.add(name); + this.defs.add(generator().attr('id', namespacedName)); } - this.knownDefs.add(name); - const def = generator(); - def.setAttribute('id', namespacedName); - this.defs.appendChild(def); return namespacedName; } @@ -203,7 +195,7 @@ define([ renderer: this, theme: this.theme, agentInfos: this.agentInfos, - textSizer: this.sizer, + textSizer: this.svg.textSizer, state: this.state, components: this.components, }; @@ -251,7 +243,7 @@ define([ agentInfos: this.agentInfos, visibleAgentIDs: this.visibleAgentIDs, momentaryAgentIDs: agentIDs, - textSizer: this.sizer, + textSizer: this.svg.textSizer, addSpacing, addSeparation, state: this.state, @@ -301,7 +293,7 @@ define([ renderer: this, theme: this.theme, agentInfos: this.agentInfos, - textSizer: this.sizer, + textSizer: this.svg.textSizer, state: this.state, components: this.components, }; @@ -346,20 +338,18 @@ define([ drawAgentLine(agentInfo, toY) { if( - agentInfo.latestYStart === null || - toY <= agentInfo.latestYStart + agentInfo.latestYStart !== null && + toY > agentInfo.latestYStart ) { - return; + this.agentLines.add(this.theme.renderAgentLine({ + x: agentInfo.x, + y0: agentInfo.latestYStart, + y1: toY, + width: agentInfo.currentRad * 2, + className: 'agent-' + agentInfo.index + '-line', + options: agentInfo.options, + })); } - - this.agentLines.appendChild(this.theme.renderAgentLine({ - x: agentInfo.x, - y0: agentInfo.latestYStart, - y1: toY, - width: agentInfo.currentRad * 2, - className: 'agent-' + agentInfo.index + '-line', - options: agentInfo.options, - })); } addHighlightObject(line, o) { @@ -372,7 +362,7 @@ define([ } forwardEvent(source, sourceEvent, forwardEvent, forwardArgs) { - source.addEventListener( + source.on( sourceEvent, this.trigger.bind(this, forwardEvent, forwardArgs) ); @@ -388,7 +378,7 @@ define([ renderer: this, theme: this.theme, agentInfos: this.agentInfos, - textSizer: this.sizer, + textSizer: this.svg.textSizer, state: this.state, components: this.components, }; @@ -403,16 +393,14 @@ define([ stageOverride = null, unmasked = false, } = {}) => { - const o = svg.make('g'); + const o = this.svg.el('g').setClass('region'); const targetStage = (stageOverride || stage); this.addHighlightObject(targetStage.ln, o); - o.setAttribute('class', 'region'); this.forwardEvent(o, 'mouseenter', 'mouseover', [targetStage]); this.forwardEvent(o, 'mouseleave', 'mouseout', [targetStage]); this.forwardEvent(o, 'click', 'click', [targetStage]); this.forwardEvent(o, 'dblclick', 'dblclick', [targetStage]); - (unmasked ? this.unmaskedShapes : this.shapes).appendChild(o); - return o; + return o.attach(unmasked ? this.unmaskedShapes : this.shapes); }; const env = { @@ -425,8 +413,7 @@ define([ lineMaskLayer: this.lineMask, theme: this.theme, agentInfos: this.agentInfos, - textSizer: this.sizer, - SVGTextBlockClass: this.SVGTextBlockClass, + textSizer: this.svg.textSizer, state: this.state, drawAgentLine: (agentID, toY, andStop = false) => { const agentInfo = this.agentInfos.get(agentID); @@ -436,6 +423,7 @@ define([ addDef: this.addDef, makeRegion, components: this.components, + svg: this.svg, }; let bottomY = topY; @@ -511,7 +499,7 @@ define([ updateBounds(stagesHeight) { const cx = (this.minX + this.maxX) / 2; - const titleSize = this.sizer.measure(this.title); + const titleSize = this.svg.textSizer.measure(this.title); const titleY = ((titleSize.height > 0) ? (-this.theme.titleMargin - titleSize.height) : 0 ); @@ -534,10 +522,10 @@ define([ 'height': this.height, }; - svg.setAttributes(this.fullMaskReveal, fullSize); - svg.setAttributes(this.lineMaskReveal, fullSize); + this.fullMaskReveal.attrs(fullSize); + this.lineMaskReveal.attrs(fullSize); - this.base.setAttribute('viewBox', ( + this.svg.body.attr('viewBox', ( x0 + ' ' + y0 + ' ' + this.width + ' ' + this.height )); @@ -554,23 +542,23 @@ define([ _reset(theme) { if(theme) { this.knownThemeDefs.clear(); - svg.empty(this.themeDefs); + this.themeDefs.empty(); } this.knownDefs.clear(); this.highlights.clear(); - svg.empty(this.defs); - svg.empty(this.fullMask); - svg.empty(this.lineMask); - svg.empty(this.backgroundFills); - svg.empty(this.agentLines); - svg.empty(this.blocks); - svg.empty(this.shapes); - svg.empty(this.unmaskedShapes); - this.fullMask.appendChild(this.fullMaskReveal); - this.lineMask.appendChild(this.lineMaskReveal); - this.defs.appendChild(this.fullMask); - this.defs.appendChild(this.lineMask); + this.defs.empty(); + this.fullMask.empty(); + this.lineMask.empty(); + this.backgroundFills.empty(); + this.agentLines.empty(); + this.blocks.empty(); + this.shapes.empty(); + this.unmaskedShapes.empty(); + this.defs.add( + this.fullMask.add(this.fullMaskReveal), + this.lineMask.add(this.lineMaskReveal) + ); this._resetState(); } @@ -583,12 +571,12 @@ define([ } if(this.highlights.has(this.currentHighlight)) { this.highlights.get(this.currentHighlight).forEach((o) => { - o.setAttribute('class', 'region'); + o.delClass('focus'); }); } if(this.highlights.has(line)) { this.highlights.get(line).forEach((o) => { - o.setAttribute('class', 'region focus'); + o.addClass('focus'); }); } this.currentHighlight = line; @@ -638,11 +626,14 @@ define([ } _switchTheme(name) { - const oldTheme = this.theme; - this.theme = this.getThemeNamed(name); + const oldThemeBuilder = this.themeBuilder; + this.themeBuilder = this.getThemeNamed(name); + if(this.themeBuilder !== oldThemeBuilder) { + this.theme = this.themeBuilder.build(this.svg); + } this.theme.reset(); - return (this.theme !== oldTheme); + return (this.themeBuilder !== oldThemeBuilder); } optimisedRenderPreReflow(sequence) { @@ -656,7 +647,7 @@ define([ attrs: this.theme.titleAttrs, formatted: sequence.meta.title, }); - this.sizer.expectMeasure(this.title); + this.svg.textSizer.expectMeasure(this.title); this.minX = 0; this.maxX = 0; @@ -665,11 +656,11 @@ define([ sequence.stages.forEach(this.prepareMeasurementsStage); this._resetState(); - this.sizer.performMeasurementsPre(); + this.svg.textSizer.performMeasurementsPre(); } optimisedRenderReflow() { - this.sizer.performMeasurementsAct(); + this.svg.textSizer.performMeasurementsAct(); } optimisedRenderPostReflow(sequence) { @@ -689,8 +680,8 @@ define([ this.currentHighlight = -1; this.setHighlight(prevHighlight); - this.sizer.performMeasurementsPost(); - this.sizer.resetCache(); + this.svg.textSizer.performMeasurementsPost(); + this.svg.textSizer.resetCache(); } render(sequence) { @@ -721,8 +712,8 @@ define([ return this.agentInfos.get(id).x; } - svg() { - return this.base; + dom() { + return this.svg.body.element; } }; }); diff --git a/scripts/sequence/Renderer_spec.js b/scripts/sequence/Renderer_spec.js index b546ec7..b0c8bd6 100644 --- a/scripts/sequence/Renderer_spec.js +++ b/scripts/sequence/Renderer_spec.js @@ -10,17 +10,20 @@ defineDescribe('Sequence Renderer', [ let renderer = null; beforeEach(() => { - renderer = new Renderer({themes: [new BasicTheme()]}); - document.body.appendChild(renderer.svg()); + renderer = new Renderer({ + themes: [new BasicTheme.Factory()], + document: window.document, + }); + document.body.appendChild(renderer.dom()); }); afterEach(() => { - document.body.removeChild(renderer.svg()); + document.body.removeChild(renderer.dom()); }); - describe('.svg', () => { + describe('.dom', () => { it('returns an SVG node containing the rendered diagram', () => { - const svg = renderer.svg(); + const svg = renderer.dom(); expect(svg.tagName).toEqual('svg'); }); }); @@ -76,7 +79,7 @@ defineDescribe('Sequence Renderer', [ ], stages: [], }); - const element = renderer.svg(); + const element = renderer.dom(); const title = element.getElementsByClassName('title')[0]; expect(title.innerHTML).toEqual('Title'); }); @@ -99,7 +102,7 @@ defineDescribe('Sequence Renderer', [ ], stages: [], }); - const element = renderer.svg(); + const element = renderer.dom(); const metadata = element.getElementsByTagName('metadata')[0]; expect(metadata.innerHTML).toEqual('hello'); }); @@ -141,7 +144,7 @@ defineDescribe('Sequence Renderer', [ ], }); - const element = renderer.svg(); + const element = renderer.dom(); const line = element.getElementsByClassName('agent-1-line')[0]; const drawnX = Number(line.getAttribute('x1')); diff --git a/scripts/sequence/SequenceDiagram.js b/scripts/sequence/SequenceDiagram.js index 6f1e4d2..8656a39 100644 --- a/scripts/sequence/SequenceDiagram.js +++ b/scripts/sequence/SequenceDiagram.js @@ -28,11 +28,11 @@ define([ 'use strict'; const themes = [ - new BasicTheme(), - new MonospaceTheme(), - new ChunkyTheme(), - new SketchTheme(SketchTheme.RIGHT), - new SketchTheme(SketchTheme.LEFT), + new BasicTheme.Factory(), + new MonospaceTheme.Factory(), + new ChunkyTheme.Factory(), + new SketchTheme.Factory(SketchTheme.RIGHT), + new SketchTheme.Factory(SketchTheme.LEFT), ]; const SharedParser = new Parser(); @@ -83,6 +83,14 @@ define([ } } + function pickDocument(container) { + if(container) { + return container.ownerDocument; + } else { + return window.document; + } + } + class SequenceDiagram extends EventObject { constructor(code = null, options = {}) { super(); @@ -92,16 +100,24 @@ define([ code = options.code; } - this.registerCodeMirrorMode = registerCodeMirrorMode; + Object.assign(this, { + code, + latestProcessed: null, + isInteractive: false, + textSizerFactory: options.textSizerFactory || null, + registerCodeMirrorMode, + + parser: SharedParser, + generator: SharedGenerator, + renderer: new Renderer(Object.assign({ + themes, + document: pickDocument(options.container), + }, options)), + exporter: new Exporter(), + }); - this.code = code; - this.parser = SharedParser; - this.generator = SharedGenerator; - this.renderer = new Renderer(Object.assign({themes}, options)); - this.exporter = new Exporter(); this.renderer.addEventForwarding(this); - this.latestProcessed = null; - this.isInteractive = false; + if(options.container) { options.container.appendChild(this.dom()); } @@ -114,6 +130,8 @@ define([ } clone(options = {}) { + const reference = (options.container || this.renderer.dom()); + return new SequenceDiagram(Object.assign({ code: this.code, container: null, @@ -121,7 +139,8 @@ define([ namespace: null, components: this.renderer.components, interactive: this.isInteractive, - SVGTextBlockClass: this.renderer.SVGTextBlockClass, + document: reference.ownerDocument, + textSizerFactory: this.textSizerFactory, }, options)); } @@ -232,9 +251,9 @@ define([ } _revertParent(state) { - const dom = this.renderer.svg(); + const dom = this.renderer.dom(); if(dom.parentNode !== state.originalParent) { - document.body.removeChild(dom); + dom.parentNode.removeChild(dom); if(state.originalParent) { state.originalParent.appendChild(dom); } @@ -248,7 +267,7 @@ define([ } optimisedRenderPreReflow(processed = null) { - const dom = this.renderer.svg(); + const dom = this.renderer.dom(); this.renderState = { originalParent: dom.parentNode, processed, @@ -256,11 +275,11 @@ define([ }; const state = this.renderState; - if(!document.body.contains(dom)) { + if(!dom.isConnected) { if(state.originalParent) { state.originalParent.removeChild(dom); } - document.body.appendChild(dom); + dom.ownerDocument.body.appendChild(dom); } try { @@ -350,7 +369,7 @@ define([ } dom() { - return this.renderer.svg(); + return this.renderer.dom(); } } @@ -381,8 +400,6 @@ define([ Object.assign(tagOptions, options) ); const newElement = diagram.dom(); - element.parentNode.insertBefore(newElement, element); - element.parentNode.removeChild(element); const attrs = element.attributes; for(let i = 0; i < attrs.length; ++ i) { newElement.setAttribute( @@ -390,6 +407,7 @@ define([ attrs[i].nodeValue ); } + element.parentNode.replaceChild(newElement, element); return diagram; } diff --git a/scripts/sequence/SequenceDiagram_spec.js b/scripts/sequence/SequenceDiagram_spec.js index db77e05..a406ae0 100644 --- a/scripts/sequence/SequenceDiagram_spec.js +++ b/scripts/sequence/SequenceDiagram_spec.js @@ -5,14 +5,14 @@ defineDescribe('SequenceDiagram', [ './Generator', './Renderer', './Exporter', - 'stubs/SVGTextBlock', + 'stubs/TestDOM', ], ( SequenceDiagram, Parser, Generator, Renderer, Exporter, - SVGTextBlock + TestDOM ) => { /* jshint +W072 */ 'use strict'; @@ -30,7 +30,7 @@ defineDescribe('SequenceDiagram', [ beforeEach(() => { diagram = new SequenceDiagram({ namespace: '', - SVGTextBlockClass: SVGTextBlock, + textSizerFactory: TestDOM.textSizerFactory, }); }); @@ -87,6 +87,7 @@ defineDescribe('SequenceDiagram', [ '' + '' + '' + + '' + 'My title here' + + ' y="-10">My title here' + + '' + '' ); }); @@ -110,10 +112,12 @@ defineDescribe('SequenceDiagram', [ // Agent 1 expect(content).toContain( - ''); - expect(content).toContain('