Skip to content

Commit

Permalink
util.svg tagged template (#1711)
Browse files Browse the repository at this point in the history
  • Loading branch information
Geliogabalus authored Aug 4, 2022
1 parent 8549a96 commit 4e70b6f
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 39 deletions.
66 changes: 34 additions & 32 deletions demo/bandwidth/src/bandwidth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { dia, g: geometry, V: vectorizer } = joint;
const svg = joint.util.svg;

const y = 200;
const x1 = 100;
Expand Down Expand Up @@ -127,38 +128,39 @@ const Bandwidth = dia.Element.define('Bandwidth', {
}
}
}, {
markup: [{
tagName: 'circle',
className: 'bandwidth__halo',
selector: 'halo'
}, {
tagName: 'rect',
selector: 'body',
}, {
tagName: 'text',
selector: 'topLabel'
}, {
tagName: 'text',
selector: 'bottomLabel'
}, {
tagName: 'path',
className: 'bandwidth__sideband',
selector: 'lowerSideband',
groupSelector: 'sidebands'
}, {
tagName: 'path',
className: 'bandwidth__sideband',
selector: 'upperSideband',
groupSelector: 'sidebands'
}, {
tagName: 'path',
selector: 'carrierFrequencyBandwidth',
groupSelector: 'frequencyMarkers'
}, {
tagName: 'path',
selector: 'carrierFrequency',
groupSelector: 'frequencyMarkers',
}],
markup: svg`
<circle
@selector="halo"
class="bandwidth__halo"
/>
<rect
@selector="body"
/>
<text
@selector="topLabel"
/>
<text
@selector="bottomLabel"
/>
<path
@selector="lowerSideband"
@group-selector="sidebands"
class="bandwidth__sideband"
/>
<path
@selector="upperSideband"
@group-selector="sidebands"
class="bandwidth__sideband"
/>
<path
@selector="carrierFrequencyBandwidth"
@group-selector="frequencyMarkers"
/>
<path
@selector="carrierFrequency"
@group-selector="frequencyMarkers"
/>
`,

updateFrequencyLabel(opt) {
const { x } = this.getBBox().center();
Expand Down
48 changes: 41 additions & 7 deletions docs/src/joint/api/dia/Cell/markup.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<pre class="docs-method-signature"><code>markup</code></pre>

<p>
Either an XML string or JSON markup specifying an array of JSON elements.
Either an XML string or JSON markup specifying an array of JSON elements. Also JSON markup can be described using an <a href="#util.svg">svg</a> tagged template.
Used as a template to build DOM Elements on the fly when the associated cellView is rendered.
</p>

Expand Down Expand Up @@ -67,29 +67,63 @@ <h4 id="dia.Cell.markup.json">JSON Markup</h4>
}]
}]</code></pre>

<h4 id="dia.Cell.markup.json">JSON Markup using tagged template</h4>

<p>
JSON markup also can be defined using an <a href="#util.svg">svg</a> tagged template. This tagged template converts SVG representation of the markup into a JSON markup object.
Let's write the previous example using an svg tagged template:
</p>

<pre><code>
const svg = joint.util.svg;

// single DOM element
markup: svg`&ltrect\&gt`

// multiple DOM elements
markup: svg`
&ltrect @selector="body"\&gt
&lttext
@selector="label"
stroke="none"
\&gt`

// nested DOM elements
markup: svg`
&ltg&gt
&ltcircle
@selector="circle1"
@group-selector="circles"
/&gt
&ltcircle
@selector="circle2"
@group-selector="circles"
/&gt
&lt/g&gt`</code></pre>

<h5 id="dia.Cell.markup.attributes">JSON Markup attributes</h5>

<p>
Anything you define in <code>markup</code> is evaluated once at CellView creation (the DOM elements and their attributes).
That means it's important to think about the runtime of your application. If you have SVG attributes that don't change
That means it's important to think about the runtime of your application. If you have SVG attributes that don't change
throughout the runtime, you can add them to the <code>markup</code>.
</p>

<p>
As <code>markup</code> is something all instances of the element type are expected to have in common, inheriting from
the subtype prototype is more efficient. Nevertheless, it is still possible to provide custom markup to individual instances
As <code>markup</code> is something all instances of the element type are expected to have in common, inheriting from
the subtype prototype is more efficient. Nevertheless, it is still possible to provide custom markup to individual instances
of your class by providing markup later.
</p>

