Large refactoring to unpick direct DOM access [#32]
This commit is contained in:
parent
42ea5d1bf9
commit
a5f32d34d8
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -419,6 +419,10 @@ Creates a new SequenceDiagram object. Options is an object which can contain:
|
|||
separate argument.</li>
|
||||
<li><code>container</code>: DOM node to append the diagram to (defaults to
|
||||
null).</li>
|
||||
<li><code>document</code>: Document object to base the diagram in (defaults to
|
||||
container's document, or <code>window.document</code>).</li>
|
||||
<li><code>textSizerFactory</code>: Function which returns an object capable of
|
||||
measuring text (defaults to wrapping <code>getComputedTextLength</code>).</li>
|
||||
<li><code>themes</code>: List of themes to make available to the diagram
|
||||
(defaults to globally registered themes).</li>
|
||||
<li><code>namespace</code>: Each diagram on a page must have a unique namespace.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 +
|
||||
'</' + this.tagName + '>'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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('<div></div>');
|
||||
});
|
||||
|
||||
it('includes attributes', () => {
|
||||
const o = doc.createElement('div');
|
||||
o.setAttribute('foo', 'bar');
|
||||
o.setAttribute('zig', 'zag');
|
||||
|
||||
expect(o.outerHTML).toEqual('<div foo="bar" zig="zag"></div>');
|
||||
});
|
||||
|
||||
it('escapes attributes', () => {
|
||||
const o = doc.createElement('div');
|
||||
o.setAttribute('foo', 'b&a"r');
|
||||
|
||||
expect(o.outerHTML).toEqual('<div foo="b&a"r"></div>');
|
||||
});
|
||||
|
||||
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('<div>abc<span>def</span>ghi</div>');
|
||||
});
|
||||
|
||||
it('escapes text content', () => {
|
||||
const o = doc.createElement('div');
|
||||
o.appendChild(doc.createTextNode('a<b>c'));
|
||||
|
||||
expect(o.outerHTML).toEqual('<div>a<b>c</div>');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,62 +147,12 @@ 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;
|
||||
}
|
||||
|
||||
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.downloadSVG = makeNode('a', {
|
||||
'href': '#',
|
||||
'download': 'SequenceDiagram.svg',
|
||||
}, [makeText('SVG')]);
|
||||
on(this.downloadSVG, ['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.diagram
|
||||
.on('render', () => {
|
||||
this.updateMinSize(this.diagram.getSize());
|
||||
this.pngDirty = true;
|
||||
});
|
||||
|
||||
this.diagram.addEventListener('mouseover', (element) => {
|
||||
})
|
||||
.on('mouseover', (element) => {
|
||||
if(this.marker) {
|
||||
this.marker.clear();
|
||||
}
|
||||
|
@ -237,16 +168,14 @@ define(['require'], (require) => {
|
|||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.diagram.addEventListener('mouseout', () => {
|
||||
})
|
||||
.on('mouseout', () => {
|
||||
if(this.marker) {
|
||||
this.marker.clear();
|
||||
this.marker = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.diagram.addEventListener('click', (element) => {
|
||||
})
|
||||
.on('click', (element) => {
|
||||
if(this.marker) {
|
||||
this.marker.clear();
|
||||
this.marker = null;
|
||||
|
@ -259,103 +188,99 @@ define(['require'], (require) => {
|
|||
);
|
||||
this.code.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.diagram.addEventListener('dblclick', (element) => {
|
||||
})
|
||||
.on('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);
|
||||
buildOptionsDownloads() {
|
||||
this.downloadPNG = dom.el('a')
|
||||
.text('Download PNG')
|
||||
.attrs({
|
||||
'href': '#',
|
||||
'download': 'SequenceDiagram.png',
|
||||
})
|
||||
.on(['focus', 'mouseover', 'mousedown'], this._downloadPNGFocus)
|
||||
.on('click', this._downloadPNGClick);
|
||||
|
||||
this.container.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
this._hideDropStyle();
|
||||
const file = getDroppedFile(event, 'image/svg+xml');
|
||||
if(file) {
|
||||
this.loadFile(file);
|
||||
}
|
||||
});
|
||||
this.downloadSVG = dom.el('a')
|
||||
.text('SVG')
|
||||
.attrs({
|
||||
'href': '#',
|
||||
'download': 'SequenceDiagram.svg',
|
||||
})
|
||||
.on('click', this._downloadSVGClick);
|
||||
|
||||
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'}, [
|
||||
this.container = dom.wrap(container)
|
||||
.add(dom.el('div').setClass('pane-hold')
|
||||
.add(
|
||||
lPane,
|
||||
viewPane,
|
||||
this.buildOptionsLinks(),
|
||||
this.buildOptionsDownloads(),
|
||||
]);
|
||||
container.appendChild(hold);
|
||||
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)
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ define(() => {
|
|||
}
|
||||
|
||||
getSVGContent(renderer) {
|
||||
let code = renderer.svg().outerHTML;
|
||||
let code = renderer.dom().outerHTML;
|
||||
|
||||
// Firefox fails to render SVGs as <img> unless they have size
|
||||
// attributes on the <svg> tag, so we must set this when
|
||||
|
|
|
@ -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', () => {
|
||||
it('produces a format compatible with SVG.formattedText', () => {
|
||||
const formatted = parser('hello everybody');
|
||||
block.set({formatted});
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(hold.children[0].innerHTML).toEqual('hello everybody');
|
||||
});
|
||||
const svg = new SVG(TestDOM.dom, TestDOM.textSizerFactory);
|
||||
const block = svg.formattedText({}, formatted).element;
|
||||
expect(block.outerHTML).toEqual(
|
||||
'<g><text x="0" y="1">hello everybody</text></g>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.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.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.shapes
|
||||
),
|
||||
this.unmaskedShapes,
|
||||
this.title
|
||||
);
|
||||
this.base.appendChild(this.unmaskedShapes);
|
||||
this.title = new this.SVGTextBlockClass(this.base);
|
||||
|
||||
this.sizer = new this.SVGTextBlockClass.SizeTester(this.base);
|
||||
}
|
||||
|
||||
addThemeDef(name, generator) {
|
||||
const namespacedName = this.namespace + name;
|
||||
if(this.knownThemeDefs.has(name)) {
|
||||
return namespacedName;
|
||||
}
|
||||
if(!this.knownThemeDefs.has(name)) {
|
||||
this.knownThemeDefs.add(name);
|
||||
const def = generator();
|
||||
def.setAttribute('id', namespacedName);
|
||||
this.themeDefs.appendChild(def);
|
||||
this.themeDefs.add(generator().attr('id', namespacedName));
|
||||
}
|
||||
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);
|
||||
const def = generator();
|
||||
def.setAttribute('id', namespacedName);
|
||||
this.defs.appendChild(def);
|
||||
this.defs.add(generator().attr('id', namespacedName));
|
||||
}
|
||||
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,13 +338,10 @@ define([
|
|||
|
||||
drawAgentLine(agentInfo, toY) {
|
||||
if(
|
||||
agentInfo.latestYStart === null ||
|
||||
toY <= agentInfo.latestYStart
|
||||
agentInfo.latestYStart !== null &&
|
||||
toY > agentInfo.latestYStart
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.agentLines.appendChild(this.theme.renderAgentLine({
|
||||
this.agentLines.add(this.theme.renderAgentLine({
|
||||
x: agentInfo.x,
|
||||
y0: agentInfo.latestYStart,
|
||||
y1: toY,
|
||||
|
@ -361,6 +350,7 @@ define([
|
|||
options: agentInfo.options,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
addHighlightObject(line, o) {
|
||||
let list = this.highlights.get(line);
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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'));
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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', [
|
|||
'<g mask="url(#FullMask)">' +
|
||||
'<g mask="url(#LineMask)"></g>' +
|
||||
'</g>' +
|
||||
'<g>' +
|
||||
'<text' +
|
||||
' x="0"' +
|
||||
' font-family="sans-serif"' +
|
||||
|
@ -94,7 +95,8 @@ defineDescribe('SequenceDiagram', [
|
|||
' line-height="1.3"' +
|
||||
' text-anchor="middle"' +
|
||||
' class="title"' +
|
||||
' y="-11">My title here</text>' +
|
||||
' y="-10">My title here</text>' +
|
||||
'</g>' +
|
||||
'</svg>'
|
||||
);
|
||||
});
|
||||
|
@ -110,10 +112,12 @@ defineDescribe('SequenceDiagram', [
|
|||
|
||||
// Agent 1
|
||||
expect(content).toContain(
|
||||
'<line x1="20.5" y1="11" x2="20.5" y2="46" class="agent-1-line"'
|
||||
'<line fill="none" stroke="#000000" stroke-width="1"' +
|
||||
' x1="20.5" y1="11" x2="20.5" y2="46" class="agent-1-line"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<rect x="10" y="0" width="21" height="11"'
|
||||
'<rect fill="transparent" class="outline"' +
|
||||
' x="10" y="0" width="21" height="11"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<text x="20.5"'
|
||||
|
@ -121,10 +125,12 @@ defineDescribe('SequenceDiagram', [
|
|||
|
||||
// Agent 2
|
||||
expect(content).toContain(
|
||||
'<line x1="51.5" y1="11" x2="51.5" y2="46" class="agent-2-line"'
|
||||
'<line fill="none" stroke="#000000" stroke-width="1"' +
|
||||
' x1="51.5" y1="11" x2="51.5" y2="46" class="agent-2-line"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<rect x="41" y="0" width="21" height="11"'
|
||||
'<rect fill="transparent" class="outline"' +
|
||||
' x="41" y="0" width="21" height="11"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<text x="51.5"'
|
||||
|
@ -147,9 +153,19 @@ defineDescribe('SequenceDiagram', [
|
|||
|
||||
expect(content).toContain('<svg viewBox="-5 -5 60 39">');
|
||||
|
||||
expect(content).toContain('<line x1="20" y1="7" x2="20" y2="29"');
|
||||
expect(content).toContain('<line x1="30" y1="7" x2="30" y2="29"');
|
||||
expect(content).toContain('<rect x="10" y="0" width="30" height="9"');
|
||||
expect(content).toContain(
|
||||
'<line fill="none" stroke="#000000" stroke-width="1"' +
|
||||
' x1="20" y1="7" x2="20" y2="29"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<line fill="none" stroke="#000000" stroke-width="1"' +
|
||||
' x1="30" y1="7" x2="30" y2="29"'
|
||||
);
|
||||
expect(content).toContain(
|
||||
'<rect fill="none" stroke="#000000" stroke-width="1.5"' +
|
||||
' rx="2" ry="2"' +
|
||||
' x="10" y="0" width="30" height="9"'
|
||||
);
|
||||
expect(content).toContain('<g class="region collapsed"');
|
||||
});
|
||||
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
define([
|
||||
'./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;
|
||||
|
@ -52,27 +53,18 @@ define([
|
|||
|
||||
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', {
|
||||
|
||||
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,
|
||||
'fill': 'transparent',
|
||||
'class': 'outline',
|
||||
}), text.label.firstLine());
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
lineTop: 0,
|
||||
|
@ -104,22 +96,20 @@ define([
|
|||
const config = env.theme.agentCap.cross;
|
||||
const d = config.size / 2;
|
||||
|
||||
const clickable = env.makeRegion();
|
||||
|
||||
clickable.appendChild(config.render({
|
||||
env.makeRegion().add(
|
||||
config.render({
|
||||
x,
|
||||
y: y + d,
|
||||
radius: d,
|
||||
options,
|
||||
}));
|
||||
clickable.appendChild(svg.make('rect', {
|
||||
}),
|
||||
env.svg.box(OUTLINE_ATTRS, {
|
||||
'x': x - d,
|
||||
'y': y,
|
||||
'width': d * 2,
|
||||
'height': d * 2,
|
||||
'fill': 'transparent',
|
||||
'class': 'outline',
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
lineTop: d,
|
||||
|
@ -165,22 +155,21 @@ define([
|
|||
);
|
||||
const height = barCfg.height;
|
||||
|
||||
const clickable = env.makeRegion();
|
||||
clickable.appendChild(barCfg.render({
|
||||
env.makeRegion().add(
|
||||
barCfg.render({
|
||||
x: x - width / 2,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
options,
|
||||
}));
|
||||
clickable.appendChild(svg.make('rect', {
|
||||
}),
|
||||
env.svg.box(OUTLINE_ATTRS, {
|
||||
'x': x - width / 2,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'fill': 'transparent',
|
||||
'class': 'outline',
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
lineTop: 0,
|
||||
|
@ -212,38 +201,37 @@ define([
|
|||
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 {
|
||||
|
@ -275,13 +263,11 @@ define([
|
|||
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 {
|
||||
|
@ -359,12 +345,7 @@ define([
|
|||
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);
|
||||
|
|
|
@ -66,8 +66,8 @@ define(() => {
|
|||
blockLayer,
|
||||
theme,
|
||||
agentInfos,
|
||||
svg,
|
||||
textSizer,
|
||||
SVGTextBlockClass,
|
||||
addDef,
|
||||
makeRegion,
|
||||
state,
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
define([
|
||||
'./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);
|
||||
|
@ -53,58 +54,46 @@ define([
|
|||
|
||||
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', {
|
||||
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,
|
||||
'fill': 'transparent',
|
||||
'class': 'outline',
|
||||
}), clickable.firstChild);
|
||||
}),
|
||||
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;
|
||||
|
@ -160,8 +149,8 @@ define([
|
|||
}
|
||||
|
||||
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;
|
||||
|
@ -215,13 +204,9 @@ define([
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
define([
|
||||
'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;
|
||||
|
@ -38,7 +39,7 @@ define([
|
|||
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,
|
||||
|
@ -76,7 +77,7 @@ define([
|
|||
|
||||
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,
|
||||
|
@ -194,7 +195,7 @@ define([
|
|||
xR,
|
||||
rad: config.loopbackRadius,
|
||||
});
|
||||
clickable.appendChild(rendered.shape);
|
||||
clickable.add(rendered.shape);
|
||||
|
||||
lArrow.render(clickable, env.theme, {
|
||||
x: rendered.p1.x - dx1,
|
||||
|
@ -227,19 +228,15 @@ define([
|
|||
(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 -
|
||||
|
@ -252,6 +249,20 @@ define([
|
|||
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,
|
||||
|
@ -260,18 +271,6 @@ define([
|
|||
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) +
|
||||
|
@ -300,7 +299,7 @@ define([
|
|||
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};
|
||||
|
@ -320,14 +319,14 @@ define([
|
|||
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,
|
||||
|
@ -352,21 +351,20 @@ define([
|
|||
')'
|
||||
);
|
||||
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) {
|
||||
|
@ -402,17 +400,16 @@ define([
|
|||
|
||||
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,
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
define([
|
||||
'./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);
|
||||
|
@ -41,8 +42,6 @@ define([
|
|||
const left = env.agentInfos.get('[');
|
||||
const right = env.agentInfos.get(']');
|
||||
|
||||
const clickable = env.makeRegion({unmasked: true});
|
||||
|
||||
let labelWidth = 0;
|
||||
let labelHeight = 0;
|
||||
if(formattedLabel) {
|
||||
|
@ -54,22 +53,22 @@ define([
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -82,20 +81,18 @@ define([
|
|||
height,
|
||||
env,
|
||||
});
|
||||
if(shape) {
|
||||
clickable.insertBefore(shape, clickable.firstChild);
|
||||
}
|
||||
if(mask) {
|
||||
env.fullMaskLayer.appendChild(mask);
|
||||
}
|
||||
clickable.insertBefore(svg.make('rect', {
|
||||
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,
|
||||
'fill': 'transparent',
|
||||
'class': 'outline',
|
||||
}), clickable.firstChild);
|
||||
}),
|
||||
shape,
|
||||
labelText
|
||||
);
|
||||
|
||||
return env.primaryY + fullHeight + env.theme.actionMargin;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
||||
define(['./BaseComponent'], (BaseComponent) => {
|
||||
'use strict';
|
||||
|
||||
const OUTLINE_ATTRS = {
|
||||
'fill': 'transparent',
|
||||
'class': 'outline',
|
||||
};
|
||||
|
||||
function findExtremes(agentInfos, agentIDs) {
|
||||
let min = null;
|
||||
let max = null;
|
||||
|
@ -39,14 +44,8 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
}, 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 = (
|
||||
|
@ -85,21 +84,21 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
|
|||
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({
|
||||
env.makeRegion().add(
|
||||
config.boxRenderer({
|
||||
x: x0,
|
||||
y: env.topY + config.margin.top,
|
||||
width: x1 - x0,
|
||||
height: fullH,
|
||||
}), clickable.firstChild);
|
||||
}),
|
||||
env.svg.box(OUTLINE_ATTRS, {
|
||||
'x': x0,
|
||||
'y': env.topY + config.margin.top,
|
||||
'width': x1 - x0,
|
||||
'height': fullH,
|
||||
}),
|
||||
labelNode
|
||||
);
|
||||
|
||||
return (
|
||||
env.topY +
|
||||
|
|
|
@ -1,25 +1,6 @@
|
|||
define([
|
||||
'svg/SVGUtilities',
|
||||
'svg/SVGShapes',
|
||||
], (
|
||||
svg,
|
||||
SVGShapes
|
||||
) => {
|
||||
define(() => {
|
||||
'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) => {
|
||||
|
@ -29,14 +10,12 @@ define([
|
|||
}
|
||||
|
||||
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() {
|
||||
}
|
||||
|
||||
|
@ -62,43 +41,39 @@ define([
|
|||
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}) => {
|
||||
// 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 svg.make(
|
||||
attrs.fill === 'none' ? 'polyline' : 'polygon',
|
||||
Object.assign({
|
||||
'points': (
|
||||
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)
|
||||
);
|
||||
};
|
||||
))
|
||||
.attrs(attrs);
|
||||
}
|
||||
|
||||
BaseTheme.renderTag = (attrs, {x, y, width, height}) => {
|
||||
renderTag(attrs, {x, y, width, height}) {
|
||||
const {rx, ry} = attrs;
|
||||
const x2 = x + width;
|
||||
const y2 = y + height;
|
||||
|
@ -110,68 +85,253 @@ define([
|
|||
'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'})));
|
||||
}
|
||||
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.appendChild(svg.make('path', Object.assign({
|
||||
'd': line,
|
||||
}, attrs, {'fill': 'none'})));
|
||||
g.add(this.svg.el('path')
|
||||
.attr('d', line)
|
||||
.attrs(attrs)
|
||||
.attr('fill', 'none')
|
||||
);
|
||||
}
|
||||
|
||||
return g;
|
||||
};
|
||||
}
|
||||
|
||||
BaseTheme.renderDB = (attrs, {x, y, width, height}) => {
|
||||
renderDB(attrs, position) {
|
||||
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,
|
||||
return this.svg.el('g').add(
|
||||
this.svg.box({
|
||||
'rx': position.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'})),
|
||||
]);
|
||||
};
|
||||
}, 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.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));
|
||||
};
|
||||
|
||||
BaseTheme.renderRef = (options, position) => {
|
||||
renderRef(options, position) {
|
||||
return {
|
||||
shape: SVGShapes.renderBox(Object.assign({}, options, {
|
||||
'fill': 'none',
|
||||
}), position),
|
||||
mask: SVGShapes.renderBox(Object.assign({}, options, {
|
||||
shape: this.svg.box(options, position).attrs({'fill': 'none'}),
|
||||
mask: this.svg.box(options, position).attrs({
|
||||
'fill': '#000000',
|
||||
'stroke': 'none',
|
||||
}), position),
|
||||
fill: SVGShapes.renderBox(Object.assign({}, options, {
|
||||
'stroke': 'none',
|
||||
}), position),
|
||||
}),
|
||||
fill: this.svg.box(options, position).attrs({'stroke': 'none'}),
|
||||
};
|
||||
}
|
||||
|
||||
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},
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -197,195 +357,5 @@ define([
|
|||
}
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
define([
|
||||
'./BaseTheme',
|
||||
'svg/SVGUtilities',
|
||||
'svg/SVGShapes',
|
||||
], (
|
||||
BaseTheme,
|
||||
svg,
|
||||
SVGShapes
|
||||
) => {
|
||||
define(['./BaseTheme'], (BaseTheme) => {
|
||||
'use strict';
|
||||
|
||||
const FONT = 'sans-serif';
|
||||
|
@ -14,7 +6,68 @@ define([
|
|||
|
||||
const WAVE = new BaseTheme.WavePattern(6, 0.5);
|
||||
|
||||
const SETTINGS = {
|
||||
const NOTE_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
};
|
||||
|
||||
const DIVIDER_LABEL_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
'text-anchor': 'middle',
|
||||
};
|
||||
|
||||
class BasicTheme extends BaseTheme {
|
||||
constructor(svg) {
|
||||
super(svg);
|
||||
|
||||
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,
|
||||
|
@ -51,7 +104,7 @@ define([
|
|||
bottom: 3,
|
||||
},
|
||||
arrowBottom: 5 + 12 * 1.3 / 2,
|
||||
boxRenderer: BaseTheme.renderDB.bind(null, {
|
||||
boxRenderer: this.renderDB.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -66,7 +119,7 @@ define([
|
|||
},
|
||||
cross: {
|
||||
size: 20,
|
||||
render: BaseTheme.renderCross.bind(null, {
|
||||
render: svg.crossFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -74,7 +127,7 @@ define([
|
|||
},
|
||||
bar: {
|
||||
height: 4,
|
||||
render: SVGShapes.renderBox.bind(null, {
|
||||
render: svg.boxFactory({
|
||||
'fill': '#000000',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -99,8 +152,8 @@ define([
|
|||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, null),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, null),
|
||||
renderFlat: this.renderFlatConnect.bind(this, null),
|
||||
renderRev: this.renderRevConnect.bind(this, null),
|
||||
},
|
||||
'dash': {
|
||||
attrs: {
|
||||
|
@ -109,8 +162,8 @@ define([
|
|||
'stroke-width': 1,
|
||||
'stroke-dasharray': '4, 2',
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, null),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, null),
|
||||
renderFlat: this.renderFlatConnect.bind(this, null),
|
||||
renderRev: this.renderRevConnect.bind(this, null),
|
||||
},
|
||||
'wave': {
|
||||
attrs: {
|
||||
|
@ -120,15 +173,15 @@ define([
|
|||
'stroke-linejoin': 'round',
|
||||
'stroke-linecap': 'round',
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, WAVE),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, WAVE),
|
||||
renderFlat: this.renderFlatConnect.bind(this, WAVE),
|
||||
renderRev: this.renderRevConnect.bind(this, WAVE),
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
'single': {
|
||||
width: 5,
|
||||
height: 10,
|
||||
render: BaseTheme.renderArrowHead,
|
||||
render: this.renderArrowHead.bind(this),
|
||||
attrs: {
|
||||
'fill': '#000000',
|
||||
'stroke-width': 0,
|
||||
|
@ -138,7 +191,7 @@ define([
|
|||
'double': {
|
||||
width: 4,
|
||||
height: 6,
|
||||
render: BaseTheme.renderArrowHead,
|
||||
render: this.renderArrowHead.bind(this),
|
||||
attrs: {
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
|
@ -149,7 +202,7 @@ define([
|
|||
'cross': {
|
||||
short: 7,
|
||||
radius: 3,
|
||||
render: BaseTheme.renderCross.bind(null, {
|
||||
render: svg.crossFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -173,16 +226,11 @@ define([
|
|||
},
|
||||
source: {
|
||||
radius: 2,
|
||||
render: ({x, y, radius}) => {
|
||||
return svg.make('circle', {
|
||||
'cx': x,
|
||||
'cy': y,
|
||||
'r': radius,
|
||||
render: svg.circleFactory({
|
||||
'fill': '#000000',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
});
|
||||
},
|
||||
}),
|
||||
},
|
||||
mask: {
|
||||
padding: {
|
||||
|
@ -212,107 +260,54 @@ define([
|
|||
'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 = {
|
||||
blocks: {
|
||||
'ref': {
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
boxRenderer: BaseTheme.renderRef.bind(null, {
|
||||
boxRenderer: this.renderRef.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1.5,
|
||||
'rx': 2,
|
||||
'ry': 2,
|
||||
}),
|
||||
section: SHARED_BLOCK_SECTION,
|
||||
section: sharedBlockSection,
|
||||
},
|
||||
'': {
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
boxRenderer: SVGShapes.renderBox.bind(null, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1.5,
|
||||
'rx': 2,
|
||||
'ry': 2,
|
||||
}),
|
||||
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
|
||||
collapsedBoxRenderer: this.renderRef.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1.5,
|
||||
'rx': 2,
|
||||
'ry': 2,
|
||||
}),
|
||||
section: SHARED_BLOCK_SECTION,
|
||||
sepRenderer: SVGShapes.renderLine.bind(null, {
|
||||
section: sharedBlockSection,
|
||||
sepRenderer: svg.lineFactory({
|
||||
'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 = {
|
||||
},
|
||||
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, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': '#FFFFFF',
|
||||
}),
|
||||
labelAttrs: NOTE_ATTRS,
|
||||
|
@ -321,7 +316,7 @@ define([
|
|||
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, {
|
||||
boxRenderer: svg.noteFactory({
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -336,7 +331,7 @@ define([
|
|||
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, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -345,16 +340,8 @@ define([
|
|||
}),
|
||||
labelAttrs: NOTE_ATTRS,
|
||||
},
|
||||
};
|
||||
|
||||
const DIVIDER_LABEL_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
'text-anchor': 'middle',
|
||||
};
|
||||
|
||||
const DIVIDERS = {
|
||||
},
|
||||
dividers: {
|
||||
'': {
|
||||
labelAttrs: DIVIDER_LABEL_ATTRS,
|
||||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
|
@ -367,7 +354,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 10,
|
||||
margin: 0,
|
||||
render: BaseTheme.renderLineDivider.bind(null, {
|
||||
render: this.renderLineDivider.bind(this, {
|
||||
lineAttrs: {
|
||||
'stroke': '#000000',
|
||||
},
|
||||
|
@ -378,7 +365,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 0,
|
||||
margin: 0,
|
||||
render: BaseTheme.renderDelayDivider.bind(null, {
|
||||
render: this.renderDelayDivider.bind(this, {
|
||||
dotSize: 1,
|
||||
gapSize: 2,
|
||||
}),
|
||||
|
@ -388,7 +375,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 10,
|
||||
margin: 10,
|
||||
render: BaseTheme.renderTearDivider.bind(null, {
|
||||
render: this.renderTearDivider.bind(this, {
|
||||
fadeBegin: 5,
|
||||
fadeSize: 10,
|
||||
zigWidth: 6,
|
||||
|
@ -398,17 +385,20 @@ define([
|
|||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return class BasicTheme extends BaseTheme {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'basic',
|
||||
settings: SETTINGS,
|
||||
blocks: BLOCKS,
|
||||
notes: NOTES,
|
||||
dividers: DIVIDERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BasicTheme.Factory = class {
|
||||
constructor() {
|
||||
this.name = 'basic';
|
||||
}
|
||||
|
||||
build(svg) {
|
||||
return new BasicTheme(svg);
|
||||
}
|
||||
};
|
||||
|
||||
return BasicTheme;
|
||||
});
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
defineDescribe('Basic Theme', ['./Basic'], (BasicTheme) => {
|
||||
defineDescribe('Basic Theme', [
|
||||
'./Basic',
|
||||
'svg/SVG',
|
||||
'stubs/TestDOM',
|
||||
], (
|
||||
BasicTheme,
|
||||
SVG,
|
||||
TestDOM
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const theme = new BasicTheme();
|
||||
const svg = new SVG(TestDOM.dom, TestDOM.textSizerFactory);
|
||||
|
||||
const themeFactory = new BasicTheme.Factory();
|
||||
const theme = themeFactory.build(svg);
|
||||
|
||||
it('has a name', () => {
|
||||
expect(theme.name).toEqual('basic');
|
||||
expect(themeFactory.name).toEqual('basic');
|
||||
});
|
||||
|
||||
it('contains settings for the theme', () => {
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
define([
|
||||
'./BaseTheme',
|
||||
'svg/SVGUtilities',
|
||||
'svg/SVGShapes',
|
||||
], (
|
||||
BaseTheme,
|
||||
svg,
|
||||
SVGShapes
|
||||
) => {
|
||||
define(['./BaseTheme'], (BaseTheme) => {
|
||||
'use strict';
|
||||
|
||||
const FONT = 'sans-serif';
|
||||
|
@ -14,7 +6,68 @@ define([
|
|||
|
||||
const WAVE = new BaseTheme.WavePattern(10, 1);
|
||||
|
||||
const SETTINGS = {
|
||||
const NOTE_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
};
|
||||
|
||||
const DIVIDER_LABEL_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
'text-anchor': 'middle',
|
||||
};
|
||||
|
||||
class ChunkyTheme extends BaseTheme {
|
||||
constructor(svg) {
|
||||
super(svg);
|
||||
|
||||
const sharedBlockSection = {
|
||||
padding: {
|
||||
top: 3,
|
||||
bottom: 4,
|
||||
},
|
||||
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,
|
||||
|
@ -54,7 +107,7 @@ define([
|
|||
bottom: 0,
|
||||
},
|
||||
arrowBottom: 2 + 14 * 1.3 / 2,
|
||||
boxRenderer: BaseTheme.renderDB.bind(null, {
|
||||
boxRenderer: this.renderDB.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 3,
|
||||
|
@ -70,7 +123,7 @@ define([
|
|||
},
|
||||
cross: {
|
||||
size: 20,
|
||||
render: BaseTheme.renderCross.bind(null, {
|
||||
render: svg.crossFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 3,
|
||||
|
@ -79,7 +132,7 @@ define([
|
|||
},
|
||||
bar: {
|
||||
height: 4,
|
||||
render: SVGShapes.renderBox.bind(null, {
|
||||
render: svg.boxFactory({
|
||||
'fill': '#000000',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 3,
|
||||
|
@ -106,8 +159,8 @@ define([
|
|||
'stroke': '#000000',
|
||||
'stroke-width': 3,
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, null),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, null),
|
||||
renderFlat: this.renderFlatConnect.bind(this, null),
|
||||
renderRev: this.renderRevConnect.bind(this, null),
|
||||
},
|
||||
'dash': {
|
||||
attrs: {
|
||||
|
@ -116,8 +169,8 @@ define([
|
|||
'stroke-width': 3,
|
||||
'stroke-dasharray': '10, 4',
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, null),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, null),
|
||||
renderFlat: this.renderFlatConnect.bind(this, null),
|
||||
renderRev: this.renderRevConnect.bind(this, null),
|
||||
},
|
||||
'wave': {
|
||||
attrs: {
|
||||
|
@ -127,15 +180,15 @@ define([
|
|||
'stroke-linejoin': 'round',
|
||||
'stroke-linecap': 'round',
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, WAVE),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, WAVE),
|
||||
renderFlat: this.renderFlatConnect.bind(this, WAVE),
|
||||
renderRev: this.renderRevConnect.bind(this, WAVE),
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
'single': {
|
||||
width: 10,
|
||||
height: 12,
|
||||
render: BaseTheme.renderArrowHead,
|
||||
render: this.renderArrowHead.bind(this),
|
||||
attrs: {
|
||||
'fill': '#000000',
|
||||
'stroke': '#000000',
|
||||
|
@ -146,7 +199,7 @@ define([
|
|||
'double': {
|
||||
width: 10,
|
||||
height: 12,
|
||||
render: BaseTheme.renderArrowHead,
|
||||
render: this.renderArrowHead.bind(this),
|
||||
attrs: {
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
|
@ -158,7 +211,7 @@ define([
|
|||
'cross': {
|
||||
short: 10,
|
||||
radius: 5,
|
||||
render: BaseTheme.renderCross.bind(null, {
|
||||
render: svg.crossFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 3,
|
||||
|
@ -184,16 +237,11 @@ define([
|
|||
},
|
||||
source: {
|
||||
radius: 5,
|
||||
render: ({x, y, radius}) => {
|
||||
return svg.make('circle', {
|
||||
'cx': x,
|
||||
'cy': y,
|
||||
'r': radius,
|
||||
render: svg.circleFactory({
|
||||
'fill': '#000000',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 3,
|
||||
});
|
||||
},
|
||||
}),
|
||||
},
|
||||
mask: {
|
||||
padding: {
|
||||
|
@ -224,107 +272,54 @@ define([
|
|||
'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 = {
|
||||
blocks: {
|
||||
'ref': {
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
boxRenderer: BaseTheme.renderRef.bind(null, {
|
||||
boxRenderer: this.renderRef.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 4,
|
||||
'rx': 5,
|
||||
'ry': 5,
|
||||
}),
|
||||
section: SHARED_BLOCK_SECTION,
|
||||
section: sharedBlockSection,
|
||||
},
|
||||
'': {
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
boxRenderer: SVGShapes.renderBox.bind(null, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 4,
|
||||
'rx': 5,
|
||||
'ry': 5,
|
||||
}),
|
||||
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
|
||||
collapsedBoxRenderer: this.renderRef.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 4,
|
||||
'rx': 5,
|
||||
'ry': 5,
|
||||
}),
|
||||
section: SHARED_BLOCK_SECTION,
|
||||
sepRenderer: SVGShapes.renderLine.bind(null, {
|
||||
section: sharedBlockSection,
|
||||
sepRenderer: svg.lineFactory({
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 2,
|
||||
'stroke-dasharray': '5, 3',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const NOTE_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
};
|
||||
|
||||
const NOTES = {
|
||||
},
|
||||
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, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': '#FFFFFF',
|
||||
}),
|
||||
labelAttrs: NOTE_ATTRS,
|
||||
|
@ -333,7 +328,7 @@ define([
|
|||
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, {
|
||||
boxRenderer: svg.noteFactory({
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 2,
|
||||
|
@ -349,7 +344,7 @@ define([
|
|||
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, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 3,
|
||||
|
@ -358,16 +353,8 @@ define([
|
|||
}),
|
||||
labelAttrs: NOTE_ATTRS,
|
||||
},
|
||||
};
|
||||
|
||||
const DIVIDER_LABEL_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
'text-anchor': 'middle',
|
||||
};
|
||||
|
||||
const DIVIDERS = {
|
||||
},
|
||||
dividers: {
|
||||
'': {
|
||||
labelAttrs: DIVIDER_LABEL_ATTRS,
|
||||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
|
@ -380,7 +367,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 10,
|
||||
margin: 0,
|
||||
render: BaseTheme.renderLineDivider.bind(null, {
|
||||
render: this.renderLineDivider.bind(this, {
|
||||
lineAttrs: {
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 2,
|
||||
|
@ -393,7 +380,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 0,
|
||||
margin: 0,
|
||||
render: BaseTheme.renderDelayDivider.bind(null, {
|
||||
render: this.renderDelayDivider.bind(this, {
|
||||
dotSize: 3,
|
||||
gapSize: 3,
|
||||
}),
|
||||
|
@ -403,7 +390,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 10,
|
||||
margin: 10,
|
||||
render: BaseTheme.renderTearDivider.bind(null, {
|
||||
render: this.renderTearDivider.bind(this, {
|
||||
fadeBegin: 5,
|
||||
fadeSize: 10,
|
||||
zigWidth: 6,
|
||||
|
@ -415,17 +402,20 @@ define([
|
|||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return class ChunkyTheme extends BaseTheme {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'chunky',
|
||||
settings: SETTINGS,
|
||||
blocks: BLOCKS,
|
||||
notes: NOTES,
|
||||
dividers: DIVIDERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ChunkyTheme.Factory = class {
|
||||
constructor() {
|
||||
this.name = 'chunky';
|
||||
}
|
||||
|
||||
build(svg) {
|
||||
return new ChunkyTheme(svg);
|
||||
}
|
||||
};
|
||||
|
||||
return ChunkyTheme;
|
||||
});
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
defineDescribe('Chunky Theme', ['./Chunky'], (ChunkyTheme) => {
|
||||
defineDescribe('Chunky Theme', [
|
||||
'./Chunky',
|
||||
'svg/SVG',
|
||||
'stubs/TestDOM',
|
||||
], (
|
||||
ChunkyTheme,
|
||||
SVG,
|
||||
TestDOM
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const theme = new ChunkyTheme();
|
||||
const svg = new SVG(TestDOM.dom, TestDOM.textSizerFactory);
|
||||
|
||||
const themeFactory = new ChunkyTheme.Factory();
|
||||
const theme = themeFactory.build(svg);
|
||||
|
||||
it('has a name', () => {
|
||||
expect(theme.name).toEqual('chunky');
|
||||
expect(themeFactory.name).toEqual('chunky');
|
||||
});
|
||||
|
||||
it('contains settings for the theme', () => {
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
define([
|
||||
'./BaseTheme',
|
||||
'svg/SVGUtilities',
|
||||
'svg/SVGShapes',
|
||||
], (
|
||||
BaseTheme,
|
||||
svg,
|
||||
SVGShapes
|
||||
) => {
|
||||
define(['./BaseTheme'], (BaseTheme) => {
|
||||
'use strict';
|
||||
|
||||
const FONT = 'monospace';
|
||||
|
@ -23,7 +15,68 @@ define([
|
|||
+0.25,
|
||||
]);
|
||||
|
||||
const SETTINGS = {
|
||||
const NOTE_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
};
|
||||
|
||||
const DIVIDER_LABEL_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
'text-anchor': 'middle',
|
||||
};
|
||||
|
||||
class MonospaceTheme extends BaseTheme {
|
||||
constructor(svg) {
|
||||
super(svg);
|
||||
|
||||
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,
|
||||
|
@ -60,7 +113,7 @@ define([
|
|||
bottom: 3,
|
||||
},
|
||||
arrowBottom: 12,
|
||||
boxRenderer: BaseTheme.renderDB.bind(null, {
|
||||
boxRenderer: this.renderDB.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -75,7 +128,7 @@ define([
|
|||
},
|
||||
cross: {
|
||||
size: 16,
|
||||
render: BaseTheme.renderCross.bind(null, {
|
||||
render: svg.crossFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -83,7 +136,7 @@ define([
|
|||
},
|
||||
bar: {
|
||||
height: 4,
|
||||
render: SVGShapes.renderBox.bind(null, {
|
||||
render: svg.boxFactory({
|
||||
'fill': '#000000',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -108,8 +161,8 @@ define([
|
|||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, null),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, null),
|
||||
renderFlat: this.renderFlatConnect.bind(this, null),
|
||||
renderRev: this.renderRevConnect.bind(this, null),
|
||||
},
|
||||
'dash': {
|
||||
attrs: {
|
||||
|
@ -118,8 +171,8 @@ define([
|
|||
'stroke-width': 1,
|
||||
'stroke-dasharray': '4, 4',
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, null),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, null),
|
||||
renderFlat: this.renderFlatConnect.bind(this, null),
|
||||
renderRev: this.renderRevConnect.bind(this, null),
|
||||
},
|
||||
'wave': {
|
||||
attrs: {
|
||||
|
@ -127,15 +180,15 @@ define([
|
|||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
},
|
||||
renderFlat: BaseTheme.renderFlatConnector.bind(null, WAVE),
|
||||
renderRev: BaseTheme.renderRevConnector.bind(null, WAVE),
|
||||
renderFlat: this.renderFlatConnect.bind(this, WAVE),
|
||||
renderRev: this.renderRevConnect.bind(this, WAVE),
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
'single': {
|
||||
width: 4,
|
||||
height: 8,
|
||||
render: BaseTheme.renderArrowHead,
|
||||
render: this.renderArrowHead.bind(this),
|
||||
attrs: {
|
||||
'fill': '#000000',
|
||||
'stroke-width': 0,
|
||||
|
@ -145,7 +198,7 @@ define([
|
|||
'double': {
|
||||
width: 3,
|
||||
height: 6,
|
||||
render: BaseTheme.renderArrowHead,
|
||||
render: this.renderArrowHead.bind(this),
|
||||
attrs: {
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
|
@ -156,7 +209,7 @@ define([
|
|||
'cross': {
|
||||
short: 8,
|
||||
radius: 4,
|
||||
render: BaseTheme.renderCross.bind(null, {
|
||||
render: svg.crossFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -180,16 +233,11 @@ define([
|
|||
},
|
||||
source: {
|
||||
radius: 2,
|
||||
render: ({x, y, radius}) => {
|
||||
return svg.make('circle', {
|
||||
'cx': x,
|
||||
'cy': y,
|
||||
'r': radius,
|
||||
render: svg.circleFactory({
|
||||
'fill': '#000000',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
});
|
||||
},
|
||||
}),
|
||||
},
|
||||
mask: {
|
||||
padding: {
|
||||
|
@ -219,101 +267,48 @@ define([
|
|||
'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 = {
|
||||
blocks: {
|
||||
'ref': {
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
boxRenderer: BaseTheme.renderRef.bind(null, {
|
||||
boxRenderer: this.renderRef.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 2,
|
||||
}),
|
||||
section: SHARED_BLOCK_SECTION,
|
||||
section: sharedBlockSection,
|
||||
},
|
||||
'': {
|
||||
margin: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
boxRenderer: SVGShapes.renderBox.bind(null, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': 'none',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 2,
|
||||
}),
|
||||
collapsedBoxRenderer: BaseTheme.renderRef.bind(null, {
|
||||
collapsedBoxRenderer: this.renderRef.bind(this, {
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 2,
|
||||
}),
|
||||
section: SHARED_BLOCK_SECTION,
|
||||
sepRenderer: SVGShapes.renderLine.bind(null, {
|
||||
section: sharedBlockSection,
|
||||
sepRenderer: svg.lineFactory({
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 2,
|
||||
'stroke-dasharray': '8, 4',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const NOTE_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
};
|
||||
|
||||
const NOTES = {
|
||||
},
|
||||
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, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': '#FFFFFF',
|
||||
}),
|
||||
labelAttrs: NOTE_ATTRS,
|
||||
|
@ -322,7 +317,7 @@ define([
|
|||
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, {
|
||||
boxRenderer: svg.noteFactory({
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -337,7 +332,7 @@ define([
|
|||
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, {
|
||||
boxRenderer: svg.boxFactory({
|
||||
'fill': '#FFFFFF',
|
||||
'stroke': '#000000',
|
||||
'stroke-width': 1,
|
||||
|
@ -346,16 +341,8 @@ define([
|
|||
}),
|
||||
labelAttrs: NOTE_ATTRS,
|
||||
},
|
||||
};
|
||||
|
||||
const DIVIDER_LABEL_ATTRS = {
|
||||
'font-family': FONT,
|
||||
'font-size': 8,
|
||||
'line-height': LINE_HEIGHT,
|
||||
'text-anchor': 'middle',
|
||||
};
|
||||
|
||||
const DIVIDERS = {
|
||||
},
|
||||
dividers: {
|
||||
'': {
|
||||
labelAttrs: DIVIDER_LABEL_ATTRS,
|
||||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
|
@ -368,7 +355,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 8,
|
||||
margin: 0,
|
||||
render: BaseTheme.renderLineDivider.bind(null, {
|
||||
render: this.renderLineDivider.bind(this, {
|
||||
lineAttrs: {
|
||||
'stroke': '#000000',
|
||||
},
|
||||
|
@ -379,7 +366,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 0,
|
||||
margin: 0,
|
||||
render: BaseTheme.renderDelayDivider.bind(null, {
|
||||
render: this.renderDelayDivider.bind(this, {
|
||||
dotSize: 2,
|
||||
gapSize: 2,
|
||||
}),
|
||||
|
@ -389,7 +376,7 @@ define([
|
|||
padding: {top: 2, left: 5, right: 5, bottom: 2},
|
||||
extend: 8,
|
||||
margin: 8,
|
||||
render: BaseTheme.renderTearDivider.bind(null, {
|
||||
render: this.renderTearDivider.bind(this, {
|
||||
fadeBegin: 4,
|
||||
fadeSize: 4,
|
||||
zigWidth: 4,
|
||||
|
@ -399,17 +386,20 @@ define([
|
|||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return class MonospaceTheme extends BaseTheme {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'monospace',
|
||||
settings: SETTINGS,
|
||||
blocks: BLOCKS,
|
||||
notes: NOTES,
|
||||
dividers: DIVIDERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MonospaceTheme.Factory = class {
|
||||
constructor() {
|
||||
this.name = 'monospace';
|
||||
}
|
||||
|
||||
build(svg) {
|
||||
return new MonospaceTheme(svg);
|
||||
}
|
||||
};
|
||||
|
||||
return MonospaceTheme;
|
||||
});
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
defineDescribe('Monospace Theme', ['./Monospace'], (MonospaceTheme) => {
|
||||
defineDescribe('Monospace Theme', [
|
||||
'./Monospace',
|
||||
'svg/SVG',
|
||||
'stubs/TestDOM',
|
||||
], (
|
||||
MonospaceTheme,
|
||||
SVG,
|
||||
TestDOM
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const theme = new MonospaceTheme();
|
||||
const svg = new SVG(TestDOM.dom, TestDOM.textSizerFactory);
|
||||
|
||||
const themeFactory = new MonospaceTheme.Factory();
|
||||
const theme = themeFactory.build(svg);
|
||||
|
||||
it('has a name', () => {
|
||||
expect(theme.name).toEqual('monospace');
|
||||
expect(themeFactory.name).toEqual('monospace');
|
||||
});
|
||||
|
||||
it('contains settings for the theme', () => {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,18 +1,31 @@
|
|||
defineDescribe('Sketch Theme', ['./Sketch'], (SketchTheme) => {
|
||||
defineDescribe('Sketch Theme', [
|
||||
'./Sketch',
|
||||
'svg/SVG',
|
||||
'stubs/TestDOM',
|
||||
], (
|
||||
SketchTheme,
|
||||
SVG,
|
||||
TestDOM
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const theme = new SketchTheme(SketchTheme.RIGHT);
|
||||
const themeL = new SketchTheme(SketchTheme.LEFT);
|
||||
const svg = new SVG(TestDOM.dom, TestDOM.textSizerFactory);
|
||||
|
||||
const themeFactory = new SketchTheme.Factory(SketchTheme.RIGHT);
|
||||
const themeFactoryL = new SketchTheme.Factory(SketchTheme.LEFT);
|
||||
const theme = themeFactory.build(svg);
|
||||
const themeL = themeFactory.build(svg);
|
||||
|
||||
it('has a name', () => {
|
||||
expect(theme.name).toEqual('sketch');
|
||||
expect(themeFactory.name).toEqual('sketch');
|
||||
});
|
||||
|
||||
it('has a left-handed variant', () => {
|
||||
expect(themeL.name).toEqual('sketch left handed');
|
||||
expect(themeFactoryL.name).toEqual('sketch left handed');
|
||||
});
|
||||
|
||||
it('contains settings for the theme', () => {
|
||||
expect(theme.outerMargin).toEqual(5);
|
||||
expect(themeL.outerMargin).toEqual(5);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
define([
|
||||
'core/ArrayUtilities_spec',
|
||||
'core/EventObject_spec',
|
||||
'svg/SVGUtilities_spec',
|
||||
'core/Random_spec',
|
||||
'core/documents/VirtualDocument_spec',
|
||||
'svg/SVG_spec',
|
||||
'svg/SVGTextBlock_spec',
|
||||
'svg/SVGShapes_spec',
|
||||
'svg/PatternedLine_spec',
|
||||
'interface/Interface_spec',
|
||||
'interface/ComponentsLibrary_spec',
|
||||
|
|
|
@ -1,191 +0,0 @@
|
|||
define(['svg/SVGUtilities'], (svg) => {
|
||||
'use strict';
|
||||
|
||||
// Simplified text block renderer, which assumes all characters render as
|
||||
// 1x1 px squares for repeatable renders in all browsers
|
||||
|
||||
function merge(state, newState) {
|
||||
for(const k in state) {
|
||||
if(state.hasOwnProperty(k)) {
|
||||
if(newState[k] !== null && newState[k] !== undefined) {
|
||||
state[k] = newState[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const EMPTY = [];
|
||||
|
||||
class SVGTextBlock {
|
||||
constructor(container, initialState = {}) {
|
||||
this.container = container;
|
||||
this.state = {
|
||||
attrs: {},
|
||||
formatted: EMPTY,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
this.nodes = [];
|
||||
this.set(initialState);
|
||||
}
|
||||
|
||||
_rebuildNodes(count) {
|
||||
if(count > this.nodes.length) {
|
||||
const attrs = Object.assign({
|
||||
'x': this.state.x,
|
||||
}, this.state.attrs);
|
||||
|
||||
while(this.nodes.length < count) {
|
||||
const text = svg.makeText();
|
||||
const element = svg.make('text', attrs, [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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._rebuildNodes(0);
|
||||
}
|
||||
|
||||
_renderText() {
|
||||
if(!this.state.formatted) {
|
||||
this._reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const formatted = this.state.formatted;
|
||||
this._rebuildNodes(formatted.length);
|
||||
|
||||
this.nodes.forEach(({text, element}, i) => {
|
||||
const ln = formatted[i].reduce((v, pt) => v + pt.text, '');
|
||||
text.nodeValue = ln;
|
||||
});
|
||||
}
|
||||
|
||||
_updateX() {
|
||||
this.nodes.forEach(({element}) => {
|
||||
element.setAttribute('x', this.state.x);
|
||||
});
|
||||
}
|
||||
|
||||
_updateY() {
|
||||
this.nodes.forEach(({element}, i) => {
|
||||
element.setAttribute('y', this.state.y + i);
|
||||
});
|
||||
}
|
||||
|
||||
firstLine() {
|
||||
if(this.nodes.length > 0) {
|
||||
return this.nodes[0].element;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
set(newState) {
|
||||
const oldState = Object.assign({}, this.state);
|
||||
merge(this.state, newState);
|
||||
|
||||
if(this.state.attrs !== oldState.attrs) {
|
||||
this._reset();
|
||||
oldState.formatted = EMPTY;
|
||||
}
|
||||
|
||||
const oldNodes = this.nodes.length;
|
||||
|
||||
if(this.state.formatted !== oldState.formatted) {
|
||||
this._renderText();
|
||||
}
|
||||
|
||||
if(this.state.x !== oldState.x) {
|
||||
this._updateX();
|
||||
}
|
||||
|
||||
if(this.state.y !== oldState.y || this.nodes.length !== oldNodes) {
|
||||
this._updateY();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SizeTester {
|
||||
constructor() {
|
||||
this.expected = new Set();
|
||||
this.measured = new Set();
|
||||
}
|
||||
|
||||
_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};
|
||||
}
|
||||
|
||||
expectMeasure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
this.expected.add(JSON.stringify(opts));
|
||||
}
|
||||
|
||||
performMeasurementsPre() {
|
||||
}
|
||||
|
||||
performMeasurementsAct() {
|
||||
this.measured = new Set(this.expected);
|
||||
}
|
||||
|
||||
performMeasurementsPost() {
|
||||
}
|
||||
|
||||
measure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
|
||||
if(!this.measured.has(JSON.stringify(opts))) {
|
||||
throw new Error('Unexpected measurement', opts);
|
||||
}
|
||||
|
||||
if(!opts.formatted || !opts.formatted.length) {
|
||||
return {width: 0, height: 0};
|
||||
}
|
||||
|
||||
let width = 0;
|
||||
opts.formatted.forEach((line) => {
|
||||
const length = line.reduce((v, pt) => v + pt.text.length, 0);
|
||||
width = Math.max(width, length);
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
height: opts.formatted.length,
|
||||
};
|
||||
}
|
||||
|
||||
measureHeight(attrs, formatted) {
|
||||
if(!formatted) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return formatted.length;
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
this.expected.clear();
|
||||
this.measured.clear();
|
||||
}
|
||||
}
|
||||
|
||||
SVGTextBlock.SizeTester = SizeTester;
|
||||
|
||||
return SVGTextBlock;
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
define([
|
||||
'core/documents/VirtualDocument',
|
||||
'core/DOMWrapper',
|
||||
], (
|
||||
VirtualDocument,
|
||||
DOMWrapper
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
class UnitaryTextSizer {
|
||||
// Simplified text sizer, which assumes all characters render as
|
||||
// 1x1 px squares for repeatable renders in all browsers
|
||||
|
||||
baseline() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
measureHeight({formatted}) {
|
||||
return formatted.length;
|
||||
}
|
||||
|
||||
prepMeasurement(attrs, formatted) {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
prepComplete() {
|
||||
}
|
||||
|
||||
performMeasurement(data) {
|
||||
return data.reduce((total, part) => total + part.text.length, 0);
|
||||
}
|
||||
|
||||
teardown() {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
VirtualDocument,
|
||||
UnitaryTextSizer,
|
||||
DOMWrapper,
|
||||
textSizerFactory: () => new UnitaryTextSizer(),
|
||||
dom: new DOMWrapper(new VirtualDocument()),
|
||||
};
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
define([], () => {
|
||||
define(() => {
|
||||
'use strict';
|
||||
|
||||
function Split(elements, options) {
|
||||
|
|
|
@ -0,0 +1,360 @@
|
|||
define([
|
||||
'./SVGTextBlock',
|
||||
'./PatternedLine',
|
||||
], (
|
||||
SVGTextBlock,
|
||||
PatternedLine
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
function calculateAnchor(x, attrs, padding) {
|
||||
let shift = 0;
|
||||
let anchorX = x;
|
||||
switch(attrs['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;
|
||||
}
|
||||
return {shift, anchorX};
|
||||
}
|
||||
|
||||
const defaultTextSizerFactory = (svg) => new SVGTextBlock.TextSizer(svg);
|
||||
|
||||
class TextSizerWrapper {
|
||||
constructor(sizer) {
|
||||
this.sizer = sizer;
|
||||
this.cache = new Map();
|
||||
this.active = null;
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
|
||||
_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);
|
||||
});
|
||||
}
|
||||
|
||||
performMeasurementsPost() {
|
||||
if(this.active.length) {
|
||||
this.sizer.teardown();
|
||||
}
|
||||
this.active = null;
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,130 +0,0 @@
|
|||
define([
|
||||
'./SVGUtilities',
|
||||
'./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)),
|
||||
]);
|
||||
}
|
||||
|
||||
function calculateAnchor(x, attrs, padding) {
|
||||
let shift = 0;
|
||||
let anchorX = x;
|
||||
switch(attrs['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;
|
||||
}
|
||||
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 {shift, anchorX} = calculateAnchor(x, labelAttrs, padding);
|
||||
|
||||
const label = new SVGTextBlockClass(labelLayer, {
|
||||
attrs: labelAttrs,
|
||||
formatted,
|
||||
x: anchorX,
|
||||
y: y + padding.top,
|
||||
});
|
||||
|
||||
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 - size.width * shift - padding.left,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
} else {
|
||||
box = renderBox(boxAttrs, {
|
||||
'x': anchorX - size.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,
|
||||
renderLine,
|
||||
renderNote,
|
||||
renderBoxedText,
|
||||
TextBlock: SVGTextBlock,
|
||||
PatternedLine,
|
||||
};
|
||||
});
|
|
@ -1,120 +0,0 @@
|
|||
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', () => {
|
||||
let o = null;
|
||||
let sizer = null;
|
||||
|
||||
beforeEach(() => {
|
||||
o = document.createElement('p');
|
||||
sizer = {
|
||||
measure: () => ({width: 64, height: 128}),
|
||||
};
|
||||
});
|
||||
|
||||
it('renders a label', () => {
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: '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,
|
||||
textSizer: sizer,
|
||||
});
|
||||
expect(rendered.label.state.formatted).toEqual([[{text: 'foo'}]]);
|
||||
expect(rendered.label.state.x).toEqual(5);
|
||||
expect(rendered.label.state.y).toEqual(10);
|
||||
expect(rendered.label.firstLine().parentNode).toEqual(o);
|
||||
});
|
||||
|
||||
it('positions a box beneath the rendered label', () => {
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: '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,
|
||||
textSizer: sizer,
|
||||
});
|
||||
expect(rendered.box.getAttribute('x')).toEqual('1');
|
||||
expect(rendered.box.getAttribute('y')).toEqual('2');
|
||||
expect(rendered.box.getAttribute('width'))
|
||||
.toEqual(String(4 + 16 + 64));
|
||||
expect(rendered.box.getAttribute('height'))
|
||||
.toEqual(String(8 + 32 + 128));
|
||||
expect(rendered.box.getAttribute('foo')).toEqual('bar');
|
||||
expect(rendered.box.parentNode).toEqual(o);
|
||||
});
|
||||
|
||||
it('returns the size of the rendered box', () => {
|
||||
const rendered = SVGShapes.renderBoxedText([[{text: '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,
|
||||
textSizer: sizer,
|
||||
});
|
||||
expect(rendered.width).toEqual(4 + 16 + 64);
|
||||
expect(rendered.height).toEqual(8 + 32 + 128);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,18 +1,9 @@
|
|||
define(['./SVGUtilities'], (svg) => {
|
||||
define(() => {
|
||||
'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)) {
|
||||
|
@ -23,17 +14,15 @@ define(['./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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -41,8 +30,9 @@ define(['./SVGUtilities'], (svg) => {
|
|||
const EMPTY = [];
|
||||
|
||||
class SVGTextBlock {
|
||||
constructor(container, initialState = {}) {
|
||||
constructor(container, svg, initialState = {}) {
|
||||
this.container = container;
|
||||
this.svg = svg;
|
||||
this.state = {
|
||||
attrs: {},
|
||||
formatted: EMPTY,
|
||||
|
@ -55,19 +45,18 @@ define(['./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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,8 +81,8 @@ define(['./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;
|
||||
}
|
||||
});
|
||||
|
@ -101,22 +90,18 @@ define(['./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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,160 +130,47 @@ define(['./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;
|
||||
baseline({attrs}) {
|
||||
return Number(attrs['font-size']);
|
||||
}
|
||||
|
||||
const attrKey = JSON.stringify(attrs);
|
||||
let attrCache = this.cache.get(attrKey);
|
||||
if(!attrCache) {
|
||||
attrCache = {
|
||||
attrs,
|
||||
lines: new Map(),
|
||||
measureHeight({attrs, formatted}) {
|
||||
const size = this.baseline({attrs, formatted});
|
||||
const lineHeight = size * (Number(attrs['line-height']) || 1);
|
||||
return formatted.length * lineHeight;
|
||||
}
|
||||
|
||||
prepMeasurement(attrs, formatted) {
|
||||
const node = this.svg.el('text')
|
||||
.attrs(attrs)
|
||||
.attach(this.testers);
|
||||
populateSvgTextLine(this.svg, node, formatted);
|
||||
return node;
|
||||
}
|
||||
|
||||
prepComplete() {
|
||||
this.container.add(this.testers);
|
||||
}
|
||||
|
||||
performMeasurement(node) {
|
||||
return node.element.getComputedTextLength();
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this.container.del(this.testers.empty());
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
_measureHeight({attrs, formatted}) {
|
||||
return formatted.length * fontDetails(attrs).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;
|
||||
}
|
||||
|
||||
_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) {
|
||||
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};
|
||||
}
|
||||
|
||||
expectMeasure(attrs, formatted) {
|
||||
const opts = this._getMeasurementOpts(attrs, formatted);
|
||||
this._expectMeasure(opts);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
defineDescribe('SVGTextBlock', [
|
||||
'./SVGTextBlock',
|
||||
'./SVGUtilities',
|
||||
'./SVG',
|
||||
'stubs/TestDOM',
|
||||
], (
|
||||
SVGTextBlock,
|
||||
svg
|
||||
SVG,
|
||||
TestDOM
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
|
||||
const attrs = {'font-size': 10, 'line-height': 1.5};
|
||||
let hold = null;
|
||||
let svg = null;
|
||||
let block = null;
|
||||
let hold = null;
|
||||
|
||||
beforeEach(() => {
|
||||
hold = svg.makeContainer();
|
||||
document.body.appendChild(hold);
|
||||
block = new SVGTextBlock(hold, {attrs});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(hold);
|
||||
svg = new SVG(TestDOM.dom, TestDOM.textSizerFactory);
|
||||
block = new SVGTextBlock(svg.body, svg, {attrs});
|
||||
hold = svg.body.element;
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
|
@ -26,28 +26,28 @@ defineDescribe('SVGTextBlock', [
|
|||
expect(block.state.formatted).toEqual([]);
|
||||
expect(block.state.x).toEqual(0);
|
||||
expect(block.state.y).toEqual(0);
|
||||
expect(hold.children.length).toEqual(0);
|
||||
expect(hold.childNodes.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('does not explode if given no setup', () => {
|
||||
block = new SVGTextBlock(hold);
|
||||
block = new SVGTextBlock(svg.body, svg);
|
||||
expect(block.state.formatted).toEqual([]);
|
||||
expect(block.state.x).toEqual(0);
|
||||
expect(block.state.y).toEqual(0);
|
||||
expect(hold.children.length).toEqual(0);
|
||||
expect(hold.childNodes.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('adds the given formatted text if specified', () => {
|
||||
block = new SVGTextBlock(hold, {
|
||||
block = new SVGTextBlock(svg.body, svg, {
|
||||
attrs,
|
||||
formatted: [[{text: 'abc'}]],
|
||||
});
|
||||
expect(block.state.formatted).toEqual([[{text: 'abc'}]]);
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(hold.childNodes.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('uses the given coordinates if specified', () => {
|
||||
block = new SVGTextBlock(hold, {attrs, x: 5, y: 7});
|
||||
block = new SVGTextBlock(svg.body, svg, {attrs, x: 5, y: 7});
|
||||
expect(block.state.x).toEqual(5);
|
||||
expect(block.state.y).toEqual(7);
|
||||
});
|
||||
|
@ -57,21 +57,21 @@ defineDescribe('SVGTextBlock', [
|
|||
it('sets the text to the given content', () => {
|
||||
block.set({formatted: [[{text: 'foo'}]]});
|
||||
expect(block.state.formatted).toEqual([[{text: 'foo'}]]);
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(hold.children[0].innerHTML).toEqual('foo');
|
||||
expect(hold.childNodes.length).toEqual(1);
|
||||
expect(hold.childNodes[0].innerHTML).toEqual('foo');
|
||||
});
|
||||
|
||||
it('renders multiline text', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
expect(hold.children.length).toEqual(2);
|
||||
expect(hold.children[0].innerHTML).toEqual('foo');
|
||||
expect(hold.children[1].innerHTML).toEqual('bar');
|
||||
expect(hold.childNodes.length).toEqual(2);
|
||||
expect(hold.childNodes[0].innerHTML).toEqual('foo');
|
||||
expect(hold.childNodes[1].innerHTML).toEqual('bar');
|
||||
});
|
||||
|
||||
it('re-uses text nodes when possible, adding more if needed', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
const line0 = hold.children[0];
|
||||
const line1 = hold.children[1];
|
||||
const line0 = hold.childNodes[0];
|
||||
const line1 = hold.childNodes[1];
|
||||
|
||||
block.set({formatted: [
|
||||
[{text: 'zig'}],
|
||||
|
@ -79,77 +79,83 @@ defineDescribe('SVGTextBlock', [
|
|||
[{text: 'baz'}],
|
||||
]});
|
||||
|
||||
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');
|
||||
expect(hold.childNodes.length).toEqual(3);
|
||||
expect(hold.childNodes[0]).toEqual(line0);
|
||||
expect(hold.childNodes[0].innerHTML).toEqual('zig');
|
||||
expect(hold.childNodes[1]).toEqual(line1);
|
||||
expect(hold.childNodes[1].innerHTML).toEqual('zag');
|
||||
expect(hold.childNodes[2].innerHTML).toEqual('baz');
|
||||
});
|
||||
|
||||
it('re-uses text nodes when possible, removing extra if needed', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
const line0 = hold.children[0];
|
||||
const line0 = hold.childNodes[0];
|
||||
|
||||
block.set({formatted: [[{text: 'zig'}]]});
|
||||
|
||||
expect(hold.children.length).toEqual(1);
|
||||
expect(hold.children[0]).toEqual(line0);
|
||||
expect(hold.children[0].innerHTML).toEqual('zig');
|
||||
expect(hold.childNodes.length).toEqual(1);
|
||||
expect(hold.childNodes[0]).toEqual(line0);
|
||||
expect(hold.childNodes[0].innerHTML).toEqual('zig');
|
||||
});
|
||||
|
||||
it('positions text nodes and applies attributes', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
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');
|
||||
expect(hold.childNodes.length).toEqual(2);
|
||||
expect(hold.childNodes[0].getAttribute('x')).toEqual('0');
|
||||
expect(hold.childNodes[0].getAttribute('y')).toEqual('1');
|
||||
expect(hold.childNodes[0].getAttribute('font-size')).toEqual('10');
|
||||
expect(hold.childNodes[1].getAttribute('x')).toEqual('0');
|
||||
expect(hold.childNodes[1].getAttribute('y')).toEqual('2');
|
||||
expect(hold.childNodes[1].getAttribute('font-size')).toEqual('10');
|
||||
});
|
||||
|
||||
it('moves all nodes', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
block.set({x: 5, y: 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');
|
||||
expect(hold.childNodes[0].getAttribute('x')).toEqual('5');
|
||||
expect(hold.childNodes[0].getAttribute('y')).toEqual('8');
|
||||
expect(hold.childNodes[1].getAttribute('x')).toEqual('5');
|
||||
expect(hold.childNodes[1].getAttribute('y')).toEqual('9');
|
||||
});
|
||||
|
||||
it('clears if the text is empty', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
block.set({formatted: []});
|
||||
expect(hold.children.length).toEqual(0);
|
||||
expect(hold.childNodes.length).toEqual(0);
|
||||
expect(block.state.formatted).toEqual([]);
|
||||
expect(block.lines.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SizeTester', () => {
|
||||
let tester = null;
|
||||
|
||||
describe('TextSizer', () => {
|
||||
beforeEach(() => {
|
||||
tester = new SVGTextBlock.SizeTester(hold);
|
||||
svg = new SVG(
|
||||
new TestDOM.DOMWrapper(window.document),
|
||||
(svgBase) => new SVGTextBlock.TextSizer(svgBase)
|
||||
);
|
||||
document.body.appendChild(svg.body.element);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(svg.body.element);
|
||||
});
|
||||
|
||||
describe('.measure', () => {
|
||||
it('calculates the size of the formatted text', () => {
|
||||
const size = tester.measure(attrs, [[{text: 'foo'}]]);
|
||||
const size = svg.textSizer.measure(attrs, [[{text: 'foo'}]]);
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
expect(size.height).toEqual(15);
|
||||
});
|
||||
|
||||
it('calculates the size of text blocks', () => {
|
||||
block.set({formatted: [[{text: 'foo'}]]});
|
||||
const size = tester.measure(block);
|
||||
const size = svg.textSizer.measure(block);
|
||||
expect(size.width).toBeGreaterThan(0);
|
||||
expect(size.height).toEqual(15);
|
||||
});
|
||||
|
||||
it('measures multiline text', () => {
|
||||
const size = tester.measure(attrs, [
|
||||
const size = svg.textSizer.measure(attrs, [
|
||||
[{text: 'foo'}],
|
||||
[{text: 'bar'}],
|
||||
]);
|
||||
|
@ -158,19 +164,19 @@ defineDescribe('SVGTextBlock', [
|
|||
});
|
||||
|
||||
it('returns 0, 0 for empty content', () => {
|
||||
const size = tester.measure(attrs, []);
|
||||
const size = svg.textSizer.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, [
|
||||
const size0 = svg.textSizer.measure(attrs, [
|
||||
[{text: 'foo'}],
|
||||
]);
|
||||
const size1 = tester.measure(attrs, [
|
||||
const size1 = svg.textSizer.measure(attrs, [
|
||||
[{text: 'longline'}],
|
||||
]);
|
||||
const size = tester.measure(attrs, [
|
||||
const size = svg.textSizer.measure(attrs, [
|
||||
[{text: 'foo'}],
|
||||
[{text: 'longline'}],
|
||||
[{text: 'foo'}],
|
||||
|
@ -182,14 +188,14 @@ defineDescribe('SVGTextBlock', [
|
|||
|
||||
describe('.measureHeight', () => {
|
||||
it('calculates the height of the rendered text', () => {
|
||||
const height = tester.measureHeight(attrs, [
|
||||
const height = svg.textSizer.measureHeight(attrs, [
|
||||
[{text: 'foo'}],
|
||||
]);
|
||||
expect(height).toEqual(15);
|
||||
});
|
||||
|
||||
it('measures multiline text', () => {
|
||||
const height = tester.measureHeight(attrs, [
|
||||
const height = svg.textSizer.measureHeight(attrs, [
|
||||
[{text: 'foo'}],
|
||||
[{text: 'bar'}],
|
||||
]);
|
||||
|
@ -197,13 +203,13 @@ defineDescribe('SVGTextBlock', [
|
|||
});
|
||||
|
||||
it('returns 0 for empty content', () => {
|
||||
const height = tester.measureHeight(attrs, []);
|
||||
const height = svg.textSizer.measureHeight(attrs, []);
|
||||
expect(height).toEqual(0);
|
||||
});
|
||||
|
||||
it('does not require the container', () => {
|
||||
tester.measureHeight(attrs, [[{text: 'foo'}]]);
|
||||
expect(hold.children.length).toEqual(0);
|
||||
svg.textSizer.measureHeight(attrs, [[{text: 'foo'}]]);
|
||||
expect(svg.body.element.childNodes.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
define(() => {
|
||||
'use strict';
|
||||
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
function makeText(text = '') {
|
||||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
function setAttributes(target, attrs) {
|
||||
for(const k in attrs) {
|
||||
if(attrs.hasOwnProperty(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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
makeText,
|
||||
make,
|
||||
makeContainer,
|
||||
setAttributes,
|
||||
empty,
|
||||
};
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
defineDescribe('SVGUtilities', ['./SVGUtilities'], (svg) => {
|
||||
'use strict';
|
||||
|
||||
const expectedNS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
describe('.makeText', () => {
|
||||
it('creates a text node with the given content', () => {
|
||||
const node = svg.makeText('foo');
|
||||
expect(node.nodeValue).toEqual('foo');
|
||||
});
|
||||
|
||||
it('defaults to empty', () => {
|
||||
const node = svg.makeText();
|
||||
expect(node.nodeValue).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.make', () => {
|
||||
it('creates a node with the SVG namespace', () => {
|
||||
const node = svg.make('path');
|
||||
expect(node.namespaceURI).toEqual(expectedNS);
|
||||
expect(node.tagName).toEqual('path');
|
||||
});
|
||||
|
||||
it('assigns the given attributes', () => {
|
||||
const node = svg.make('path', {'foo': 'bar'});
|
||||
expect(node.getAttribute('foo')).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.makeContainer', () => {
|
||||
it('creates an svg node with the SVG namespace', () => {
|
||||
const node = svg.makeContainer();
|
||||
expect(node.namespaceURI).toEqual(expectedNS);
|
||||
expect(node.getAttribute('xmlns')).toEqual(expectedNS);
|
||||
expect(node.getAttribute('version')).toEqual('1.1');
|
||||
expect(node.tagName).toEqual('svg');
|
||||
});
|
||||
|
||||
it('assigns the given attributes', () => {
|
||||
const node = svg.makeContainer({'foo': 'bar'});
|
||||
expect(node.getAttribute('foo')).toEqual('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.empty', () => {
|
||||
it('removes all child nodes from the given node', () => {
|
||||
const node = document.createElement('p');
|
||||
const a = document.createElement('p');
|
||||
const b = document.createElement('p');
|
||||
node.appendChild(a);
|
||||
node.appendChild(b);
|
||||
|
||||
svg.empty(node);
|
||||
expect(node.children.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,150 @@
|
|||
defineDescribe('SVG', [
|
||||
'./SVG',
|
||||
'stubs/TestDOM',
|
||||
], (
|
||||
SVG,
|
||||
TestDOM
|
||||
) => {
|
||||
'use strict';
|
||||
|
||||
const expectedNS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
const svg = new SVG(TestDOM.dom, TestDOM.textSizerFactory);
|
||||
|
||||
describe('.txt', () => {
|
||||
it('creates a text node with the given content', () => {
|
||||
const node = svg.txt('foo');
|
||||
expect(node.nodeValue).toEqual('foo');
|
||||
});
|
||||
|
||||
it('defaults to empty', () => {
|
||||
const node = svg.txt();
|
||||
expect(node.nodeValue).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.el', () => {
|
||||
it('creates a wrapped node with the SVG namespace', () => {
|
||||
const node = svg.el('path').element;
|
||||
expect(node.namespaceURI).toEqual(expectedNS);
|
||||
expect(node.tagName).toEqual('path');
|
||||
});
|
||||
|
||||
it('overrides the namespace if desired', () => {
|
||||
const node = svg.el('path', 'foo').element;
|
||||
expect(node.namespaceURI).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.body', () => {
|
||||
it('is a wrapped svg node with the SVG namespace', () => {
|
||||
const node = svg.body.element;
|
||||
expect(node.namespaceURI).toEqual(expectedNS);
|
||||
expect(node.getAttribute('xmlns')).toEqual(expectedNS);
|
||||
expect(node.getAttribute('version')).toEqual('1.1');
|
||||
expect(node.tagName).toEqual('svg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.box', () => {
|
||||
it('returns a wrapped simple rect SVG element', () => {
|
||||
const node = svg.box({
|
||||
'foo': 'bar',
|
||||
}, {
|
||||
'x': 10,
|
||||
'y': 20,
|
||||
'width': 30,
|
||||
'height': 40,
|
||||
}).element;
|
||||
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('.note', () => {
|
||||
it('returns a wrapped rectangle with a page flick', () => {
|
||||
const node = svg.note({
|
||||
'foo': 'bar',
|
||||
}, {
|
||||
'zig': 'zag',
|
||||
}, {
|
||||
'x': 10,
|
||||
'y': 20,
|
||||
'width': 30,
|
||||
'height': 40,
|
||||
}).element;
|
||||
expect(node.tagName).toEqual('g');
|
||||
expect(node.childNodes.length).toEqual(2);
|
||||
const back = node.childNodes[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.childNodes[1];
|
||||
expect(flick.getAttribute('zig')).toEqual('zag');
|
||||
expect(flick.getAttribute('points')).toEqual(
|
||||
'33 20 ' +
|
||||
'33 27 ' +
|
||||
'40 27'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.boxedText', () => {
|
||||
const PADDING = {left: 4, top: 8, right: 16, bottom: 32};
|
||||
const LABEL_ATTRS = {'font-size': 10, 'line-height': 1.5, 'foo': 'bar'};
|
||||
const LABEL = [[{text: 'foo'}]];
|
||||
|
||||
beforeEach(() => {
|
||||
svg.textSizer.expectMeasure(LABEL_ATTRS, LABEL);
|
||||
svg.textSizer.performMeasurements();
|
||||
});
|
||||
|
||||
it('renders a label', () => {
|
||||
const rendered = svg.boxedText({
|
||||
padding: PADDING,
|
||||
boxAttrs: {},
|
||||
labelAttrs: LABEL_ATTRS,
|
||||
}, LABEL, {x: 1, y: 2});
|
||||
const block = rendered.label.textBlock;
|
||||
|
||||
expect(block.state.formatted).toEqual([[{text: 'foo'}]]);
|
||||
expect(block.state.x).toEqual(5);
|
||||
expect(block.state.y).toEqual(10);
|
||||
});
|
||||
|
||||
it('positions a box beneath the rendered label', () => {
|
||||
const rendered = svg.boxedText({
|
||||
padding: PADDING,
|
||||
boxAttrs: {'foo': 'bar'},
|
||||
labelAttrs: LABEL_ATTRS,
|
||||
}, LABEL, {x: 1, y: 2});
|
||||
const box = rendered.element.childNodes[0];
|
||||
|
||||
expect(box.getAttribute('x')).toEqual('1');
|
||||
expect(box.getAttribute('y')).toEqual('2');
|
||||
expect(box.getAttribute('width')).toEqual(String(4 + 16 + 3));
|
||||
expect(box.getAttribute('height')).toEqual(String(8 + 32 + 1));
|
||||
expect(box.getAttribute('foo')).toEqual('bar');
|
||||
});
|
||||
|
||||
it('returns the size of the rendered box', () => {
|
||||
const rendered = svg.boxedText({
|
||||
padding: PADDING,
|
||||
boxAttrs: {},
|
||||
labelAttrs: LABEL_ATTRS,
|
||||
}, LABEL, {x: 1, y: 2});
|
||||
|
||||
expect(rendered.width).toEqual(4 + 16 + 3);
|
||||
expect(rendered.height).toEqual(8 + 32 + 1);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue