From d76c42bf5e6395d3867581613f132e4afb877642 Mon Sep 17 00:00:00 2001 From: David Evans Date: Thu, 26 Oct 2017 21:09:36 +0100 Subject: [PATCH] Support multiline titles --- scripts/sequence/Renderer.js | 39 +++++---- scripts/sequence/SVGTextBlock.js | 97 ++++++++++++++++++++++ scripts/sequence/SVGTextBlock_spec.js | 113 ++++++++++++++++++++++++++ scripts/specs.js | 1 + 4 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 scripts/sequence/SVGTextBlock.js create mode 100644 scripts/sequence/SVGTextBlock_spec.js diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index 0b9aca8..94b4e55 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -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; diff --git a/scripts/sequence/SVGTextBlock.js b/scripts/sequence/SVGTextBlock.js new file mode 100644 index 0000000..3a71395 --- /dev/null +++ b/scripts/sequence/SVGTextBlock.js @@ -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; + } + }; +}); diff --git a/scripts/sequence/SVGTextBlock_spec.js b/scripts/sequence/SVGTextBlock_spec.js new file mode 100644 index 0000000..417b064 --- /dev/null +++ b/scripts/sequence/SVGTextBlock_spec.js @@ -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); + }); + }); +}); diff --git a/scripts/specs.js b/scripts/specs.js index c92bf97..e2d1b05 100644 --- a/scripts/specs.js +++ b/scripts/specs.js @@ -5,4 +5,5 @@ define([ 'sequence/Renderer_spec', 'sequence/ArrayUtilities_spec', 'sequence/SVGUtilities_spec', + 'sequence/SVGTextBlock_spec', ]);