Multiline text everywhere

This commit is contained in:
David Evans 2017-10-28 00:09:54 +01:00
parent 3ca6857ee1
commit 3fd9d4eb5f
15 changed files with 682 additions and 329 deletions

View File

@ -29,6 +29,8 @@ Gremlin -> Bowie: Do what?
Bowie -> Gremlin: Remind me of the babe! Bowie -> Gremlin: Remind me of the babe!
Bowie -> Audience: Sings Bowie -> Audience: Sings
terminators box
``` ```
### Connection Types ### Connection Types
@ -62,12 +64,13 @@ Foo <- ]: From the right
<img src="screenshots/NotesAndState.png" alt="Notes and State preview" width="150" align="right" /> <img src="screenshots/NotesAndState.png" alt="Notes and State preview" width="150" align="right" />
``` ```
title Note placements title Note Placements
note over Foo: Foo says something note over Foo: Foo says something
note left of Foo: Stuff note left of Foo: Stuff
note right of Bar: More stuff note right of Bar: More stuff
note over Foo, Bar: Foo and Bar note over Foo, Bar: "Foo and Bar
on multiple lines"
note between Foo, Bar: Link note between Foo, Bar: Link
state over Foo: Foo is ponderous state over Foo: Foo is ponderous
@ -78,7 +81,7 @@ state over Foo: Foo is ponderous
<img src="screenshots/Logic.png" alt="Logic preview" width="200" align="right" /> <img src="screenshots/Logic.png" alt="Logic preview" width="200" align="right" />
``` ```
title At the bank title At the Bank
begin Person, ATM, Bank begin Person, ATM, Bank
Person -> ATM: Request money Person -> ATM: Request money
@ -97,6 +100,25 @@ else
end end
``` ```
### Multiline Text
<img src="screenshots/MultilineText.png" alt="Multiline Text preview" width="150" align="right" />
```
title 'My Multiline
Title'
note over Foo: 'Also possible\nwith escapes'
Foo -> Bar: 'Lines of text\non this arrow'
if 'Even multiline\ninside conditions like this'
Foo -> 'Multiline\nagent'
end
state over Foo: 'Newlines here,\ntoo!'
```
### Short-Lived Agents ### Short-Lived Agents
<img src="screenshots/ShortLivedAgents.png" alt="Short Lived Agents preview" width="200" align="right" /> <img src="screenshots/ShortLivedAgents.png" alt="Short Lived Agents preview" width="200" align="right" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -2,58 +2,62 @@ define([
'./ArrayUtilities', './ArrayUtilities',
'./SVGUtilities', './SVGUtilities',
'./SVGTextBlock', './SVGTextBlock',
'./SVGShapes',
], ( ], (
array, array,
svg, svg,
SVGTextBlock SVGTextBlock,
SVGShapes
) => { ) => {
'use strict'; 'use strict';
function boxRenderer(attrs, position) {
return svg.make('rect', Object.assign({}, position, attrs));
}
function noteRenderer(attrs, flickAttrs, position) {
const g = svg.make('g');
const x0 = position.x;
const x1 = position.x + position.width;
const y0 = position.y;
const y1 = position.y + position.height;
const flick = 7;
g.appendChild(svg.make('path', Object.assign({
'd': (
'M ' + x0 + ' ' + y0 +
' L ' + (x1 - flick) + ' ' + y0 +
' L ' + x1 + ' ' + (y0 + flick) +
' L ' + x1 + ' ' + y1 +
' L ' + x0 + ' ' + y1 +
' Z'
),
}, attrs)));
g.appendChild(svg.make('path', Object.assign({
'd': (
'M ' + (x1 - flick) + ' ' + y0 +
' L ' + (x1 - flick) + ' ' + (y0 + flick) +
' L ' + x1 + ' ' + (y0 + flick)
),
}, flickAttrs)));
return g;
}
const SEP_ZERO = {left: 0, right: 0}; const SEP_ZERO = {left: 0, right: 0};
const LINE_HEIGHT = 1.3; const LINE_HEIGHT = 1.3;
const TITLE_MARGIN = 10; const TITLE_MARGIN = 10;
const OUTER_MARGIN = 5; const OUTER_MARGIN = 5;
const AGENT_BOX_PADDING = 10;
const AGENT_MARGIN = 10; const AGENT_MARGIN = 10;
const AGENT_CROSS_SIZE = 20;
const AGENT_NONE_HEIGHT = 10;
const ACTION_MARGIN = 5; const ACTION_MARGIN = 5;
const AGENT_CAP = {
box: {
padding: {
top: 5,
left: 10,
right: 10,
bottom: 5,
},
boxAttrs: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
},
labelAttrs: {
'font-family': 'sans-serif',
'font-size': 12,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle',
},
},
cross: {
size: 20,
attrs: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
},
bar: {
attrs: {
'fill': '#000000',
'height': 5,
},
},
none: {
height: 10,
},
};
const CONNECT = { const CONNECT = {
lineAttrs: { lineAttrs: {
'solid': { 'solid': {
@ -84,12 +88,18 @@ define([
attrs: { attrs: {
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-size': 8, 'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle', 'text-anchor': 'middle',
}, },
}, },
mask: { mask: {
padding: 3, padding: {
attrs: { top: 0,
left: 3,
right: 3,
bottom: 0,
},
maskAttrs: {
'fill': '#FFFFFF', 'fill': '#FFFFFF',
}, },
}, },
@ -130,24 +140,24 @@ define([
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-weight': 'bold', 'font-weight': 'bold',
'font-size': 9, 'font-size': 9,
'line-height': LINE_HEIGHT,
'text-anchor': 'left', 'text-anchor': 'left',
}, },
}, },
label: { label: {
maskPadding: { padding: {
left: 3, top: 1,
left: 5,
right: 3, right: 3,
bottom: 0,
}, },
maskAttrs: { maskAttrs: {
'fill': '#FFFFFF', 'fill': '#FFFFFF',
}, },
labelPadding: {
left: 5,
right: 5,
},
labelAttrs: { labelAttrs: {
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-size': 8, 'font-size': 8,
'line-height': LINE_HEIGHT,
'text-anchor': 'left', 'text-anchor': 'left',
}, },
}, },
@ -166,7 +176,7 @@ define([
margin: {top: 0, left: 5, right: 5, bottom: 0}, margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 5, left: 5, right: 10, bottom: 5}, padding: {top: 5, left: 5, right: 10, bottom: 5},
overlap: {left: 10, right: 10}, overlap: {left: 10, right: 10},
boxRenderer: noteRenderer.bind(null, { boxRenderer: SVGShapes.renderNote.bind(null, {
'fill': '#FFFFFF', 'fill': '#FFFFFF',
'stroke': '#000000', 'stroke': '#000000',
'stroke-width': 1, 'stroke-width': 1,
@ -178,13 +188,14 @@ define([
labelAttrs: { labelAttrs: {
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-size': 8, 'font-size': 8,
'line-height': LINE_HEIGHT,
}, },
}, },
'state': { 'state': {
margin: {top: 0, left: 5, right: 5, bottom: 0}, margin: {top: 0, left: 5, right: 5, bottom: 0},
padding: {top: 7, left: 7, right: 7, bottom: 7}, padding: {top: 7, left: 7, right: 7, bottom: 7},
overlap: {left: 10, right: 10}, overlap: {left: 10, right: 10},
boxRenderer: boxRenderer.bind(null, { boxRenderer: SVGShapes.renderBox.bind(null, {
'fill': '#FFFFFF', 'fill': '#FFFFFF',
'stroke': '#000000', 'stroke': '#000000',
'stroke-width': 1, 'stroke-width': 1,
@ -194,6 +205,7 @@ define([
labelAttrs: { labelAttrs: {
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-size': 8, 'font-size': 8,
'line-height': LINE_HEIGHT,
}, },
}, },
}; };
@ -202,6 +214,7 @@ define([
TITLE: { TITLE: {
'font-family': 'sans-serif', 'font-family': 'sans-serif',
'font-size': 20, 'font-size': 20,
'line-height': LINE_HEIGHT,
'text-anchor': 'middle', 'text-anchor': 'middle',
'class': 'title', 'class': 'title',
}, },
@ -211,28 +224,21 @@ define([
'stroke': '#000000', 'stroke': '#000000',
'stroke-width': 1, 'stroke-width': 1,
}, },
AGENT_BOX: {
'fill': '#FFFFFF',
'stroke': '#000000',
'stroke-width': 1,
'height': 24,
},
AGENT_BOX_LABEL: {
'font-family': 'sans-serif',
'font-size': 12,
'text-anchor': 'middle',
},
AGENT_CROSS: {
'fill': 'none',
'stroke': '#000000',
'stroke-width': 1,
},
AGENT_BAR: {
'fill': '#000000',
'height': 5,
},
}; };
function drawHorizontalArrowHead(container, {x, y, dx, dy, attrs}) {
container.appendChild(svg.make(
attrs.fill === 'none' ? 'polyline' : 'polygon',
Object.assign({
'points': (
(x + dx) + ' ' + (y - dy) + ' ' +
x + ' ' + y + ' ' +
(x + dx) + ' ' + (y + dy)
),
}, attrs)
));
}
function traverse(stages, callbacks) { function traverse(stages, callbacks) {
stages.forEach((stage) => { stages.forEach((stage) => {
if(stage.type === 'block') { if(stage.type === 'block') {
@ -321,19 +327,20 @@ define([
}); });
this.agentLines = svg.make('g'); this.agentLines = svg.make('g');
this.mask = svg.make('g');
this.blocks = svg.make('g'); this.blocks = svg.make('g');
this.sections = svg.make('g'); this.sections = svg.make('g');
this.agentDecor = svg.make('g'); this.actionShapes = svg.make('g');
this.actions = svg.make('g'); this.actionLabels = svg.make('g');
this.base.appendChild(this.agentLines); this.base.appendChild(this.agentLines);
this.base.appendChild(this.mask);
this.base.appendChild(this.blocks); this.base.appendChild(this.blocks);
this.base.appendChild(this.sections); this.base.appendChild(this.sections);
this.base.appendChild(this.agentDecor); this.base.appendChild(this.actionShapes);
this.base.appendChild(this.actions); this.base.appendChild(this.actionLabels);
this.title = new SVGTextBlock(this.base, ATTRS.TITLE, LINE_HEIGHT); this.title = new SVGTextBlock(this.base, ATTRS.TITLE);
this.testers = svg.make('g'); this.sizer = new SVGTextBlock.SizeTester(this.base);
this.testersCache = new Map();
} }
findExtremes(agents) { findExtremes(agents) {
@ -384,24 +391,36 @@ define([
}); });
} }
separationAgentCapBox(agentInfo) { separationAgentCapBox({label}) {
const width = (
this.sizer.measure(AGENT_CAP.box.labelAttrs, label).width +
AGENT_CAP.box.padding.left +
AGENT_CAP.box.padding.right
);
return { return {
left: agentInfo.labelWidth / 2, left: width / 2,
right: agentInfo.labelWidth / 2, right: width / 2,
}; };
} }
separationAgentCapCross() { separationAgentCapCross() {
return { return {
left: AGENT_CROSS_SIZE / 2, left: AGENT_CAP.cross.size / 2,
right: AGENT_CROSS_SIZE / 2, right: AGENT_CAP.cross.size / 2,
}; };
} }
separationAgentCapBar(agentInfo) { separationAgentCapBar({label}) {
const width = (
this.sizer.measure(AGENT_CAP.box.labelAttrs, label).width +
AGENT_CAP.box.padding.left +
AGENT_CAP.box.padding.right
);
return { return {
left: agentInfo.labelWidth / 2, left: width / 2,
right: agentInfo.labelWidth / 2, right: width / 2,
}; };
} }
@ -432,7 +451,7 @@ define([
agents[0], agents[0],
agents[1], agents[1],
this.testTextWidth(CONNECT.label.attrs, label) + this.sizer.measure(CONNECT.label.attrs, label).width +
CONNECT.arrow.width * 2 + CONNECT.arrow.width * 2 +
CONNECT.label.padding * 2 + CONNECT.label.padding * 2 +
ATTRS.AGENT_LINE['stroke-width'] ATTRS.AGENT_LINE['stroke-width']
@ -442,7 +461,7 @@ define([
separationNoteOver({agents, mode, label}) { separationNoteOver({agents, mode, label}) {
const config = NOTE[mode]; const config = NOTE[mode];
const width = ( const width = (
this.testTextWidth(config.labelAttrs, label) + this.sizer.measure(config.labelAttrs, label).width +
config.padding.left + config.padding.left +
config.padding.right config.padding.right
); );
@ -478,7 +497,7 @@ define([
const agentSpaces = new Map(); const agentSpaces = new Map();
agentSpaces.set(left, { agentSpaces.set(left, {
left: ( left: (
this.testTextWidth(config.labelAttrs, label) + this.sizer.measure(config.labelAttrs, label).width +
config.padding.left + config.padding.left +
config.padding.right + config.padding.right +
config.margin.left + config.margin.left +
@ -497,7 +516,7 @@ define([
agentSpaces.set(right, { agentSpaces.set(right, {
left: 0, left: 0,
right: ( right: (
this.testTextWidth(config.labelAttrs, label) + this.sizer.measure(config.labelAttrs, label).width +
config.padding.left + config.padding.left +
config.padding.right + config.padding.right +
config.margin.left + config.margin.left +
@ -515,7 +534,7 @@ define([
left, left,
right, right,
this.testTextWidth(config.labelAttrs, label) + this.sizer.measure(config.labelAttrs, label).width +
config.padding.left + config.padding.left +
config.padding.right + config.padding.right +
config.margin.left + config.margin.left +
@ -529,13 +548,14 @@ define([
} }
separationSectionBegin(scope, {left, right}, {mode, label}) { separationSectionBegin(scope, {left, right}, {mode, label}) {
const config = BLOCK.section;
const width = ( const width = (
this.testTextWidth(BLOCK.section.mode.labelAttrs, mode) + this.sizer.measure(config.mode.labelAttrs, mode).width +
BLOCK.section.mode.padding.left + config.mode.padding.left +
BLOCK.section.mode.padding.right + config.mode.padding.right +
this.testTextWidth(BLOCK.section.label.labelAttrs, label) + this.sizer.measure(config.label.labelAttrs, label).width +
BLOCK.section.label.labelPadding.left + config.label.padding.left +
BLOCK.section.label.labelPadding.right config.label.padding.right
); );
this.addSeparation(left, right, width); this.addSeparation(left, right, width);
} }
@ -548,42 +568,36 @@ define([
this.separationAction[stage.type](stage); this.separationAction[stage.type](stage);
} }
renderAgentCapBox({x, labelWidth, label}) { renderAgentCapBox({x, label}) {
this.agentDecor.appendChild(svg.make('rect', Object.assign({ const {height} = SVGShapes.renderBoxedText(label, {
'x': x - labelWidth / 2, x,
'y': this.currentY, y: this.currentY,
'width': labelWidth, padding: AGENT_CAP.box.padding,
}, ATTRS.AGENT_BOX))); boxAttrs: AGENT_CAP.box.boxAttrs,
labelAttrs: AGENT_CAP.box.labelAttrs,
const name = svg.make('text', Object.assign({ boxLayer: this.actionShapes,
'x': x, labelLayer: this.actionLabels,
'y': this.currentY + ( });
ATTRS.AGENT_BOX.height +
ATTRS.AGENT_BOX_LABEL['font-size'] * (2 - LINE_HEIGHT)
) / 2,
}, ATTRS.AGENT_BOX_LABEL));
name.appendChild(svg.makeText(label));
this.agentDecor.appendChild(name);
return { return {
lineTop: 0, lineTop: 0,
lineBottom: ATTRS.AGENT_BOX.height, lineBottom: height,
height: ATTRS.AGENT_BOX.height, height,
}; };
} }
renderAgentCapCross({x}) { renderAgentCapCross({x}) {
const y = this.currentY; const y = this.currentY;
const d = AGENT_CROSS_SIZE / 2; const d = AGENT_CAP.cross.size / 2;
this.agentDecor.appendChild(svg.make('path', Object.assign({ this.actionShapes.appendChild(svg.make('path', Object.assign({
'd': ( 'd': (
'M ' + (x - d) + ' ' + y + 'M ' + (x - d) + ' ' + y +
' L ' + (x + d) + ' ' + (y + d * 2) + ' L ' + (x + d) + ' ' + (y + d * 2) +
' M ' + (x + d) + ' ' + y + ' M ' + (x + d) + ' ' + y +
' L ' + (x - d) + ' ' + (y + d * 2) ' L ' + (x - d) + ' ' + (y + d * 2)
), ),
}, ATTRS.AGENT_CROSS))); }, AGENT_CAP.cross.attrs)));
return { return {
lineTop: d, lineTop: d,
@ -592,124 +606,115 @@ define([
}; };
} }
renderAgentCapBar({x, labelWidth}) { renderAgentCapBar({x, label}) {
this.agentDecor.appendChild(svg.make('rect', Object.assign({ const width = (
'x': x - labelWidth / 2, this.sizer.measure(AGENT_CAP.box.labelAttrs, label).width +
AGENT_CAP.box.padding.left +
AGENT_CAP.box.padding.right
);
this.actionShapes.appendChild(svg.make('rect', Object.assign({
'x': x - width / 2,
'y': this.currentY, 'y': this.currentY,
'width': labelWidth, 'width': width,
}, ATTRS.AGENT_BAR))); }, AGENT_CAP.bar.attrs)));
return { return {
lineTop: 0, lineTop: 0,
lineBottom: ATTRS.AGENT_BAR.height, lineBottom: AGENT_CAP.bar.attrs.height,
height: ATTRS.AGENT_BAR.height, height: AGENT_CAP.bar.attrs.height,
}; };
} }
renderAgentCapNone() { renderAgentCapNone() {
return { return {
lineTop: AGENT_NONE_HEIGHT, lineTop: AGENT_CAP.none.height,
lineBottom: 0, lineBottom: 0,
height: AGENT_NONE_HEIGHT, height: AGENT_CAP.none.height,
}; };
} }
renderAgentBegin({mode, agents}) { renderAgentBegin({mode, agents}) {
let shifts = {height: 0}; let maxHeight = 0;
agents.forEach((agent) => { agents.forEach((agent) => {
const agentInfo = this.agentInfos.get(agent); const agentInfo = this.agentInfos.get(agent);
shifts = this.renderAgentCap[mode](agentInfo); const shifts = this.renderAgentCap[mode](agentInfo);
maxHeight = Math.max(maxHeight, shifts.height);
agentInfo.latestYStart = this.currentY + shifts.lineBottom; agentInfo.latestYStart = this.currentY + shifts.lineBottom;
}); });
this.currentY += shifts.height + ACTION_MARGIN; this.currentY += maxHeight + ACTION_MARGIN;
} }
renderAgentEnd({mode, agents}) { renderAgentEnd({mode, agents}) {
let shifts = {height: 0}; let maxHeight = 0;
agents.forEach((agent) => { agents.forEach((agent) => {
const agentInfo = this.agentInfos.get(agent); const agentInfo = this.agentInfos.get(agent);
const x = agentInfo.x; const x = agentInfo.x;
shifts = this.renderAgentCap[mode](agentInfo); const shifts = this.renderAgentCap[mode](agentInfo);
this.agentLines.appendChild(svg.make('path', Object.assign({ maxHeight = Math.max(maxHeight, shifts.height);
'd': ( this.agentLines.appendChild(svg.make('line', Object.assign({
'M ' + x + ' ' + agentInfo.latestYStart + 'x1': x,
' L ' + x + ' ' + (this.currentY + shifts.lineTop) 'y1': agentInfo.latestYStart,
), 'x2': x,
'y2': this.currentY + shifts.lineTop,
'class': 'agent-' + agentInfo.index + '-line', 'class': 'agent-' + agentInfo.index + '-line',
}, ATTRS.AGENT_LINE))); }, ATTRS.AGENT_LINE)));
agentInfo.latestYStart = null; agentInfo.latestYStart = null;
}); });
this.currentY += shifts.height + ACTION_MARGIN; this.currentY += maxHeight + ACTION_MARGIN;
} }
renderConnection({label, agents, line, left, right}) { renderConnection({label, agents, line, left, right}) {
/* jshint -W074, -W071 */ // TODO: tidy this up
const from = this.agentInfos.get(agents[0]); const from = this.agentInfos.get(agents[0]);
const to = this.agentInfos.get(agents[1]); const to = this.agentInfos.get(agents[1]);
const dy = CONNECT.arrow.height / 2; const dy = CONNECT.arrow.height / 2;
const dx = CONNECT.arrow.width;
const dir = (from.x < to.x) ? 1 : -1; const dir = (from.x < to.x) ? 1 : -1;
const short = ATTRS.AGENT_LINE['stroke-width']; const short = ATTRS.AGENT_LINE['stroke-width'];
let y = this.currentY;
if(label) { const height = (
const mask = svg.make('rect', CONNECT.mask.attrs); this.sizer.measureHeight(CONNECT.label.attrs, label) +
const labelNode = svg.make('text', CONNECT.label.attrs);
labelNode.appendChild(svg.makeText(label));
const sz = CONNECT.label.attrs['font-size'];
this.actions.appendChild(mask);
this.actions.appendChild(labelNode);
y += Math.max(
dy,
CONNECT.label.margin.top + CONNECT.label.margin.top +
sz * LINE_HEIGHT +
CONNECT.label.margin.bottom CONNECT.label.margin.bottom
); );
const w = labelNode.getComputedTextLength();
const x = (from.x + to.x) / 2;
const yBase = (
y -
sz * (LINE_HEIGHT - 1) -
CONNECT.label.margin.bottom
);
labelNode.setAttribute('x', x);
labelNode.setAttribute('y', yBase);
mask.setAttribute('x', x - w / 2 - CONNECT.mask.padding);
mask.setAttribute('y', yBase - sz);
mask.setAttribute('width', w + CONNECT.mask.padding * 2);
mask.setAttribute('height', sz * LINE_HEIGHT);
} else {
y += dy;
}
this.actions.appendChild(svg.make('path', Object.assign({ let y = this.currentY + Math.max(dy, height);
'd': (
'M ' + (from.x + (left ? short : 0) * dir) + ' ' + y + SVGShapes.renderBoxedText(label, {
' L ' + (to.x - (right ? short : 0) * dir) + ' ' + y x: (from.x + to.x) / 2,
), y: y - height + CONNECT.label.margin.top,
padding: CONNECT.mask.padding,
boxAttrs: CONNECT.mask.maskAttrs,
labelAttrs: CONNECT.label.attrs,
boxLayer: this.mask,
labelLayer: this.actionLabels,
});
this.actionShapes.appendChild(svg.make('line', Object.assign({
'x1': from.x + (left ? short : 0) * dir,
'y1': y,
'x2': to.x - (right ? short : 0) * dir,
'y2': y,
}, CONNECT.lineAttrs[line]))); }, CONNECT.lineAttrs[line])));
if(left) { if(left) {
this.actions.appendChild(svg.make('path', Object.assign({ drawHorizontalArrowHead(this.actionShapes, {
'd': ( x: from.x + short * dir,
'M ' + (from.x + (dx + short) * dir) + ' ' + (y - dy) + y,
' L ' + (from.x + short * dir) + ' ' + y + dx: CONNECT.arrow.width * dir,
' L ' + (from.x + (dx + short) * dir) + ' ' + (y + dy) + dy,
(CONNECT.arrow.attrs.fill === 'none' ? '' : ' Z') attrs: CONNECT.arrow.attrs,
), });
}, CONNECT.arrow.attrs)));
} }
if(right) { if(right) {
this.actions.appendChild(svg.make('path', Object.assign({ drawHorizontalArrowHead(this.actionShapes, {
'd': ( x: to.x - short * dir,
'M ' + (to.x - (dx + short) * dir) + ' ' + (y - dy) + y,
' L ' + (to.x - short * dir) + ' ' + y + dx: -CONNECT.arrow.width * dir,
' L ' + (to.x - (dx + short) * dir) + ' ' + (y + dy) + dy,
(CONNECT.arrow.attrs.fill === 'none' ? '' : ' Z') attrs: CONNECT.arrow.attrs,
), });
}, CONNECT.arrow.attrs)));
} }
this.currentY = y + dy + ACTION_MARGIN; this.currentY = y + dy + ACTION_MARGIN;
@ -718,19 +723,25 @@ define([
renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) { renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) {
const config = NOTE[mode]; const config = NOTE[mode];
const sz = config.labelAttrs['font-size'];
this.currentY += config.margin.top; this.currentY += config.margin.top;
const labelNode = svg.make('text', Object.assign({ const y = this.currentY + config.padding.top;
'y': this.currentY + config.padding.top + sz, const labelNode = new SVGTextBlock(
'text-anchor': anchor, this.actionLabels,
}, config.labelAttrs)); config.labelAttrs,
labelNode.appendChild(svg.makeText(label)); {text: label, y}
this.actions.appendChild(labelNode); );
const w = labelNode.getComputedTextLength(); const fullW = (
const fullW = w + config.padding.left + config.padding.right; labelNode.width +
config.padding.left +
config.padding.right
);
const fullH = (
config.padding.top +
labelNode.height +
config.padding.bottom
);
if(x0 === null && xMid !== null) { if(x0 === null && xMid !== null) {
x0 = xMid - fullW / 2; x0 = xMid - fullW / 2;
} }
@ -739,35 +750,30 @@ define([
} else if(x0 === null) { } else if(x0 === null) {
x0 = x1 - fullW; x0 = x1 - fullW;
} }
switch(anchor) { switch(config.labelAttrs['text-anchor']) {
case 'start': case 'middle':
labelNode.setAttribute('x', x0 + config.padding.left); labelNode.reanchor((
break;
case 'end':
labelNode.setAttribute('x', x1 - config.padding.right);
break;
default:
labelNode.setAttribute('x', (
x0 + config.padding.left + x0 + config.padding.left +
x1 - config.padding.right x1 - config.padding.right
) / 2); ) / 2, y);
break;
case 'end':
labelNode.reanchor(x1 - config.padding.right, y);
break;
default:
labelNode.reanchor(x0 + config.padding.left, y);
break;
} }
this.actions.insertBefore(config.boxRenderer({ this.actionShapes.appendChild(config.boxRenderer({
x: x0, x: x0,
y: this.currentY, y: this.currentY,
width: x1 - x0, width: x1 - x0,
height: ( height: fullH,
config.padding.top + }));
sz * LINE_HEIGHT +
config.padding.bottom
),
}), labelNode);
this.currentY += ( this.currentY += (
config.padding.top + fullH +
sz * LINE_HEIGHT +
config.padding.bottom +
config.margin.bottom + config.margin.bottom +
ACTION_MARGIN ACTION_MARGIN
); );
@ -822,8 +828,6 @@ define([
} }
renderSectionBegin(scope, {left, right}, {mode, label}) { renderSectionBegin(scope, {left, right}, {mode, label}) {
/* jshint -W071 */ // TODO: tidy this up (split text rendering)
const agentInfoL = this.agentInfos.get(left); const agentInfoL = this.agentInfos.get(left);
const agentInfoR = this.agentInfos.get(right); const agentInfoR = this.agentInfos.get(right);
@ -831,71 +835,38 @@ define([
scope.first = false; scope.first = false;
} else { } else {
this.currentY += BLOCK.section.padding.bottom; this.currentY += BLOCK.section.padding.bottom;
this.sections.appendChild(svg.make('path', Object.assign({ this.sections.appendChild(svg.make('line', Object.assign({
'd': ( 'x1': agentInfoL.x,
'M' + agentInfoL.x + ' ' + this.currentY + 'y1': this.currentY,
' L' + agentInfoR.x + ' ' + this.currentY 'x2': agentInfoR.x,
), 'y2': this.currentY,
}, BLOCK.separator.attrs))); }, BLOCK.separator.attrs)));
} }
let x = agentInfoL.x; const modeRender = SVGShapes.renderBoxedText(mode, {
if(mode) { x: agentInfoL.x,
const sz = BLOCK.section.mode.labelAttrs['font-size']; y: this.currentY,
const modeBox = svg.make('rect', Object.assign({ padding: BLOCK.section.mode.padding,
'x': x, boxAttrs: BLOCK.section.mode.boxAttrs,
'y': this.currentY, labelAttrs: BLOCK.section.mode.labelAttrs,
'height': ( boxLayer: this.blocks,
sz * LINE_HEIGHT + labelLayer: this.actionLabels,
BLOCK.section.mode.padding.top + });
BLOCK.section.mode.padding.bottom
), const labelRender = SVGShapes.renderBoxedText(label, {
}, BLOCK.section.mode.boxAttrs)); x: agentInfoL.x + modeRender.width,
const modeLabel = svg.make('text', Object.assign({ y: this.currentY,
'x': x + BLOCK.section.mode.padding.left, padding: BLOCK.section.label.padding,
'y': ( boxAttrs: BLOCK.section.label.maskAttrs,
this.currentY + sz + labelAttrs: BLOCK.section.label.labelAttrs,
BLOCK.section.mode.padding.top boxLayer: this.mask,
), labelLayer: this.actionLabels,
}, BLOCK.section.mode.labelAttrs)); });
modeLabel.appendChild(svg.makeText(mode));
this.blocks.appendChild(modeBox); this.currentY += (
this.actions.appendChild(modeLabel); Math.max(modeRender.height, labelRender.height) +
const w = ( BLOCK.section.padding.top
modeLabel.getComputedTextLength() +
BLOCK.section.mode.padding.left +
BLOCK.section.mode.padding.right
); );
modeBox.setAttribute('width', w);
x += w;
this.currentY += sz * LINE_HEIGHT;
}
if(label) {
x += BLOCK.section.label.labelPadding.left;
const sz = BLOCK.section.label.labelAttrs['font-size'];
const mask = svg.make('rect', Object.assign({
'x': x - BLOCK.section.label.maskPadding.left,
'y': this.currentY - sz * LINE_HEIGHT,
'height': sz * LINE_HEIGHT,
}, BLOCK.section.label.maskAttrs));
const labelLabel = svg.make('text', Object.assign({
'x': x,
'y': this.currentY - sz * (LINE_HEIGHT - 1),
}, BLOCK.section.label.labelAttrs));
labelLabel.appendChild(svg.makeText(label));
this.actions.appendChild(mask);
this.actions.appendChild(labelLabel);
const w = (
labelLabel.getComputedTextLength() +
BLOCK.section.label.maskPadding.left +
BLOCK.section.label.maskPadding.right
);
mask.setAttribute('width', w);
}
this.currentY += BLOCK.section.padding.top;
} }
renderSectionEnd(/*scope, block, section*/) { renderSectionEnd(/*scope, block, section*/) {
@ -920,46 +891,20 @@ define([
this.renderAction[stage.type](stage); this.renderAction[stage.type](stage);
} }
testTextWidth(attrs, content) {
let tester = this.testersCache.get(attrs);
if(!tester) {
const text = svg.makeText();
const node = svg.make('text', attrs);
node.appendChild(text);
this.testers.appendChild(node);
tester = {text, node};
this.testersCache.set(attrs, tester);
}
tester.text.nodeValue = content;
return tester.node.getComputedTextLength();
}
buildAgentInfos(agents, stages) { buildAgentInfos(agents, stages) {
svg.empty(this.testers);
this.testersCache.clear();
this.base.appendChild(this.testers);
this.agentInfos = new Map(); this.agentInfos = new Map();
agents.forEach((agent, index) => { agents.forEach((agent, index) => {
this.agentInfos.set(agent, { this.agentInfos.set(agent, {
label: agent, label: agent,
labelWidth: (
this.testTextWidth(ATTRS.AGENT_BOX_LABEL, agent) +
AGENT_BOX_PADDING * 2
),
index, index,
x: null, x: null,
latestYStart: null, latestYStart: null,
separations: new Map(), separations: new Map(),
}); });
}); });
this.agentInfos.get('[').labelWidth = 0;
this.agentInfos.get(']').labelWidth = 0;
this.visibleAgents = ['[', ']']; this.visibleAgents = ['[', ']'];
traverse(stages, this.separationTraversalFns); traverse(stages, this.separationTraversalFns);
this.base.removeChild(this.testers);
agents.forEach((agent) => { agents.forEach((agent) => {
const agentInfo = this.agentInfos.get(agent); const agentInfo = this.agentInfos.get(agent);
@ -999,10 +944,11 @@ define([
render({meta, agents, stages}) { render({meta, agents, stages}) {
svg.empty(this.agentLines); svg.empty(this.agentLines);
svg.empty(this.mask);
svg.empty(this.blocks); svg.empty(this.blocks);
svg.empty(this.sections); svg.empty(this.sections);
svg.empty(this.agentDecor); svg.empty(this.actionShapes);
svg.empty(this.actions); svg.empty(this.actionLabels);
this.title.setText(meta.title); this.title.setText(meta.title);
@ -1015,6 +961,9 @@ define([
const stagesHeight = Math.max(this.currentY - ACTION_MARGIN, 0); const stagesHeight = Math.max(this.currentY - ACTION_MARGIN, 0);
this.updateBounds(stagesHeight); this.updateBounds(stagesHeight);
this.sizer.resetCache();
this.sizer.detach();
} }
getAgentX(name) { getAgentX(name) {

View File

@ -59,7 +59,7 @@ defineDescribe('Sequence Renderer', ['./Renderer'], (Renderer) => {
const element = renderer.svg(); const element = renderer.svg();
const line = element.getElementsByClassName('agent-1-line')[0]; const line = element.getElementsByClassName('agent-1-line')[0];
const drawnX = Number(line.getAttribute('d').split(' ')[1]); const drawnX = Number(line.getAttribute('x1'));
expect(drawnX).toEqual(renderer.getAgentX('A')); expect(drawnX).toEqual(renderer.getAgentX('A'));
}); });

View File

@ -0,0 +1,108 @@
define(['./SVGUtilities', './SVGTextBlock'], (svg, SVGTextBlock) => {
'use strict';
function renderBox(attrs, position) {
return svg.make('rect', Object.assign({}, position, attrs));
}
function renderNote(attrs, flickAttrs, position) {
const g = svg.make('g');
const x0 = position.x;
const x1 = position.x + position.width;
const y0 = position.y;
const y1 = position.y + position.height;
const flick = 7;
g.appendChild(svg.make('polygon', Object.assign({
'points': (
x0 + ' ' + y0 + ' ' +
(x1 - flick) + ' ' + y0 + ' ' +
x1 + ' ' + (y0 + flick) + ' ' +
x1 + ' ' + y1 + ' ' +
x0 + ' ' + y1
),
}, attrs)));
g.appendChild(svg.make('polyline', Object.assign({
'points': (
(x1 - flick) + ' ' + y0 + ' ' +
(x1 - flick) + ' ' + (y0 + flick) + ' ' +
x1 + ' ' + (y0 + flick)
),
}, flickAttrs)));
return g;
}
function renderBoxedText(text, {
x,
y,
padding,
boxAttrs,
labelAttrs,
boxLayer,
labelLayer,
boxRenderer = null,
}) {
if(!text) {
return {width: 0, height: 0, label: null, box: null};
}
let shift = 0;
let anchorX = x;
switch(labelAttrs['text-anchor']) {
case 'middle':
shift = 0.5;
anchorX += (padding.left - padding.right) / 2;
break;
case 'end':
shift = 1;
anchorX -= padding.right;
break;
default:
shift = 0;
anchorX += padding.left;
break;
}
const label = new SVGTextBlock(labelLayer, labelAttrs, {
text,
x: anchorX,
y: y + padding.top,
});
const width = (label.width + padding.left + padding.right);
const height = (label.height + padding.top + padding.bottom);
let box = null;
if(boxRenderer) {
box = boxRenderer({
'x': anchorX - label.width * shift - padding.left,
'y': y,
'width': width,
'height': height,
});
} else {
box = renderBox(boxAttrs, {
'x': anchorX - label.width * shift - padding.left,
'y': y,
'width': width,
'height': height,
});
}
if(boxLayer === labelLayer) {
boxLayer.insertBefore(box, label.firstLine());
} else {
boxLayer.appendChild(box);
}
return {width, height, label, box};
}
return {
renderBox,
renderNote,
renderBoxedText,
};
});

View File

@ -0,0 +1,107 @@
defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
'use strict';
describe('renderBox', () => {
it('returns a simple rect SVG element', () => {
const node = SVGShapes.renderBox({
'foo': 'bar',
}, {
'x': 10,
'y': 20,
'width': 30,
'height': 40,
});
expect(node.tagName).toEqual('rect');
expect(node.getAttribute('foo')).toEqual('bar');
expect(node.getAttribute('x')).toEqual('10');
expect(node.getAttribute('y')).toEqual('20');
expect(node.getAttribute('width')).toEqual('30');
expect(node.getAttribute('height')).toEqual('40');
});
});
describe('renderNote', () => {
it('returns a group containing a rectangle with a page flick', () => {
const node = SVGShapes.renderNote({
'foo': 'bar',
}, {
'zig': 'zag',
}, {
'x': 10,
'y': 20,
'width': 30,
'height': 40,
});
expect(node.tagName).toEqual('g');
expect(node.children.length).toEqual(2);
const back = node.children[0];
expect(back.getAttribute('foo')).toEqual('bar');
expect(back.getAttribute('points')).toEqual(
'10 20 ' +
'33 20 ' +
'40 27 ' +
'40 60 ' +
'10 60'
);
const flick = node.children[1];
expect(flick.getAttribute('zig')).toEqual('zag');
expect(flick.getAttribute('points')).toEqual(
'33 20 ' +
'33 27 ' +
'40 27'
);
});
});
describe('renderBoxedText', () => {
it('renders a label', () => {
const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText('foo', {
x: 1,
y: 2,
padding: {left: 4, top: 8, right: 16, bottom: 32},
boxAttrs: {},
labelAttrs: {'font-size': 10, 'line-height': 1.5, 'foo': 'bar'},
boxLayer: o,
labelLayer: o,
});
expect(rendered.label.text).toEqual('foo');
expect(rendered.label.x).toEqual(5);
expect(rendered.label.y).toEqual(10);
expect(rendered.label.firstLine().parentNode).toEqual(o);
});
it('positions a box beneath the rendered label', () => {
const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText('foo', {
x: 1,
y: 2,
padding: {left: 4, top: 8, right: 16, bottom: 32},
boxAttrs: {'foo': 'bar'},
labelAttrs: {'font-size': 10, 'line-height': 1.5},
boxLayer: o,
labelLayer: o,
});
expect(rendered.box.getAttribute('x')).toEqual('1');
expect(rendered.box.getAttribute('y')).toEqual('2');
expect(rendered.box.getAttribute('height')).toEqual('55');
expect(rendered.box.getAttribute('foo')).toEqual('bar');
expect(rendered.box.parentNode).toEqual(o);
});
it('returns the size of the rendered box', () => {
const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText('foo', {
x: 1,
y: 2,
padding: {left: 4, top: 8, right: 16, bottom: 32},
boxAttrs: {},
labelAttrs: {'font-size': 10, 'line-height': 1.5},
boxLayer: o,
labelLayer: o,
});
expect(rendered.width).toBeGreaterThan(20 - 1);
expect(rendered.height).toEqual(55);
});
});
});

View File

@ -1,16 +1,23 @@
define(['./SVGUtilities'], (svg) => { define(['./SVGUtilities'], (svg) => {
'use strict'; 'use strict';
return class SVGTextBlock { function fontDetails(attrs) {
const size = Number(attrs['font-size']);
const lineHeight = size * (Number(attrs['line-height']) || 1);
return {
size,
lineHeight,
};
}
class SVGTextBlock {
constructor( constructor(
container, container,
attrs, attrs,
lineHeight,
{text = '', x = 0, y = 0} = {} {text = '', x = 0, y = 0} = {}
) { ) {
this.container = container; this.container = container;
this.attrs = attrs; this.attrs = attrs;
this.lineHeight = lineHeight;
this.text = ''; this.text = '';
this.x = x; this.x = x;
this.y = y; this.y = y;
@ -21,12 +28,11 @@ define(['./SVGUtilities'], (svg) => {
} }
_updateY() { _updateY() {
const sz = Number(this.attrs['font-size']); const {size, lineHeight} = fontDetails(this.attrs);
const space = sz * this.lineHeight;
this.nodes.forEach(({element}, i) => { this.nodes.forEach(({element}, i) => {
element.setAttribute('y', this.y + i * space + sz); element.setAttribute('y', this.y + i * lineHeight + size);
}); });
this.height = space * this.nodes.length; this.height = lineHeight * this.nodes.length;
} }
_rebuildNodes(count) { _rebuildNodes(count) {
@ -51,6 +57,14 @@ define(['./SVGUtilities'], (svg) => {
this._updateY(); this._updateY();
} }
firstLine() {
if(this.nodes.length > 0) {
return this.nodes[0].element;
} else {
return null;
}
}
setText(newText) { setText(newText) {
if(newText === this.text) { if(newText === this.text) {
return; return;
@ -93,5 +107,69 @@ define(['./SVGUtilities'], (svg) => {
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
} }
}
class SizeTester {
constructor(container) {
this.testers = svg.make('g', {'display': 'none'});
this.container = container;
this.cache = new Map();
}
measure(attrs, content) {
if(!content) {
return {width: 0, height: 0};
}
let tester = this.cache.get(attrs);
if(!tester) {
const text = svg.makeText();
const node = svg.make('text', attrs);
node.appendChild(text);
this.testers.appendChild(node);
tester = {text, node};
this.cache.set(attrs, tester);
}
if(!this.testers.parentNode) {
this.container.appendChild(this.testers);
}
const lines = content.split('\n');
let width = 0;
lines.forEach((line) => {
tester.text.nodeValue = line;
width = Math.max(width, tester.node.getComputedTextLength());
});
return {
width,
height: lines.length * fontDetails(attrs).lineHeight,
}; };
}
measureHeight(attrs, content) {
if(!content) {
return 0;
}
const lines = content.split('\n');
return lines.length * fontDetails(attrs).lineHeight;
}
resetCache() {
svg.empty(this.testers);
this.cache.clear();
}
detach() {
if(this.testers.parentNode) {
this.container.removeChild(this.testers);
}
}
}
SVGTextBlock.SizeTester = SizeTester;
return SVGTextBlock;
}); });

View File

@ -1,14 +1,20 @@
defineDescribe('SVGTextBlock', ['./SVGTextBlock'], (SVGTextBlock) => { defineDescribe('SVGTextBlock', [
'./SVGTextBlock',
'./SVGUtilities',
], (
SVGTextBlock,
svg
) => {
'use strict'; 'use strict';
const attrs = {'font-size': 10}; const attrs = {'font-size': 10, 'line-height': 1.5};
let hold = null; let hold = null;
let block = null; let block = null;
beforeEach(() => { beforeEach(() => {
hold = document.createElement('p'); hold = svg.makeContainer();
document.body.appendChild(hold); document.body.appendChild(hold);
block = new SVGTextBlock(hold, attrs, 1.5); block = new SVGTextBlock(hold, attrs);
}); });
afterEach(() => { afterEach(() => {
@ -24,19 +30,19 @@ defineDescribe('SVGTextBlock', ['./SVGTextBlock'], (SVGTextBlock) => {
}); });
it('adds the given text if specified', () => { it('adds the given text if specified', () => {
block = new SVGTextBlock(hold, attrs, 1.5, {text: 'abc'}); block = new SVGTextBlock(hold, attrs, {text: 'abc'});
expect(block.text).toEqual('abc'); expect(block.text).toEqual('abc');
expect(hold.children.length).toEqual(1); expect(hold.children.length).toEqual(1);
}); });
it('uses the given coordinates if specified', () => { it('uses the given coordinates if specified', () => {
block = new SVGTextBlock(hold, attrs, 1.5, {x: 5, y: 7}); block = new SVGTextBlock(hold, attrs, {x: 5, y: 7});
expect(block.x).toEqual(5); expect(block.x).toEqual(5);
expect(block.y).toEqual(7); expect(block.y).toEqual(7);
}); });
}); });
describe('setText', () => { describe('.setText', () => {
it('sets the text to the given content', () => { it('sets the text to the given content', () => {
block.setText('foo'); block.setText('foo');
expect(block.text).toEqual('foo'); expect(block.text).toEqual('foo');
@ -51,6 +57,12 @@ defineDescribe('SVGTextBlock', ['./SVGTextBlock'], (SVGTextBlock) => {
expect(hold.children[1].innerHTML).toEqual('bar'); expect(hold.children[1].innerHTML).toEqual('bar');
}); });
it('populates width and height with the size of the text', () => {
block.setText('foo\nbar');
expect(block.width).toBeGreaterThan(0);
expect(block.height).toEqual(30);
});
it('re-uses text nodes when possible, adding more if needed', () => { it('re-uses text nodes when possible, adding more if needed', () => {
block.setText('foo\nbar'); block.setText('foo\nbar');
const line0 = hold.children[0]; const line0 = hold.children[0];
@ -89,7 +101,7 @@ defineDescribe('SVGTextBlock', ['./SVGTextBlock'], (SVGTextBlock) => {
}); });
}); });
describe('reanchor', () => { describe('.reanchor', () => {
it('moves all nodes', () => { it('moves all nodes', () => {
block.setText('foo\nbaz'); block.setText('foo\nbaz');
block.reanchor(5, 7); block.reanchor(5, 7);
@ -100,7 +112,7 @@ defineDescribe('SVGTextBlock', ['./SVGTextBlock'], (SVGTextBlock) => {
}); });
}); });
describe('clear', () => { describe('.clear', () => {
it('resets the text empty', () => { it('resets the text empty', () => {
block.setText('foo\nbaz'); block.setText('foo\nbaz');
block.setText(''); block.setText('');
@ -110,4 +122,80 @@ defineDescribe('SVGTextBlock', ['./SVGTextBlock'], (SVGTextBlock) => {
expect(block.height).toEqual(0); expect(block.height).toEqual(0);
}); });
}); });
describe('SizeTester', () => {
let tester = null;
beforeEach(() => {
tester = new SVGTextBlock.SizeTester(hold);
});
describe('.measure', () => {
it('calculates the size of the rendered text', () => {
const size = tester.measure(attrs, 'foo');
expect(size.width).toBeGreaterThan(0);
expect(size.height).toEqual(15);
});
it('measures multiline text', () => {
const size = tester.measure(attrs, 'foo\nbar');
expect(size.width).toBeGreaterThan(0);
expect(size.height).toEqual(30);
});
it('returns 0, 0 for empty content', () => {
const size = tester.measure(attrs, '');
expect(size.width).toEqual(0);
expect(size.height).toEqual(0);
});
it('returns the maximum width for multiline text', () => {
const size0 = tester.measure(attrs, 'foo');
const size1 = tester.measure(attrs, 'longline');
const size = tester.measure(attrs, 'foo\nlongline\nfoo');
expect(size1.width).toBeGreaterThan(size0.width);
expect(size.width).toEqual(size1.width);
});
});
describe('.measureHeight', () => {
it('calculates the height of the rendered text', () => {
const height = tester.measureHeight(attrs, 'foo');
expect(height).toEqual(15);
});
it('measures multiline text', () => {
const height = tester.measureHeight(attrs, 'foo\nbar');
expect(height).toEqual(30);
});
it('returns 0 for empty content', () => {
const height = tester.measureHeight(attrs, '');
expect(height).toEqual(0);
});
it('does not require the container', () => {
tester.measureHeight(attrs, 'foo');
expect(hold.children.length).toEqual(0);
});
});
describe('.detach', () => {
it('removes the test node from the DOM', () => {
tester.measure(attrs, 'foo');
expect(hold.children.length).toEqual(1);
tester.detach();
expect(hold.children.length).toEqual(0);
});
it('does not prevent using the tester again later', () => {
tester.measure(attrs, 'foo');
tester.detach();
const size = tester.measure(attrs, 'foo');
expect(hold.children.length).toEqual(1);
expect(size.width).toBeGreaterThan(0);
});
});
});
}); });

View File

@ -6,4 +6,5 @@ define([
'sequence/ArrayUtilities_spec', 'sequence/ArrayUtilities_spec',
'sequence/SVGUtilities_spec', 'sequence/SVGUtilities_spec',
'sequence/SVGTextBlock_spec', 'sequence/SVGTextBlock_spec',
'sequence/SVGShapes_spec',
]); ]);