Adding a layout pass to SVG image components
text nodes are the only elements that need to be "measured". The dimensions of all other image components can be determined based on the dimensions of their children. This adds a pre-rendering pass to work out dimensions so multiple renders don't need to happen
This commit is contained in:
parent
21c392752e
commit
b299d32fc3
@ -22,8 +22,8 @@ exports[`App removing rendered expression 1`] = `
|
|||||||
<LoadNamespace(FormActions) />
|
<LoadNamespace(FormActions) />
|
||||||
</LoadNamespace(Form)>
|
</LoadNamespace(Form)>
|
||||||
<Render
|
<Render
|
||||||
|
data="LAYOUT(PARSED(test expression))"
|
||||||
onRender={[Function]}
|
onRender={[Function]}
|
||||||
parsed="PARSED(test expression)"
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
@ -139,8 +139,8 @@ exports[`App rendering an expression 3`] = `
|
|||||||
<LoadNamespace(FormActions) />
|
<LoadNamespace(FormActions) />
|
||||||
</LoadNamespace(Form)>
|
</LoadNamespace(Form)>
|
||||||
<Render
|
<Render
|
||||||
|
data="LAYOUT(PARSED(test expression))"
|
||||||
onRender={[Function]}
|
onRender={[Function]}
|
||||||
parsed="PARSED(test expression)"
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
@ -167,8 +167,8 @@ exports[`App rendering image details 1`] = `
|
|||||||
<LoadNamespace(FormActions) />
|
<LoadNamespace(FormActions) />
|
||||||
</LoadNamespace(Form)>
|
</LoadNamespace(Form)>
|
||||||
<Render
|
<Render
|
||||||
|
data="LAYOUT(PARSED(test expression))"
|
||||||
onRender={[Function]}
|
onRender={[Function]}
|
||||||
parsed="PARSED(test expression)"
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
@ -201,8 +201,8 @@ exports[`App rendering image details 2`] = `
|
|||||||
/>
|
/>
|
||||||
</LoadNamespace(Form)>
|
</LoadNamespace(Form)>
|
||||||
<Render
|
<Render
|
||||||
|
data="LAYOUT(PARSED(test expression))"
|
||||||
onRender={[Function]}
|
onRender={[Function]}
|
||||||
parsed="PARSED(test expression)"
|
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
@ -73,13 +73,13 @@ class App extends React.PureComponent {
|
|||||||
`syntax/${ syntax }`
|
`syntax/${ syntax }`
|
||||||
);
|
);
|
||||||
|
|
||||||
const parsed = syntaxModule.parse(expr);
|
const exprData = syntaxModule.layout(syntaxModule.parse(expr));
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
render: {
|
render: {
|
||||||
syntax,
|
syntax,
|
||||||
parsed,
|
exprData,
|
||||||
Component: syntaxModule.Render
|
Component: syntaxModule.Render
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -119,7 +119,7 @@ class App extends React.PureComponent {
|
|||||||
imageDetails,
|
imageDetails,
|
||||||
render: {
|
render: {
|
||||||
syntax: renderSyntax,
|
syntax: renderSyntax,
|
||||||
parsed,
|
exprData,
|
||||||
Component
|
Component
|
||||||
}
|
}
|
||||||
} = this.state;
|
} = this.state;
|
||||||
@ -137,7 +137,7 @@ class App extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
const renderProps = {
|
const renderProps = {
|
||||||
onRender: this.handleSvg,
|
onRender: this.handleSvg,
|
||||||
parsed
|
data: exprData
|
||||||
};
|
};
|
||||||
|
|
||||||
const doRender = renderSyntax === syntax;
|
const doRender = renderSyntax === syntax;
|
||||||
|
@ -6,6 +6,7 @@ import { App } from 'components/App';
|
|||||||
|
|
||||||
jest.mock('syntax/js', () => ({
|
jest.mock('syntax/js', () => ({
|
||||||
parse: expr => `PARSED(${ expr })`,
|
parse: expr => `PARSED(${ expr })`,
|
||||||
|
layout: parsed => `LAYOUT(${ parsed })`,
|
||||||
Render: () => ''
|
Render: () => ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -28,13 +28,21 @@ const render = (data, extraProps) => {
|
|||||||
|
|
||||||
class Render extends React.PureComponent {
|
class Render extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
parsed: PropTypes.object.isRequired,
|
data: PropTypes.object.isRequired,
|
||||||
onRender: PropTypes.func.isRequired
|
onRender: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
svgContainer = React.createRef()
|
svgContainer = React.createRef()
|
||||||
|
|
||||||
provideSVGData = () => {
|
componentDidMount() {
|
||||||
|
this.provideSVGData();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.provideSVGData();
|
||||||
|
}
|
||||||
|
|
||||||
|
provideSVGData() {
|
||||||
if (!this.svgContainer.current) {
|
if (!this.svgContainer.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -48,10 +56,10 @@ class Render extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { parsed } = this.props;
|
const { data } = this.props;
|
||||||
|
|
||||||
return <div className={ style.render } ref={ this.svgContainer }>
|
return <div className={ style.render } ref={ this.svgContainer }>
|
||||||
{ render(parsed, { onReflow: this.provideSVGData }) }
|
{ render(data, { onReflow: this.provideSVGData }) }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
src/layout.js
Normal file
22
src/layout.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import SVG from 'rendering/SVG/layout';
|
||||||
|
import Text from 'rendering/Text/layout';
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
SVG,
|
||||||
|
Text
|
||||||
|
};
|
||||||
|
|
||||||
|
const layout = data => {
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type } = data;
|
||||||
|
|
||||||
|
return nodeTypes[type]({
|
||||||
|
props: {},
|
||||||
|
...data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default layout;
|
@ -24,7 +24,9 @@ class SVG extends React.PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
onReflow: PropTypes.func,
|
onReflow: PropTypes.func,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
padding: PropTypes.number
|
padding: PropTypes.number,
|
||||||
|
innerWidth: PropTypes.number,
|
||||||
|
innerHeight: PropTypes.number
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -36,18 +38,11 @@ class SVG extends React.PureComponent {
|
|||||||
height: 0
|
height: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReflow = box => {
|
|
||||||
const { padding } = this.props;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
width: Math.round(box.width + 2 * padding),
|
|
||||||
height: Math.round(box.height + 2 * padding)
|
|
||||||
}, () => this.props.onReflow(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { width, height } = this.state;
|
const { padding, innerWidth, innerHeight, children } = this.props;
|
||||||
const { padding, children } = this.props;
|
|
||||||
|
const width = Math.round(innerWidth + 2 * padding);
|
||||||
|
const height = Math.round(innerHeight + 2 * padding);
|
||||||
|
|
||||||
const svgProps = {
|
const svgProps = {
|
||||||
width,
|
width,
|
||||||
@ -60,9 +55,7 @@ class SVG extends React.PureComponent {
|
|||||||
return <svg { ...svgProps }>
|
return <svg { ...svgProps }>
|
||||||
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
|
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
|
||||||
<g transform={ `translate(${ padding } ${ padding })` }>
|
<g transform={ `translate(${ padding } ${ padding })` }>
|
||||||
{ React.cloneElement(React.Children.only(children), {
|
{ children }
|
||||||
onReflow: this.handleReflow
|
|
||||||
}) }
|
|
||||||
</g>
|
</g>
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
12
src/rendering/SVG/layout.js
Normal file
12
src/rendering/SVG/layout.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import layout from 'layout';
|
||||||
|
|
||||||
|
const layoutSVG = data => {
|
||||||
|
const child = layout(data.children[0]);
|
||||||
|
|
||||||
|
data.props.innerWidth = child.box.width;
|
||||||
|
data.props.innerHeight = child.box.height;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default layoutSVG;
|
@ -6,6 +6,7 @@ import * as style from './style';
|
|||||||
class Text extends React.PureComponent {
|
class Text extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
quoted: PropTypes.bool,
|
quoted: PropTypes.bool,
|
||||||
|
transform: PropTypes.string,
|
||||||
onReflow: PropTypes.func,
|
onReflow: PropTypes.func,
|
||||||
children: PropTypes.oneOfType([
|
children: PropTypes.oneOfType([
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
@ -13,31 +14,6 @@ class Text extends React.PureComponent {
|
|||||||
]).isRequired
|
]).isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
|
||||||
transform: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
textRef = React.createRef()
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.reflow();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.reflow();
|
|
||||||
}
|
|
||||||
|
|
||||||
reflow() {
|
|
||||||
const box = this.textRef.current.getBBox();
|
|
||||||
const transform = `translate(${ -box.x } ${ -box.y })`;
|
|
||||||
|
|
||||||
if (transform === this.state.transform) {
|
|
||||||
return; // No update required
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ transform }, () => this.props.onReflow(box));
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
const { children, quoted } = this.props;
|
const { children, quoted } = this.props;
|
||||||
|
|
||||||
@ -53,12 +29,11 @@ class Text extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { transform } = this.state;
|
const { transform } = this.props;
|
||||||
|
|
||||||
const textProps = {
|
const textProps = {
|
||||||
style: style.text,
|
style: style.text,
|
||||||
transform,
|
transform
|
||||||
ref: this.textRef
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <text { ...textProps }>
|
return <text { ...textProps }>
|
||||||
|
27
src/rendering/Text/layout.js
Normal file
27
src/rendering/Text/layout.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import Text from 'rendering/Text';
|
||||||
|
|
||||||
|
const layoutText = data => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<svg width="0" height="0" viewBox="0 0 0 0">
|
||||||
|
<Text { ...data.props }>{ data.children }</Text>
|
||||||
|
</svg>,
|
||||||
|
container);
|
||||||
|
|
||||||
|
const box = container.querySelector('svg > text').getBBox();
|
||||||
|
document.body.removeChild(container);
|
||||||
|
|
||||||
|
data.box = {
|
||||||
|
width: box.width,
|
||||||
|
height: box.height
|
||||||
|
};
|
||||||
|
data.props.transform = `translate(${ -box.x } ${ -box.y })`;
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default layoutText;
|
@ -1,4 +1,5 @@
|
|||||||
import Render from 'components/Render';
|
import Render from 'components/Render';
|
||||||
|
import layout from 'layout';
|
||||||
|
|
||||||
const parse = expr => {
|
const parse = expr => {
|
||||||
return {
|
return {
|
||||||
@ -19,5 +20,6 @@ const parse = expr => {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
parse,
|
parse,
|
||||||
|
layout,
|
||||||
Render
|
Render
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Render from 'components/Render';
|
import Render from 'components/Render';
|
||||||
|
import layout from 'layout';
|
||||||
|
|
||||||
const parse = expr => {
|
const parse = expr => {
|
||||||
return {
|
return {
|
||||||
@ -19,5 +20,6 @@ const parse = expr => {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
parse,
|
parse,
|
||||||
|
layout,
|
||||||
Render
|
Render
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user