<p>
Anything in the <code>attrs</code> attribute is evaluated on every change of the model (e.g. a <code>resize</code> or
Anything in the <code>attrs</code> attribute is evaluated on every change of the model (e.g. a <code>resize</code> or
an <code>attrs</code> change). As JointJS <a target="_blank" href="https://resources.jointjs.com/tutorial/special-attributes">
special attributes</a> mostly depend on the current state of the model (<code>size</code>, <code>attrs</code>, <code>angle</code>),
special attributes</a> mostly depend on the current state of the model (<code>size</code>, <code>attrs</code>, <code>angle</code>),
they should always be defined inside <code>attrs</code>.
</p>

<p>
SVG attributes that are modified at some point in the application should also be added to <code>attrs</code> (e.g. user
SVG attributes that are modified at some point in the application should also be added to <code>attrs</code> (e.g. user
changes the color of the element).
</p>

Expand Down
49 changes: 49 additions & 0 deletions docs/src/joint/api/util/svg.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<pre class="docs-method-signature"><code>svg(strings)</code></pre>
<p>
This is the tagged template which converts a string into a <a href="#dia.Cell.markup">markup</a> object while declaring a <a href="#dia.Cell">Cell</a>.
The resulting object contains fields which are described in <a href="#dia.Cell.markup.json">this part of the documentation.</a>
SVG is converted into JSON markup as follows:
<table>
<tr>
<th>Property</th>
<th>Representation</th>
</tr>
<tr>
<td>tagName</td>
<td>SVG tag name.</td>
</tr>
<tr>
<td>selector</td>
<td><code>@selector</code> attribute.</td>
</tr>
<tr>
<td>groupSelector</td>
<td><code>@group-selector</code> attribute. Accepts comma-separated lists e.g. <code>@group-selector="group1, group2"</code>.</td>
</tr>
<tr>
<td>namespaceURI</td>
<td>The namespace URI of the element. It defaults to the SVG namespace <a target="_blank" href="https://www.w3.org/2000/svg"><code>"http://www.w3.org/2000/svg"</code></a>.</td>
</tr>
<tr>
<td>attributes</td>
<td>Attributes of the element.</td>
</tr>
<tr>
<td>style</td>
<td>The <code>style</code> attribute of the element parsed as key-value pairs.</td>
</tr>
<tr>
<td>className</td>
<td>The <code>class</code> attribute of the element.</td>
</tr>
<tr>
<td>children</td>
<td>The children of the element.</td>
</tr>
<tr>
<td>textContent</td>
<td>The text content of the element.</td>
</tr>
</table>
</p>

1 change: 1 addition & 0 deletions src/util/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './wrappers.mjs';
export * from './util.mjs';
export * from './cloneCells.mjs';
export * from './getRectPoint.mjs';
export * from './svgTagTemplate.mjs';
76 changes: 76 additions & 0 deletions src/util/svgTagTemplate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
export function svg(strings) {
const markup = parseFromSVGString(strings[0]);
return markup;
}

function parseFromSVGString(str) {
const parser = new DOMParser();
const markupString = `<svg>${str.trim()}</svg>`;
const xmldocument = parser.parseFromString(markupString.replace(/@/g, ''), 'application/xml');
if (xmldocument.getElementsByTagName('parsererror')[0]) {
throw new Error('Invalid SVG markup');
}
const document = parser.parseFromString(markupString, 'text/html');
const svg = document.querySelector('svg');
return build(svg);
}

function build(root) {
const markup = [];

Array.from(root.children).forEach(node => {
const markupNode = {};
const { tagName, attributes, textContent, namespaceURI, style } = node;

markupNode.tagName = tagName;
markupNode.namespaceURI = namespaceURI;

const stylesObject = {};
for (var i = style.length; i--;) {
var nameString = style[i];
stylesObject[nameString] = style.getPropertyValue(nameString);
}
markupNode.style = stylesObject;

// selector fallbacks to tagName
const selectorAttribute = attributes.getNamedItem('@selector');
if (selectorAttribute) {
markupNode.selector = selectorAttribute.value;
attributes.removeNamedItem('@selector');
}

const groupSelectorAttribute = attributes.getNamedItem('@group-selector');
if (groupSelectorAttribute) {
const groupSelectors = groupSelectorAttribute.value.split(',');
markupNode.groupSelector = groupSelectors.map(s => s.trim());

attributes.removeNamedItem('@group-selector');
}

const className = attributes.getNamedItem('class');
markupNode.className = (className ? className.value : null);

if (textContent) {
markupNode.textContent = textContent;
}

const nodeAttrs = {};

Array.from(attributes).forEach(nodeAttribute => {
const { name, value } = nodeAttribute;
nodeAttrs[name] = value;
});

if (Object.keys(nodeAttrs).length > 0) {
markupNode.attributes = nodeAttrs;
}

if (node.childElementCount > 0) {
markupNode.children = build(node);
}

markup.push(markupNode);
});

return markup;
}
16 changes: 16 additions & 0 deletions test/jointjs/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -1347,4 +1347,20 @@ QUnit.module('util', function(hooks) {
assert.equal(util.getRectPoint(rect, 'rightMiddle').toString(), '20@19.5');
assert.equal(util.getRectPoint(rect, 'bottomMiddle').toString(), '13.5@28');
});

// Temporary commented due to old version of browser in PhantomJS. Uncomment when test will be moved to Puppeteer
/* QUnit.test('svgTaggedTemplate', function(assert) {
var markup = joint.util.svg(['<rect @selector="selector1"/><circle @group-selector="group-selector1, group-selector2" class="circle"/><g><rect style="pointer-events:auto"/><circle stroke="red"/>textContent</g>']);
assert.equal(markup.length, 3);
assert.equal(markup[0].namespaceURI, 'http://www.w3.org/2000/svg');
assert.equal(markup[0].tagName, 'rect');
assert.equal(markup[0].selector, 'selector1');
assert.equal(markup[1].groupSelector[0], 'group-selector1');
assert.equal(markup[1].groupSelector[1], 'group-selector2');
assert.equal(markup[1].className, 'circle');
assert.equal(markup[2].children.length, 2);
assert.equal(markup[2].textContent, 'textContent');
assert.equal(markup[2].children[0].style['pointer-events'], 'auto');
assert.equal(markup[2].children[1].attributes['stroke'], 'red');
}); */
});
2 changes: 2 additions & 0 deletions types/joint.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2786,6 +2786,8 @@ export namespace util {

export function uuid(): string;

export function svg(strings: TemplateStringsArray): dia.MarkupJSON;

export function guid(obj?: { [key: string]: any }): string;

export function toKebabCase(str: string): string;
Expand Down

0 comments on commit 4e70b6f

Please sign in to comment.