First cut of SVG rendering components
These still need work, but they're functional enough to render a diagram
This commit is contained in:
parent
3fdc74bdf2
commit
82b780e9c3
@ -36,6 +36,7 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"transform-runtime",
|
"transform-runtime",
|
||||||
"transform-class-properties",
|
"transform-class-properties",
|
||||||
|
"transform-object-rest-spread",
|
||||||
"syntax-dynamic-import"
|
"syntax-dynamic-import"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -120,6 +121,7 @@
|
|||||||
"babel-loader": "^7.1.2",
|
"babel-loader": "^7.1.2",
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.6.1",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
@ -142,6 +144,7 @@
|
|||||||
"i18next": "^10.3.0",
|
"i18next": "^10.3.0",
|
||||||
"i18next-browser-languagedetector": "^2.1.0",
|
"i18next-browser-languagedetector": "^2.1.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"immutable": "^3.8.2",
|
||||||
"jest": "^22.2.2",
|
"jest": "^22.2.2",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"postcss-cssnext": "^3.1.0",
|
"postcss-cssnext": "^3.1.0",
|
||||||
|
@ -44,5 +44,10 @@ exports[`App rendering 1`] = `
|
|||||||
Sample warning message
|
Sample warning message
|
||||||
</p>
|
</p>
|
||||||
</Message>
|
</Message>
|
||||||
|
<div
|
||||||
|
className="render"
|
||||||
|
>
|
||||||
|
Testing image
|
||||||
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
`;
|
`;
|
||||||
|
@ -1,32 +1,46 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from './style.css';
|
||||||
|
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import Message from 'components/Message';
|
import Message from 'components/Message';
|
||||||
|
import renderImage from 'components/SVG';
|
||||||
|
import { syntaxes, demoImage } from 'devel';
|
||||||
|
|
||||||
const syntaxes = {
|
class App extends React.PureComponent {
|
||||||
js: 'JavaScript',
|
state = {
|
||||||
pcre: 'PCRE'
|
syntaxes,
|
||||||
};
|
image: demoImage,
|
||||||
|
downloadUrls: [
|
||||||
const downloadUrls = [
|
|
||||||
{ url: '#svg', filename: 'image.svg', type: 'image/svg+xml', label: 'Download SVG' },
|
{ url: '#svg', filename: 'image.svg', type: 'image/svg+xml', label: 'Download SVG' },
|
||||||
{ url: '#png', filename: 'image.png', type: 'image/png', label: 'Download PNG' }
|
{ url: '#png', filename: 'image.png', type: 'image/png', label: 'Download PNG' }
|
||||||
];
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = ({ expr, syntax}) => console.log(syntax, expr); // eslint-disable-line no-console
|
handleSubmit = ({expr, syntax}) => {
|
||||||
|
console.log(syntax, expr); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
const App = () => <React.Fragment>
|
render() {
|
||||||
|
const { downloadUrls, syntaxes, image } = this.state;
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
<Form
|
<Form
|
||||||
syntaxes={ syntaxes }
|
syntaxes={ syntaxes }
|
||||||
downloadUrls={ downloadUrls }
|
downloadUrls={ downloadUrls }
|
||||||
permalinkUrl="#permalink"
|
permalinkUrl="#permalink"
|
||||||
onSubmit={ handleSubmit }/>
|
onSubmit={ this.handleSubmit }/>
|
||||||
<Message type="error" heading="Sample Error">
|
<Message type="error" heading="Sample Error">
|
||||||
<p>Sample error message</p>
|
<p>Sample error message</p>
|
||||||
</Message>
|
</Message>
|
||||||
<Message type="warning" heading="Sample Warning">
|
<Message type="warning" heading="Sample Warning">
|
||||||
<p>Sample warning message</p>
|
<p>Sample warning message</p>
|
||||||
</Message>
|
</Message>
|
||||||
</React.Fragment>;
|
<div className={ style.render }>
|
||||||
|
{ renderImage(image) }
|
||||||
|
</div>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
17
src/components/App/style.css
Normal file
17
src/components/App/style.css
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@import url('../../globals.css');
|
||||||
|
|
||||||
|
.render {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--color-white);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
margin: var(--spacing-margin) 0;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
display: block;
|
||||||
|
transform: scaleZ(1); /* Move to separate render layer in Chrome */
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,14 @@
|
|||||||
|
jest.mock('components/SVG');
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import App from 'components/App';
|
import App from 'components/App';
|
||||||
|
import renderImage from 'components/SVG';
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
test('rendering', () => {
|
test('rendering', () => {
|
||||||
|
renderImage.mockReturnValue('Testing image');
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<App/>
|
<App/>
|
||||||
);
|
);
|
||||||
|
83
src/components/SVG/Base.js
Normal file
83
src/components/SVG/Base.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Map } from 'immutable';
|
||||||
|
|
||||||
|
class Base extends React.PureComponent {
|
||||||
|
_currentBBox() {
|
||||||
|
return this.tempBBox ? this.tempBBox : (this.state || {}).bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBBox(box, recalculate = {}) {
|
||||||
|
let bbox = this._currentBBox() || Map({ width: 0, height: 0});
|
||||||
|
|
||||||
|
bbox = bbox.merge(box);
|
||||||
|
|
||||||
|
if (!bbox.has('axisY') || recalculate.axisY) {
|
||||||
|
bbox = bbox.set('axisY', bbox.get('height') / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bbox.has('axisX1') || recalculate.axisX1) {
|
||||||
|
bbox = bbox.set('axisX1', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bbox.has('axisX2') || recalculate.axisX2) {
|
||||||
|
bbox = bbox.set('axisX2', bbox.get('width'));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tempBBox = bbox; // Want to get the updated bbox while setState is pending
|
||||||
|
this.setState({ bbox }, () => {
|
||||||
|
this.tempBBox = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getBBox() {
|
||||||
|
const bbox = this._currentBBox() || Map();
|
||||||
|
return {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
axisY: 0,
|
||||||
|
axisX1: 0,
|
||||||
|
axisX2: 0,
|
||||||
|
...bbox.toJS()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async doPreReflow() {
|
||||||
|
const components = this.preReflow();
|
||||||
|
|
||||||
|
// No child components
|
||||||
|
if (components === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of child components
|
||||||
|
if (components.map) {
|
||||||
|
const componentsReflowed = await Promise.all(components.map(c => c.doReflow()));
|
||||||
|
|
||||||
|
return componentsReflowed.reduce((memo, value) => memo || value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One child component
|
||||||
|
return components.doReflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
async doReflow() {
|
||||||
|
const oldBBox = this._currentBBox();
|
||||||
|
const shouldReflow = await this.doPreReflow();
|
||||||
|
|
||||||
|
if (shouldReflow) {
|
||||||
|
this.reflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._currentBBox() !== oldBBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
preReflow() {
|
||||||
|
// Implemented in subclass, return array of components to reflow before this
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
// Implemented in subclasses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Base;
|
84
src/components/SVG/Box.js
Normal file
84
src/components/SVG/Box.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Base from './Base';
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
/** @extends React.PureComponent */
|
||||||
|
class Box extends Base {
|
||||||
|
static defaultProps = {
|
||||||
|
padding: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
preReflow() {
|
||||||
|
return this.contained;
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const { padding, useAnchors } = this.props;
|
||||||
|
const box = this.contained.getBBox();
|
||||||
|
const labelBox = this.label ? this.label.getBBox() : { width: 0, height: 0};
|
||||||
|
|
||||||
|
this.setBBox({
|
||||||
|
width: Math.max(box.width + 2 * padding, labelBox.width),
|
||||||
|
height: box.height + 2 * padding + labelBox.height,
|
||||||
|
axisY: (useAnchors ? box.axisY : box.height / 2) + padding + labelBox.height,
|
||||||
|
axisX1: useAnchors ? box.axisX1 + padding : 0,
|
||||||
|
axisX2: useAnchors ? box.axisX2 + padding : box.width + 2 * padding
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width: this.getBBox().width,
|
||||||
|
height: box.height + 2 * padding,
|
||||||
|
contentTransform: `translate(${ padding } ${ padding + labelBox.height })`,
|
||||||
|
rectTransform: `translate(0 ${ labelBox.height })`,
|
||||||
|
labelTransform: `translate(0 ${ labelBox.height })`
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
containedRef = contained => this.contained = contained
|
||||||
|
|
||||||
|
labelRef = label => this.label = label
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { style: propStyle, radius, label, children } = this.props;
|
||||||
|
const { width, height, labelTransform, rectTransform, contentTransform } = this.state || {};
|
||||||
|
|
||||||
|
const rectProps = {
|
||||||
|
style: propStyle,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rx: radius,
|
||||||
|
ry: radius,
|
||||||
|
transform: rectTransform
|
||||||
|
};
|
||||||
|
const textProps = {
|
||||||
|
transform: labelTransform,
|
||||||
|
style: style.infoText,
|
||||||
|
ref: this.labelRef
|
||||||
|
};
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<rect { ...rectProps } ></rect>
|
||||||
|
{ label && <text { ...textProps }>{ label }</text> }
|
||||||
|
<g transform={ contentTransform }>
|
||||||
|
{ React.cloneElement(React.Children.only(children), {
|
||||||
|
ref: this.containedRef
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box.propTypes = {
|
||||||
|
padding: PropTypes.number,
|
||||||
|
useAnchors: PropTypes.bool,
|
||||||
|
style: PropTypes.object,
|
||||||
|
radius: PropTypes.number,
|
||||||
|
label: PropTypes.string,
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Box;
|
107
src/components/SVG/HorizontalLayout.js
Normal file
107
src/components/SVG/HorizontalLayout.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
|
||||||
|
import Base from './Base';
|
||||||
|
import style from './style';
|
||||||
|
import Path from './path';
|
||||||
|
|
||||||
|
/** @extends React.PureComponent */
|
||||||
|
class HorizontalLayout extends Base {
|
||||||
|
static defaultProps = {
|
||||||
|
withConnectors: false,
|
||||||
|
spacing: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
childTransforms: List()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChildTransforms(childBoxes) {
|
||||||
|
return childBoxes.reduce((transforms, box, i) => (
|
||||||
|
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
|
||||||
|
), this.state.childTransforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConnectorPaths(childBoxes) {
|
||||||
|
let last = childBoxes[0];
|
||||||
|
|
||||||
|
return childBoxes.slice(1).reduce((path, box) => {
|
||||||
|
try {
|
||||||
|
return path
|
||||||
|
.moveTo({ x: last.offsetX + last.axisX2, y: this.getBBox().axisY })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX1 });
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
last = box;
|
||||||
|
}
|
||||||
|
}, new Path()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
preReflow() {
|
||||||
|
return this.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
if (this.children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const { spacing, withConnectors } = this.props;
|
||||||
|
|
||||||
|
const childBoxes = this.children.map(child => child.getBBox());
|
||||||
|
const verticalCenter = childBoxes.reduce((center, box) => Math.max(center, box.axisY), 0);
|
||||||
|
const width = childBoxes.reduce((width, box) => width + box.width, 0) + (childBoxes.length - 1) * spacing;
|
||||||
|
const height = childBoxes.reduce((ascHeight, box) => Math.max(ascHeight, box.axisY), 0) +
|
||||||
|
childBoxes.reduce((decHeight, box) => Math.max(decHeight, box.height - box.axisY), 0);
|
||||||
|
this.setBBox({ width, height, axisY: verticalCenter }, { axisX1: true, axisX2: true });
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
childBoxes.forEach(box => {
|
||||||
|
box.offsetX = offset;
|
||||||
|
box.offsetY = this.getBBox().axisY - box.axisY;
|
||||||
|
offset += box.width + spacing;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
childTransforms: this.updateChildTransforms(childBoxes),
|
||||||
|
connectorPaths: withConnectors ? this.updateConnectorPaths(childBoxes) : ''
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
childRef = i => child => this.children[i] = child
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children } = this.props;
|
||||||
|
const { childTransforms, connectorPaths } = this.state;
|
||||||
|
|
||||||
|
this.children = [];
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<path d={ connectorPaths } style={ style.connectors }></path>
|
||||||
|
{ React.Children.map(children, (child, i) => (
|
||||||
|
<g transform={ childTransforms.get(i) }>
|
||||||
|
{ React.cloneElement(child, {
|
||||||
|
ref: this.childRef(i)
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalLayout.propTypes = {
|
||||||
|
spacing: PropTypes.number,
|
||||||
|
withConnectors: PropTypes.bool,
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node
|
||||||
|
]).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HorizontalLayout;
|
103
src/components/SVG/Image.js
Normal file
103
src/components/SVG/Image.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Base from './Base';
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
const namespaceProps = {
|
||||||
|
'xmlns': 'http://www.w3.org/2000/svg',
|
||||||
|
'xmlns:cc': 'http://creativecommons.org/ns#',
|
||||||
|
'xmlns:rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
|
||||||
|
};
|
||||||
|
const metadata = `<rdf:rdf>
|
||||||
|
<cc:license rdf:about="http://creativecommons.org/licenses/by/3.0/">
|
||||||
|
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"></cc:permits>
|
||||||
|
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"></cc:permits>
|
||||||
|
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"></cc:requires>
|
||||||
|
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"></cc:requires>
|
||||||
|
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"></cc:permits>
|
||||||
|
</cc:license>
|
||||||
|
</rdf:rdf>`;
|
||||||
|
|
||||||
|
/** @extends React.PureComponent */
|
||||||
|
class Image extends Base {
|
||||||
|
static defaultProps = {
|
||||||
|
padding: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
publishedMarkup = ''
|
||||||
|
|
||||||
|
state = {
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.doReflow().then(() => this.publishMarkup());
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.doReflow().then(() => this.publishMarkup());
|
||||||
|
}
|
||||||
|
|
||||||
|
publishMarkup() {
|
||||||
|
const { onRender } = this.props;
|
||||||
|
const markup = this.svg.outerHTML;
|
||||||
|
|
||||||
|
if (onRender && this.publishedMarkup !== markup) {
|
||||||
|
this.publishedMarkup = markup;
|
||||||
|
onRender(this.svg.outerHTML);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preReflow() {
|
||||||
|
return this.contained;
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const { padding } = this.props;
|
||||||
|
const box = this.contained.getBBox();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width: Math.round(box.width + 2 * padding),
|
||||||
|
height: Math.round(box.height + 2 * padding)
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
containedRef = contained => this.contained = contained
|
||||||
|
|
||||||
|
svgRef = svg => this.svg = svg
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { width, height } = this.state;
|
||||||
|
const { padding, children } = this.props;
|
||||||
|
|
||||||
|
const svgProps = {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
viewBox: [0, 0, width, height].join(' '),
|
||||||
|
style: style.image,
|
||||||
|
ref: this.svgRef,
|
||||||
|
...namespaceProps
|
||||||
|
};
|
||||||
|
|
||||||
|
return <svg { ...svgProps }>
|
||||||
|
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
|
||||||
|
<g transform={ `translate(${ padding } ${ padding })` }>
|
||||||
|
{ React.cloneElement(React.Children.only(children), {
|
||||||
|
ref: this.containedRef
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image.propTypes = {
|
||||||
|
onRender: PropTypes.func,
|
||||||
|
padding: PropTypes.number,
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Image;
|
144
src/components/SVG/Loop.js
Normal file
144
src/components/SVG/Loop.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Base from './Base';
|
||||||
|
import style from './style';
|
||||||
|
import Path from './path';
|
||||||
|
|
||||||
|
const skipPath = (box, greedy) => {
|
||||||
|
const vert = Math.max(0, box.axisY - 10);
|
||||||
|
const horiz = box.width - 10;
|
||||||
|
|
||||||
|
let path = new Path({ relative: true });
|
||||||
|
|
||||||
|
if (!greedy) {
|
||||||
|
path
|
||||||
|
.moveTo({ x: 10, y: box.axisY + box.offsetY - 15, relative: false })
|
||||||
|
.lineTo({ x: 5, y: 5 })
|
||||||
|
.moveTo({ x: -5, y: -5 })
|
||||||
|
.lineTo({ x: -5, y: 5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
.moveTo({ x: 0, y: box.axisY + box.offsetY, relative: false })
|
||||||
|
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
|
||||||
|
.lineTo({ y: -vert })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: -10, x: 10, y: -10 })
|
||||||
|
.lineTo({ x: horiz })
|
||||||
|
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: 10 })
|
||||||
|
.lineTo({ y: vert })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const repeatPath = (box, greedy) => {
|
||||||
|
const vert = box.height - box.axisY - 10;
|
||||||
|
|
||||||
|
let path = new Path({ relative: true });
|
||||||
|
|
||||||
|
if (greedy) {
|
||||||
|
path
|
||||||
|
.moveTo({ x: box.offsetX + box.width + 10, y: box.axisY + box.offsetY + 15, relative: false })
|
||||||
|
.lineTo({ x: 5, y: -5 })
|
||||||
|
.moveTo({ x: -5, y: 5 })
|
||||||
|
.lineTo({ x: -5, y: -5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
.moveTo({ x: box.offsetX, y: box.axisY + box.offsetY, relative: false })
|
||||||
|
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: 10 })
|
||||||
|
.lineTo({ y: vert })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 })
|
||||||
|
.lineTo({ x: box.width })
|
||||||
|
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
|
||||||
|
.lineTo({ y: -vert })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: -10, x: -10, y: -10 });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @extends React.PureComponent */
|
||||||
|
class Loop extends Base {
|
||||||
|
get contentOffset() {
|
||||||
|
const { skip, repeat } = this.props;
|
||||||
|
|
||||||
|
if (skip) {
|
||||||
|
return { x: 15, y: 10 };
|
||||||
|
} else if (repeat) {
|
||||||
|
return { x: 10, y: 0 };
|
||||||
|
} else {
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preReflow() {
|
||||||
|
return this.contained;
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const { skip, repeat, greedy } = this.props;
|
||||||
|
const box = this.contained.getBBox();
|
||||||
|
const labelBox = this.label ? this.label.getBBox() : { width: 0, height: 0 };
|
||||||
|
|
||||||
|
let height = box.height + labelBox.height;
|
||||||
|
if (skip) {
|
||||||
|
height += 10;
|
||||||
|
}
|
||||||
|
if (repeat) {
|
||||||
|
height += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setBBox({
|
||||||
|
width: box.width + this.contentOffset.x * 2,
|
||||||
|
height,
|
||||||
|
axisY: box.axisY + this.contentOffset.y,
|
||||||
|
axisX1: box.axisX1 + this.contentOffset.x,
|
||||||
|
axisX2: box.axisX2 + this.contentOffset.x
|
||||||
|
});
|
||||||
|
|
||||||
|
box.offsetX = this.contentOffset.x;
|
||||||
|
box.offsetY = this.contentOffset.y;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
labelTransform: `translate(${ this.getBBox().width - labelBox.width - 10 } ${ this.getBBox().height + 2 })`,
|
||||||
|
loopPaths: [
|
||||||
|
skip && skipPath(box, greedy),
|
||||||
|
repeat && repeatPath(box, greedy)
|
||||||
|
].filter(Boolean).join('')
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
containedRef = contained => this.contained = contained
|
||||||
|
|
||||||
|
labelRef = label => this.label = label
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, children } = this.props;
|
||||||
|
const { loopPaths, labelTransform } = this.state || {};
|
||||||
|
|
||||||
|
const textProps = {
|
||||||
|
transform: labelTransform,
|
||||||
|
style: style.infoText,
|
||||||
|
ref: this.labelRef
|
||||||
|
};
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<path d={ loopPaths } style={ style.connectors }></path>
|
||||||
|
{ label && <text { ...textProps }>{ label }</text> }
|
||||||
|
<g transform={ `translate(${ this.contentOffset.x } ${ this.contentOffset.y })` }>
|
||||||
|
{ React.cloneElement(React.Children.only(children), {
|
||||||
|
ref: this.containedRef
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loop.propTypes = {
|
||||||
|
skip: PropTypes.bool,
|
||||||
|
repeat: PropTypes.bool,
|
||||||
|
greedy: PropTypes.bool,
|
||||||
|
label: PropTypes.string,
|
||||||
|
children: PropTypes.node.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loop;
|
46
src/components/SVG/Pin.js
Normal file
46
src/components/SVG/Pin.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Base from './Base';
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
/** @extends React.PureComponent */
|
||||||
|
class Pin extends Base {
|
||||||
|
static defaultProps = {
|
||||||
|
radius: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const { radius } = this.props;
|
||||||
|
|
||||||
|
this.setBBox({
|
||||||
|
width: radius * 2,
|
||||||
|
height: radius * 2
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
transform: `translate(${ radius } ${ radius })`
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { radius } = this.props;
|
||||||
|
const { transform } = this.state || {};
|
||||||
|
|
||||||
|
const circleProps = {
|
||||||
|
r: radius,
|
||||||
|
style: style.pin,
|
||||||
|
transform
|
||||||
|
};
|
||||||
|
|
||||||
|
return <circle { ...circleProps }></circle>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pin.propTypes = {
|
||||||
|
radius: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pin;
|
51
src/components/SVG/Text.js
Normal file
51
src/components/SVG/Text.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Base from './Base';
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
/** @extends React.PureComponent */
|
||||||
|
class Text extends Base {
|
||||||
|
reflow() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const box = this.text.getBBox();
|
||||||
|
|
||||||
|
this.setBBox({
|
||||||
|
width: box.width,
|
||||||
|
height: box.height
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
transform: `translate(${-box.x} ${-box.y})`
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
textRef = text => this.text = text
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { transform } = this.state || {};
|
||||||
|
const { style: styleProp, children } = this.props;
|
||||||
|
|
||||||
|
const textProps = {
|
||||||
|
style: { ...style.text, ...styleProp },
|
||||||
|
transform,
|
||||||
|
ref: this.textRef
|
||||||
|
};
|
||||||
|
|
||||||
|
return <text { ...textProps }>
|
||||||
|
{ children }
|
||||||
|
</text>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text.propTypes = {
|
||||||
|
style: PropTypes.object,
|
||||||
|
transform: PropTypes.string,
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node
|
||||||
|
]).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Text;
|
149
src/components/SVG/VerticalLayout.js
Normal file
149
src/components/SVG/VerticalLayout.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
|
||||||
|
import Base from './Base';
|
||||||
|
import style from './style';
|
||||||
|
import Path from './path';
|
||||||
|
|
||||||
|
const connectorMargin = 20;
|
||||||
|
|
||||||
|
/** @extends React.PureComponent */
|
||||||
|
class VerticalLayout extends Base {
|
||||||
|
static defaultProps = {
|
||||||
|
withConnectors: false,
|
||||||
|
spacing: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
childTransforms: List()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChildTransforms(childBoxes) {
|
||||||
|
return childBoxes.reduce((transforms, box, i) => (
|
||||||
|
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
|
||||||
|
), this.state.childTransforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCurve(box) {
|
||||||
|
const thisBox = this.getBBox();
|
||||||
|
const distance = Math.abs(box.offsetY + box.axisY - thisBox.axisY);
|
||||||
|
|
||||||
|
if (distance >= 15) {
|
||||||
|
const curve = (box.axisY + box.offsetY > thisBox.axisY) ? 10 : -10;
|
||||||
|
|
||||||
|
return new Path()
|
||||||
|
// Left
|
||||||
|
.moveTo({ x: 10, y: box.axisY + box.offsetY - curve })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: curve, x: 10, y: curve, relative: true })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX1 })
|
||||||
|
// Right
|
||||||
|
.moveTo({ x: thisBox.width - 10, y: box.axisY + box.offsetY - curve })
|
||||||
|
.quadraticCurveTo({ cx: 0, cy: curve, x: -10, y: curve, relative: true })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX2 });
|
||||||
|
} else {
|
||||||
|
const anchor = box.offsetY + box.axisY - thisBox.axisY;
|
||||||
|
|
||||||
|
return new Path()
|
||||||
|
// Left
|
||||||
|
.moveTo({ x: 0, y: thisBox.axisY })
|
||||||
|
.cubicCurveTo({ cx1: 15, cy1: 0, cx2: 10, cy2: anchor, x: 20, y: anchor, relative: true })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX1 })
|
||||||
|
// Right
|
||||||
|
.moveTo({ x: thisBox.width, y: thisBox.axisY })
|
||||||
|
.cubicCurveTo({ cx1: -15, cy1: 0, cx2: -10, cy2: anchor, x: -20, y: anchor, relative: true })
|
||||||
|
.lineTo({ x: box.offsetX + box.axisX2 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeSide(box) {
|
||||||
|
const thisBox = this.getBBox();
|
||||||
|
const distance = Math.abs(box.offsetY + box.axisY - thisBox.axisY);
|
||||||
|
|
||||||
|
if (distance >= 15) {
|
||||||
|
const shift = (box.offsetY + box.axisY > thisBox.axisY) ? 10 : -10;
|
||||||
|
const edge = box.offsetY + box.axisY - shift;
|
||||||
|
|
||||||
|
return new Path()
|
||||||
|
// Left
|
||||||
|
.moveTo({ x: 0, y: thisBox.axisY })
|
||||||
|
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: shift, relative: true })
|
||||||
|
.lineTo({ y: edge })
|
||||||
|
// Right
|
||||||
|
.moveTo({ x: thisBox.width, y: thisBox.axisY })
|
||||||
|
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: shift, relative: true })
|
||||||
|
.lineTo({ y: edge });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preReflow() {
|
||||||
|
return this.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
reflow() {
|
||||||
|
if (this.children.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const { spacing, withConnectors } = this.props;
|
||||||
|
|
||||||
|
const childBoxes = this.children.map(child => child.getBBox());
|
||||||
|
const horizontalCenter = childBoxes.reduce((center, box) => Math.max(center, box.width / 2), 0);
|
||||||
|
const margin = withConnectors ? connectorMargin : 0;
|
||||||
|
const width = childBoxes.reduce((width, box) => Math.max(width, box.width), 0) + 2 * margin;
|
||||||
|
const height = childBoxes.reduce((height, box) => height + box.height, 0) + (childBoxes.length - 1) * spacing;
|
||||||
|
this.setBBox({ width, height }, { axisY: true, axisX1: true, axisX2: true });
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
childBoxes.forEach(box => {
|
||||||
|
box.offsetX = horizontalCenter - box.width / 2 + margin;
|
||||||
|
box.offsetY = offset;
|
||||||
|
offset += spacing + box.height;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
childTransforms: this.updateChildTransforms(childBoxes),
|
||||||
|
connectorPaths: withConnectors ? [
|
||||||
|
...childBoxes.map(box => this.makeCurve(box)),
|
||||||
|
this.makeSide(childBoxes[0]),
|
||||||
|
this.makeSide(childBoxes[childBoxes.length - 1])
|
||||||
|
].join('') : ''
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
childRef = i => child => this.children[i] = child
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children } = this.props;
|
||||||
|
const { childTransforms, connectorPaths } = this.state;
|
||||||
|
|
||||||
|
this.children = [];
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
<path d={ connectorPaths } style={ style.connectors }></path>
|
||||||
|
{ React.Children.map(children, (child, i) => (
|
||||||
|
<g transform={ childTransforms.get(i) }>
|
||||||
|
{ React.cloneElement(child, {
|
||||||
|
ref: this.childRef(i)
|
||||||
|
}) }
|
||||||
|
</g>
|
||||||
|
)) }
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout.propTypes = {
|
||||||
|
spacing: PropTypes.number,
|
||||||
|
withConnectors: PropTypes.bool,
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node
|
||||||
|
]).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerticalLayout;
|
41
src/components/SVG/index.js
Normal file
41
src/components/SVG/index.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Box from './Box';
|
||||||
|
import HorizontalLayout from './HorizontalLayout';
|
||||||
|
import Image from './Image';
|
||||||
|
import Loop from './Loop';
|
||||||
|
import Pin from './Pin';
|
||||||
|
import Text from './Text';
|
||||||
|
import VerticalLayout from './VerticalLayout';
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
Box,
|
||||||
|
HorizontalLayout,
|
||||||
|
Image,
|
||||||
|
Loop,
|
||||||
|
Pin,
|
||||||
|
Text,
|
||||||
|
VerticalLayout
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderChildren = children => {
|
||||||
|
if (!children || children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children.length === 1 ?
|
||||||
|
renderImage(children[0]) :
|
||||||
|
children.map(renderImage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderImage = (node, key) => {
|
||||||
|
if (typeof node === 'string') {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, props, children } = node;
|
||||||
|
|
||||||
|
return React.createElement(nodeTypes[type], { key, ...props }, renderChildren(children));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default renderImage;
|
103
src/components/SVG/path.js
Normal file
103
src/components/SVG/path.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
class Path {
|
||||||
|
constructor({ relative } = {}) {
|
||||||
|
this.relative = relative || false;
|
||||||
|
this.currentPosition = { x: 0, y: 0 };
|
||||||
|
this.lastStartPosition = { x: 0, y: 0 };
|
||||||
|
this.pathParts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTo({ x, y, relative }) {
|
||||||
|
relative = relative === undefined ? this.relative : relative;
|
||||||
|
const command = relative ? 'm' : 'M';
|
||||||
|
|
||||||
|
if (relative) {
|
||||||
|
this.currentPosition = {
|
||||||
|
x: x + this.currentPosition.x,
|
||||||
|
y: y + this.currentPosition.y
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.currentPosition = { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pathParts.push(`${command}${x},${y}`);
|
||||||
|
this.lastStartPosition = { ...this.currentPosition };
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineTo({ x, y, relative }) {
|
||||||
|
relative = relative === undefined ? this.relative : relative;
|
||||||
|
if (x !== undefined && (y === undefined || y === this.currentPosition.y)) {
|
||||||
|
const command = relative ? 'h' : 'H';
|
||||||
|
this.pathParts.push(`${command}${x}`);
|
||||||
|
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
|
||||||
|
} else if (y !== undefined && (x === undefined || x === this.currentPosition.x)) {
|
||||||
|
const command = relative ? 'v' : 'V';
|
||||||
|
this.pathParts.push(`${command}${y}`);
|
||||||
|
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
|
||||||
|
} else {
|
||||||
|
const command = relative ? 'l' : 'L';
|
||||||
|
this.pathParts.push(`${command}${x},${y}`);
|
||||||
|
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
|
||||||
|
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
closePath() {
|
||||||
|
this.pathParts.push('Z');
|
||||||
|
this.currentPosition = { ...this.lastStartPosition };
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
cubicCurveTo({ cx1, cy1, cx2, cy2, x, y, relative }) {
|
||||||
|
relative = relative === undefined ? this.relative : relative;
|
||||||
|
if (cx1 === undefined || cy1 === undefined) {
|
||||||
|
const command = relative ? 's' : 'S';
|
||||||
|
this.pathParts.push(`${command}${cx2},${cy2} ${x},${y}`);
|
||||||
|
} else {
|
||||||
|
const command = relative ? 'c' : 'C';
|
||||||
|
this.pathParts.push(`${command}${cx1},${cy1} ${cx2},${cy2} ${x},${y}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
|
||||||
|
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
quadraticCurveTo({ cx, cy, x, y, relative }) {
|
||||||
|
relative = relative === undefined ? this.relative : relative;
|
||||||
|
if (cx === undefined || cy === undefined) {
|
||||||
|
const command = relative ? 't' : 'T';
|
||||||
|
this.pathParts.push(`${command} ${x},${y}`);
|
||||||
|
} else {
|
||||||
|
const command = relative ? 'q' : 'Q';
|
||||||
|
this.pathParts.push(`${command}${cx},${cy} ${x},${y}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
|
||||||
|
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
arcTo({ rx, ry, rotation, arc, sweep, x, y, relative }) {
|
||||||
|
relative = relative === undefined ? this.relative : relative;
|
||||||
|
const command = relative ? 'a' : 'A';
|
||||||
|
|
||||||
|
this.pathParts.push(`${command}${rx},${ry} ${rotation} ${arc} ${sweep},${x},${y}`);
|
||||||
|
|
||||||
|
this.currentPosition.x = relative ? this.currentPosition.x + x : x;
|
||||||
|
this.currentPosition.y = relative ? this.currentPosition.y + y : y;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.pathParts.join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Path;
|
43
src/components/SVG/style.js
Normal file
43
src/components/SVG/style.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Styles are in JS instead of CSS so they will be inlined as attributes
|
||||||
|
// instead of served as a CSS file. This is so styles are included in
|
||||||
|
// downloaded SVG files.
|
||||||
|
|
||||||
|
//const green = '#bada55';
|
||||||
|
const brown = '#6b6659';
|
||||||
|
//const tan = '#cbcbba';
|
||||||
|
const black = '#000';
|
||||||
|
const white = '#fff';
|
||||||
|
//const red = '#b3151a';
|
||||||
|
//const orange = '#fa0';
|
||||||
|
|
||||||
|
const fontFamily = 'Arial';
|
||||||
|
const fontSize = '16px';
|
||||||
|
const fontSizeSmall = '12px';
|
||||||
|
|
||||||
|
const strokeBase = {
|
||||||
|
strokeWidth: '2px',
|
||||||
|
stroke: black
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
image: {
|
||||||
|
backgroundColor: white
|
||||||
|
},
|
||||||
|
connectors: {
|
||||||
|
fillOpacity: 0,
|
||||||
|
...strokeBase
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontFamily: fontFamily
|
||||||
|
},
|
||||||
|
infoText: {
|
||||||
|
fontSize: fontSizeSmall,
|
||||||
|
fontFamily: fontFamily,
|
||||||
|
dominantBaseline: 'text-after-edge'
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
fill: brown,
|
||||||
|
...strokeBase
|
||||||
|
}
|
||||||
|
};
|
138
src/devel.js
Normal file
138
src/devel.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// Data used during development.
|
||||||
|
// Once everything is built, this file will go away
|
||||||
|
|
||||||
|
const syntaxes = {
|
||||||
|
js: 'JavaScript',
|
||||||
|
pcre: 'PCRE'
|
||||||
|
};
|
||||||
|
|
||||||
|
const demoImage = {
|
||||||
|
type: 'Image',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'HorizontalLayout',
|
||||||
|
props: {
|
||||||
|
withConnectors: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Pin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'VerticalLayout',
|
||||||
|
props: {
|
||||||
|
withConnectors: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Loop',
|
||||||
|
props: {
|
||||||
|
skip: false,
|
||||||
|
repeat: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Box',
|
||||||
|
props: {
|
||||||
|
style: { fill: '#bada55' },
|
||||||
|
radius: 3
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Text',
|
||||||
|
children: [
|
||||||
|
'Demo Text'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Loop',
|
||||||
|
props: {
|
||||||
|
skip: true,
|
||||||
|
repeat: false
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Box',
|
||||||
|
props: {
|
||||||
|
style: { fill: '#bada55' },
|
||||||
|
radius: 3
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Text',
|
||||||
|
children: [
|
||||||
|
'Demo Text'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Loop',
|
||||||
|
props: {
|
||||||
|
skip: true,
|
||||||
|
repeat: true,
|
||||||
|
greedy: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Box',
|
||||||
|
props: {
|
||||||
|
style: { fill: '#bada55' },
|
||||||
|
radius: 3
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Text',
|
||||||
|
children: [
|
||||||
|
'Demo Text'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Loop',
|
||||||
|
props: {
|
||||||
|
skip: true,
|
||||||
|
repeat: true,
|
||||||
|
label: 'Loop label'
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Box',
|
||||||
|
props: {
|
||||||
|
style: { fill: '#bada55' },
|
||||||
|
radius: 3
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'Text',
|
||||||
|
children: [
|
||||||
|
'Demo Text'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Pin'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
syntaxes,
|
||||||
|
demoImage
|
||||||
|
};
|
13
yarn.lock
13
yarn.lock
@ -650,7 +650,7 @@ babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0:
|
|||||||
version "6.18.0"
|
version "6.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
|
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
|
||||||
|
|
||||||
babel-plugin-syntax-object-rest-spread@^6.13.0:
|
babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
|
||||||
version "6.13.0"
|
version "6.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
|
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
|
||||||
|
|
||||||
@ -858,6 +858,13 @@ babel-plugin-transform-flow-strip-types@^6.22.0:
|
|||||||
babel-plugin-syntax-flow "^6.18.0"
|
babel-plugin-syntax-flow "^6.18.0"
|
||||||
babel-runtime "^6.22.0"
|
babel-runtime "^6.22.0"
|
||||||
|
|
||||||
|
babel-plugin-transform-object-rest-spread@^6.26.0:
|
||||||
|
version "6.26.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
|
||||||
|
dependencies:
|
||||||
|
babel-plugin-syntax-object-rest-spread "^6.8.0"
|
||||||
|
babel-runtime "^6.26.0"
|
||||||
|
|
||||||
babel-plugin-transform-react-display-name@^6.23.0:
|
babel-plugin-transform-react-display-name@^6.23.0:
|
||||||
version "6.25.0"
|
version "6.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1"
|
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1"
|
||||||
@ -3761,6 +3768,10 @@ image-size@^0.5.0:
|
|||||||
version "0.5.5"
|
version "0.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
|
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
|
||||||
|
|
||||||
|
immutable@^3.8.2:
|
||||||
|
version "3.8.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
|
||||||
|
|
||||||
import-local@^1.0.0:
|
import-local@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc"
|
resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc"
|
||||||
|
Loading…
Reference in New Issue
Block a user