diff --git a/lib/sequence-diagram.js b/lib/sequence-diagram.js index 4d96421..a8a292a 100644 --- a/lib/sequence-diagram.js +++ b/lib/sequence-diagram.js @@ -3387,13 +3387,17 @@ define('svg/SVGUtilities',[],() => { return document.createTextNode(text); } - function make(type, attrs = {}, children = []) { - const o = document.createElementNS(NS, type); + function setAttributes(target, attrs) { for(const k in attrs) { if(attrs.hasOwnProperty(k)) { - o.setAttribute(k, attrs[k]); + target.setAttribute(k, attrs[k]); } } + } + + function make(type, attrs = {}, children = []) { + const o = document.createElementNS(NS, type); + setAttributes(o, attrs); for(const c of children) { o.appendChild(c); } @@ -3417,6 +3421,7 @@ define('svg/SVGUtilities',[],() => { makeText, make, makeContainer, + setAttributes, empty, }; }); @@ -3461,6 +3466,27 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { }); } + function measureLine(tester, line) { + if(!line.length) { + return 0; + } + + const labelKey = JSON.stringify(line); + const knownWidth = tester.widths.get(labelKey); + if(knownWidth !== undefined) { + return knownWidth; + } + + // getComputedTextLength forces a reflow, so only call it if nothing + // else can tell us the length + + svg.empty(tester.node); + populateSvgTextLine(tester.node, line); + const width = tester.node.getComputedTextLength(); + tester.widths.set(labelKey, width); + return width; + } + const EMPTY = []; class SVGTextBlock { @@ -3472,8 +3498,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { x: 0, y: 0, }; - this.width = 0; - this.height = 0; this.lines = []; this.set(initialState); } @@ -3499,8 +3523,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { _reset() { this._rebuildLines(0); - this.width = 0; - this.height = 0; } _renderText() { @@ -3516,7 +3538,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { this._rebuildLines(formatted.length); - let maxWidth = 0; this.lines.forEach((ln, i) => { const id = JSON.stringify(formatted[i]); if(id !== ln.latest) { @@ -3524,9 +3545,7 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { populateSvgTextLine(ln.node, formatted[i]); ln.latest = id; } - maxWidth = Math.max(maxWidth, ln.node.getComputedTextLength()); }); - this.width = maxWidth; } _updateX() { @@ -3540,7 +3559,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { this.lines.forEach(({node}, i) => { node.setAttribute('y', this.state.y + i * lineHeight + size); }); - this.height = lineHeight * this.lines.length; } firstLine() { @@ -3587,47 +3605,62 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => { this.cache = new Map(); } - measure(attrs, formatted) { - if(!formatted || !formatted.length) { - return {width: 0, height: 0}; - } - if(!Array.isArray(formatted)) { - throw new Error('Invalid formatted text: ' + formatted); + _measureHeight({attrs, formatted}) { + return formatted.length * fontDetails(attrs).lineHeight; + } + + _measureWidth({attrs, formatted}) { + if(!formatted.length) { + return 0; } - let tester = this.cache.get(attrs); + const attrKey = JSON.stringify(attrs); + let tester = this.cache.get(attrKey); if(!tester) { - tester = svg.make('text', attrs); - this.testers.appendChild(tester); - this.cache.set(attrs, tester); + const node = svg.make('text', attrs); + this.testers.appendChild(node); + tester = { + node, + widths: new Map(), + }; + this.cache.set(attrKey, tester); } if(!this.testers.parentNode) { this.container.appendChild(this.testers); } - let width = 0; - formatted.forEach((line) => { - svg.empty(tester); - populateSvgTextLine(tester, line); - width = Math.max(width, tester.getComputedTextLength()); - }); + return (formatted + .map((line) => measureLine(tester, line)) + .reduce((a, b) => Math.max(a, b), 0) + ); + } + _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}; + } + + measure(attrs, formatted) { + const opts = this._getMeasurementOpts(attrs, formatted); return { - width, - height: formatted.length * fontDetails(attrs).lineHeight, + width: this._measureWidth(opts), + height: this._measureHeight(opts), }; } measureHeight(attrs, formatted) { - if(!formatted) { - return 0; - } - if(!Array.isArray(formatted)) { - throw new Error('Invalid formatted text: ' + formatted); - } - - return formatted.length * fontDetails(attrs).lineHeight; + const opts = this._getMeasurementOpts(attrs, formatted); + return this._measureHeight(opts); } resetCache() { @@ -3838,6 +3871,7 @@ define('svg/SVGShapes',[ labelLayer, boxRenderer = null, SVGTextBlockClass = SVGTextBlock, + textSizer, }) { if(!formatted || !formatted.length) { return {width: 0, height: 0, label: null, box: null}; @@ -3852,20 +3886,21 @@ define('svg/SVGShapes',[ y: y + padding.top, }); - const width = (label.width + padding.left + padding.right); - const height = (label.height + padding.top + padding.bottom); + const size = textSizer.measure(label); + const width = (size.width + padding.left + padding.right); + const height = (size.height + padding.top + padding.bottom); let box = null; if(boxRenderer) { box = boxRenderer({ - 'x': anchorX - label.width * shift - padding.left, + 'x': anchorX - size.width * shift - padding.left, 'y': y, 'width': width, 'height': height, }); } else { box = renderBox(boxAttrs, { - 'x': anchorX - label.width * shift - padding.left, + 'x': anchorX - size.width * shift - padding.left, 'y': y, 'width': width, 'height': height, @@ -4064,6 +4099,7 @@ define('sequence/components/Block',[ boxLayer: blockInfo.hold, labelLayer: clickable, SVGTextBlockClass: env.SVGTextBlockClass, + textSizer: env.textSizer, }); const labelRender = SVGShapes.renderBoxedText(label, { @@ -4075,6 +4111,7 @@ define('sequence/components/Block',[ boxLayer: env.lineMaskLayer, labelLayer: clickable, SVGTextBlockClass: env.SVGTextBlockClass, + textSizer: env.textSizer, }); const labelHeight = Math.max( @@ -4434,6 +4471,7 @@ define('sequence/components/AgentCap',[ boxLayer: clickable, labelLayer: clickable, SVGTextBlockClass: env.SVGTextBlockClass, + textSizer: env.textSizer, }); clickable.insertBefore(svg.make('rect', { 'x': x - text.width / 2, @@ -5003,6 +5041,7 @@ define('sequence/components/Connect',[ boxLayer: env.lineMaskLayer, labelLayer: clickable, SVGTextBlockClass: env.SVGTextBlockClass, + textSizer: env.textSizer, }); const labelW = (label ? ( renderedText.width + @@ -5129,6 +5168,7 @@ define('sequence/components/Connect',[ boxLayer: env.lineMaskLayer, labelLayer, SVGTextBlockClass: env.SVGTextBlockClass, + textSizer: env.textSizer, }); } @@ -5348,15 +5388,16 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base formatted: label, y, }); + const size = env.textSizer.measure(labelNode); const fullW = ( - labelNode.width + + size.width + config.padding.left + config.padding.right ); const fullH = ( config.padding.top + - labelNode.height + + size.height + config.padding.bottom ); if(x0 === null && xMid !== null) { @@ -5638,6 +5679,7 @@ define('sequence/components/Divider',[ boxLayer: env.fullMaskLayer, labelLayer: clickable, SVGTextBlockClass: env.SVGTextBlockClass, + textSizer: env.textSizer, }); labelWidth = boxed.width; } @@ -6171,12 +6213,13 @@ define('sequence/Renderer',[ updateBounds(stagesHeight) { const cx = (this.minX + this.maxX) / 2; - const titleY = ((this.title.height > 0) ? - (-this.theme.titleMargin - this.title.height) : 0 + const titleSize = this.sizer.measure(this.title); + const titleY = ((titleSize.height > 0) ? + (-this.theme.titleMargin - titleSize.height) : 0 ); this.title.set({x: cx, y: titleY}); - const halfTitleWidth = this.title.width / 2; + const halfTitleWidth = titleSize.width / 2; const margin = this.theme.outerMargin; const x0 = Math.min(this.minX, cx - halfTitleWidth) - margin; const x1 = Math.max(this.maxX, cx + halfTitleWidth) + margin; @@ -6186,15 +6229,15 @@ define('sequence/Renderer',[ this.width = x1 - x0; this.height = y1 - y0; - this.fullMaskReveal.setAttribute('x', x0); - this.fullMaskReveal.setAttribute('y', y0); - this.fullMaskReveal.setAttribute('width', this.width); - this.fullMaskReveal.setAttribute('height', this.height); + const fullSize = { + 'x': x0, + 'y': y0, + 'width': this.width, + 'height': this.height, + }; - this.lineMaskReveal.setAttribute('x', x0); - this.lineMaskReveal.setAttribute('y', y0); - this.lineMaskReveal.setAttribute('width', this.width); - this.lineMaskReveal.setAttribute('height', this.height); + svg.setAttributes(this.fullMaskReveal, fullSize); + svg.setAttributes(this.lineMaskReveal, fullSize); this.base.setAttribute('viewBox', ( x0 + ' ' + y0 + ' ' + diff --git a/lib/sequence-diagram.min.js b/lib/sequence-diagram.min.js index dd12e1a..1140a75 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 x.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],y[e]=!0,g.apply(r,t)}if(!s(f,e)&&!s(y,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={},y={},x=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,x,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,x=0;x{"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){return e.v===t.v&&e.prefix===t.prefix&&e.suffix===t.suffix&&e.q===t.q}function n(t,n,r){let s=r.suggest;return Array.isArray(s)||(s=[s]),e.flatMap(s,e=>!0===e?[function(e,t){return Object.keys(t.then).length>0?{v:e,suffix:" ",q:!1}:{v:e,suffix:"\n",q:!1}}(n,r)]:"object"==typeof e?e.known?t["known"+e.known]||[]:[e]:"string"==typeof e&&e?[{v:e,q:""===n}]:[])}function r(r,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,n(r,o,l),t)}),i}function s(n,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(n["known"+r.type],[{v:r.value,suffix:" ",q:!0}],t),r.type="",r.value=""),o&&(r.type=o,r.value&&(r.value+=s.s),r.value+=s.v)}function i(t,n,i){const a={type:"",value:""};let l=i;const h=[l];return t.line.forEach((n,i)=>{i===t.line.length-1&&(t.completions=r(t,h));const d=n.q?"":n.v;let g=l.then[d];void 0===g?(g=l.then[""],t.isVar=!0):t.isVar=n.q,"number"==typeof g?h.length-=g:h.push(g||o),l=e.last(h),s(t,a,n,l)}),n&&s(t,a,null,{}),t.nextCompletions=r(t,h),t.valid=Boolean(l.then["\n"])||0===Object.keys(l.then).length,l.type}function a(e){const t=e.baseToken||{};return{value:t.v||"",quoted:t.q||!1}}const o={type:"error line-error",then:{"":0}},l=(()=>{function e(e,t){return{type:"string",suggest:t,then:Object.assign({"":0},e)}}function t(e){return{type:"variable",suggest:{known:"Agent"},then:Object.assign({},e,{"":0,",":{type:"operator",suggest:!0,then:{"":1}}})}}function n(e){return{type:"keyword",suggest:[e+" of ",e+": "],then:{of:{type:"keyword",suggest:!0,then:{"":d}},":":{type:"operator",suggest:!0,then:{"":a}},"":d}}}function r(e,t){const n={type:"operator",suggest:!0,then:{"+":o,"-":o,"*":o,"!":o,"":e}};return{"+":{type:"operator",suggest:!0,then:{"+":o,"-":o,"*":n,"!":o,"":e}},"-":{type:"operator",suggest:!0,then:{"+":o,"-":o,"*":n,"!":{type:"operator",then:{"+":o,"-":o,"*":o,"!":o,"":e}},"":e}},"*":{type:"operator",suggest:!0,then:Object.assign({"+":n,"-":n,"*":o,"!":o,"":e},t)},"!":n,"":e}}const s={type:"",suggest:"\n",then:{}},i={type:"",then:{}},a=e({"\n":s}),l={type:"variable",suggest:{known:"Agent"},then:{"":0,"\n":s,",":{type:"operator",suggest:!0,then:{"":1}},as:{type:"keyword",suggest:!0,then:{"":{type:"variable",suggest:{known:"Agent"},then:{"":0,",":{type:"operator",suggest:!0,then:{"":3}},"\n":s}}}}}},h={type:"operator",suggest:!0,then:{"":a,"\n":i}},d=t({":":h}),g={type:"variable",suggest:{known:"Agent"},then:{"":0,",":{type:"operator",suggest:!0,then:{"":d}},":":o}},c={type:"variable",suggest:{known:"Agent"},then:{"":0,",":o,":":h}},u={type:"variable",suggest:{known:"Agent"},then:{"":0,":":{type:"operator",suggest:!0,then:{"":a,"\n":i}},"\n":s}},p={":":{type:"operator",suggest:!0,then:{"":e({as:{type:"keyword",suggest:!0,then:{"":{type:"variable",suggest:{known:"Agent"},then:{"":0,"\n":s}}}}})}}},f={type:"keyword",suggest:!0,then:Object.assign({over:{type:"keyword",suggest:!0,then:{"":t(p)}}},p)},m={"\n":s,":":{type:"operator",suggest:!0,then:{"":a,"\n":i}},with:{type:"keyword",suggest:["with height "],then:{height:{type:"keyword",suggest:!0,then:{"":{type:"number",suggest:["6 ","30 "],then:{"\n":s,":":{type:"operator",suggest:!0,then:{"":a,"\n":i}}}}}}}}},b={type:"keyword",suggest:!0,then:{"":a,":":{type:"operator",suggest:!0,then:{"":a}},"\n":s}},y={title:{type:"keyword",suggest:!0,then:{"":a}},theme:{type:"keyword",suggest:!0,then:{"":{type:"string",suggest:{global:"themes",suffix:"\n"},then:{"":0,"\n":s}}}},headers:{type:"keyword",suggest:!0,then:{none:{type:"keyword",suggest:!0,then:{}},cross:{type:"keyword",suggest:!0,then:{}},box:{type:"keyword",suggest:!0,then:{}},fade:{type:"keyword",suggest:!0,then:{}},bar:{type:"keyword",suggest:!0,then:{}}}},terminators:{type:"keyword",suggest:!0,then:{none:{type:"keyword",suggest:!0,then:{}},cross:{type:"keyword",suggest:!0,then:{}},box:{type:"keyword",suggest:!0,then:{}},fade:{type:"keyword",suggest:!0,then:{}},bar:{type:"keyword",suggest:!0,then:{}}}},divider:{type:"keyword",suggest:!0,then:Object.assign({line:{type:"keyword",suggest:!0,then:m},space:{type:"keyword",suggest:!0,then:m},delay:{type:"keyword",suggest:!0,then:m},tear:{type:"keyword",suggest:!0,then:m}},m)},define:{type:"keyword",suggest:!0,then:{"":l,as:o}},begin:{type:"keyword",suggest:!0,then:{"":l,reference:f,as:o}},end:{type:"keyword",suggest:!0,then:{"":l,as:o,"\n":s}},if:b,else:{type:"keyword",suggest:["else\n","else if: "],then:{if:{type:"keyword",suggest:"if: ",then:{"":a,":":{type:"operator",suggest:!0,then:{"":a}}}},"\n":s}},repeat:b,group:b,note:{type:"keyword",suggest:!0,then:{over:{type:"keyword",suggest:!0,then:{"":d}},left:n("left"),right:n("right"),between:{type:"keyword",suggest:!0,then:{"":g}}}},state:{type:"keyword",suggest:"state over ",then:{over:{type:"keyword",suggest:!0,then:{"":c}}}},text:{type:"keyword",suggest:!0,then:{left:n("left"),right:n("right")}},autolabel:{type:"keyword",suggest:!0,then:{off:{type:"keyword",suggest:!0,then:{}},"":e({"\n":s},[{v:"