Support multiline titles

This commit is contained in:
David Evans 2017-10-26 21:09:36 +01:00
parent 94b41000bb
commit d76c42bf5e
4 changed files with 230 additions and 20 deletions

View File

@ -1,4 +1,12 @@
define(['./ArrayUtilities', './SVGUtilities'], (array, svg) => {
define([
'./ArrayUtilities',
'./SVGUtilities',
'./SVGTextBlock',
], (
array,
svg,
SVGTextBlock
) => {
'use strict';
function boxRenderer(attrs, position) {
@ -312,13 +320,6 @@ define(['./ArrayUtilities', './SVGUtilities'], (array, svg) => {
'height': '100%',
});
this.title = svg.make('text', Object.assign({
'y': ATTRS.TITLE['font-size'] + OUTER_MARGIN,
}, ATTRS.TITLE));
this.titleText = svg.makeText();
this.title.appendChild(this.titleText);
this.base.appendChild(this.title);
this.agentLines = svg.make('g');
this.blocks = svg.make('g');
this.sections = svg.make('g');
@ -329,6 +330,7 @@ define(['./ArrayUtilities', './SVGUtilities'], (array, svg) => {
this.base.appendChild(this.sections);
this.base.appendChild(this.agentDecor);
this.base.appendChild(this.actions);
this.title = new SVGTextBlock(this.base, ATTRS.TITLE, LINE_HEIGHT);
this.testers = svg.make('g');
this.testersCache = new Map();
@ -975,19 +977,16 @@ define(['./ArrayUtilities', './SVGUtilities'], (array, svg) => {
}
updateBounds(stagesHeight) {
const titleWidth = this.title.getComputedTextLength();
const cx = (this.minX + this.maxX) / 2;
this.title.setAttribute('x', cx);
this.title.setAttribute('y', -TITLE_MARGIN);
const x0 = Math.min(this.minX, cx - titleWidth / 2) - OUTER_MARGIN;
const x1 = Math.max(this.maxX, cx + titleWidth / 2) + OUTER_MARGIN;
const y0 = (
-TITLE_MARGIN
- ATTRS.TITLE['font-size'] * LINE_HEIGHT
- OUTER_MARGIN
const titleY = ((this.title.height > 0) ?
(-TITLE_MARGIN - this.title.height) : 0
);
this.title.reanchor(cx, titleY);
const halfTitleWidth = this.title.width / 2;
const x0 = Math.min(this.minX, cx - halfTitleWidth) - OUTER_MARGIN;
const x1 = Math.max(this.maxX, cx + halfTitleWidth) + OUTER_MARGIN;
const y0 = titleY - OUTER_MARGIN;
const y1 = stagesHeight + OUTER_MARGIN;
this.base.setAttribute('viewBox', (
@ -1005,7 +1004,7 @@ define(['./ArrayUtilities', './SVGUtilities'], (array, svg) => {
svg.empty(this.agentDecor);
svg.empty(this.actions);
this.titleText.nodeValue = meta.title || '';
this.title.setText(meta.title);
this.minX = 0;
this.maxX = 0;

View File

@ -0,0 +1,97 @@
define(['./SVGUtilities'], (svg) => {
'use strict';
return class SVGTextBlock {
constructor(
container,
attrs,
lineHeight,
{text = '', x = 0, y = 0} = {}
) {
this.container = container;
this.attrs = attrs;
this.lineHeight = lineHeight;
this.text = '';
this.x = x;
this.y = y;
this.width = 0;
this.height = 0;
this.nodes = [];
this.setText(text);
}
_updateY() {
const sz = Number(this.attrs['font-size']);
const space = sz * this.lineHeight;
this.nodes.forEach(({element}, i) => {
element.setAttribute('y', this.y + i * space + sz);
});
this.height = space * this.nodes.length;
}
_rebuildNodes(count) {
if(count === this.nodes.length) {
return;
}
if(count > this.nodes.length) {
const attrs = Object.assign({'x': this.x}, this.attrs);
while(this.nodes.length < count) {
const element = svg.make('text', attrs);
const text = svg.makeText();
element.appendChild(text);
this.container.appendChild(element);
this.nodes.push({element, text});
}
} else {
while(this.nodes.length > count) {
const {element} = this.nodes.pop();
this.container.removeChild(element);
}
}
this._updateY();
}
setText(newText) {
if(newText === this.text) {
return;
}
if(!newText) {
this.clear();
return;
}
this.text = newText;
const lines = this.text.split('\n');
this._rebuildNodes(lines.length);
let maxWidth = 0;
this.nodes.forEach(({text, element}, i) => {
if(text.nodeValue !== lines[i]) {
text.nodeValue = lines[i];
}
maxWidth = Math.max(maxWidth, element.getComputedTextLength());
});
this.width = maxWidth;
}
reanchor(newX, newY) {
if(newX !== this.x) {
this.x = newX;
this.nodes.forEach(({element}) => {
element.setAttribute('x', this.x);
});
}
if(newY !== this.y) {
this.y = newY;
this._updateY();
}
}
clear() {
this._rebuildNodes(0);
this.text = '';
this.width = 0;
this.height = 0;
}
};
});

View File

@ -0,0 +1,113 @@
defineDescribe('SVGTextBlock', ['./SVGTextBlock'], (SVGTextBlock) => {
'use strict';
const attrs = {'font-size': 10};
let hold = null;
let block = null;
beforeEach(() => {
hold = document.createElement('p');
document.body.appendChild(hold);
block = new SVGTextBlock(hold, attrs, 1.5);
});
afterEach(() => {
document.body.removeChild(hold);
});
describe('constructor', () => {
it('defaults to blank text at 0, 0', () => {
expect(block.text).toEqual('');
expect(block.x).toEqual(0);
expect(block.y).toEqual(0);
expect(hold.children.length).toEqual(0);
});
it('adds the given text if specified', () => {
block = new SVGTextBlock(hold, attrs, 1.5, {text: 'abc'});
expect(block.text).toEqual('abc');
expect(hold.children.length).toEqual(1);
});
it('uses the given coordinates if specified', () => {
block = new SVGTextBlock(hold, attrs, 1.5, {x: 5, y: 7});
expect(block.x).toEqual(5);
expect(block.y).toEqual(7);
});
});
describe('setText', () => {
it('sets the text to the given content', () => {
block.setText('foo');
expect(block.text).toEqual('foo');
expect(hold.children.length).toEqual(1);
expect(hold.children[0].innerHTML).toEqual('foo');
});
it('renders multiline text', () => {
block.setText('foo\nbar');
expect(hold.children.length).toEqual(2);
expect(hold.children[0].innerHTML).toEqual('foo');
expect(hold.children[1].innerHTML).toEqual('bar');
});
it('re-uses text nodes when possible, adding more if needed', () => {
block.setText('foo\nbar');
const line0 = hold.children[0];
const line1 = hold.children[1];
block.setText('zig\nzag\nbaz');
expect(hold.children.length).toEqual(3);
expect(hold.children[0]).toEqual(line0);
expect(hold.children[0].innerHTML).toEqual('zig');
expect(hold.children[1]).toEqual(line1);
expect(hold.children[1].innerHTML).toEqual('zag');
expect(hold.children[2].innerHTML).toEqual('baz');
});
it('re-uses text nodes when possible, removing extra if needed', () => {
block.setText('foo\nbar');
const line0 = hold.children[0];
block.setText('zig');
expect(hold.children.length).toEqual(1);
expect(hold.children[0]).toEqual(line0);
expect(hold.children[0].innerHTML).toEqual('zig');
});
it('positions text nodes and applies attributes', () => {
block.setText('foo\nbar');
expect(hold.children.length).toEqual(2);
expect(hold.children[0].getAttribute('x')).toEqual('0');
expect(hold.children[0].getAttribute('y')).toEqual('10');
expect(hold.children[0].getAttribute('font-size')).toEqual('10');
expect(hold.children[1].getAttribute('x')).toEqual('0');
expect(hold.children[1].getAttribute('y')).toEqual('25');
expect(hold.children[1].getAttribute('font-size')).toEqual('10');
});
});
describe('reanchor', () => {
it('moves all nodes', () => {
block.setText('foo\nbaz');
block.reanchor(5, 7);
expect(hold.children[0].getAttribute('x')).toEqual('5');
expect(hold.children[0].getAttribute('y')).toEqual('17');
expect(hold.children[1].getAttribute('x')).toEqual('5');
expect(hold.children[1].getAttribute('y')).toEqual('32');
});
});
describe('clear', () => {
it('resets the text empty', () => {
block.setText('foo\nbaz');
block.setText('');
expect(hold.children.length).toEqual(0);
expect(block.text).toEqual('');
expect(block.width).toEqual(0);
expect(block.height).toEqual(0);
});
});
});

View File

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