Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Break on breaks for View components #2411

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/orange-moons-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@react-pdf/layout': minor
---

- Nested page break support for views.
- Fallback behavior for unwrappable content to wrap if it is a wrappable component (View / Text) when it doesn't fit within a page. Content will start on next page.
2 changes: 0 additions & 2 deletions packages/examples/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ const EXAMPLES = {
const Viewer = () => {
const [example, setExample] = useState('pageWrap');

console.log(example);

const handleExampleChange = e => {
setExample(e.target.dataset.name);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/layout/src/node/getWrap.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as P from '@react-pdf/primitives';
import { isNil } from '@react-pdf/fns';

const NON_WRAP_TYPES = [P.Svg, P.Note, P.Image, P.Canvas];
export const NON_WRAP_TYPES = [P.Svg, P.Note, P.Image, P.Canvas];

const getWrap = node => {
if (NON_WRAP_TYPES.includes(node.type)) return false;
Expand Down
4 changes: 3 additions & 1 deletion packages/layout/src/steps/resolveDimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,15 @@ const freeYogaNodes = node => {
export const resolvePageDimensions = (page, fontStore) => {
if (isNil(page)) return null;

return compose(
const result = compose(
destroyYogaNodes,
freeYogaNodes,
persistDimensions,
calculateLayout,
createYogaNodes(page, fontStore),
)(page);

return result;
};

/**
Expand Down
84 changes: 72 additions & 12 deletions packages/layout/src/steps/resolvePagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isNil, omit, compose } from '@react-pdf/fns';
import isFixed from '../node/isFixed';
import splitText from '../text/splitText';
import splitNode from '../node/splitNode';
import canNodeWrap from '../node/getWrap';
import canNodeWrap, { NON_WRAP_TYPES } from '../node/getWrap';
import getWrapArea from '../page/getWrapArea';
import getContentArea from '../page/getContentArea';
import createInstances from '../node/createInstances';
Expand Down Expand Up @@ -42,6 +42,34 @@ const warnUnavailableSpace = node => {
);
};

const warnFallbackSpace = node => {
console.warn(
`Node of type ${node.type} can't wrap between pages and it's bigger than available page height, falling back to wrap, and putting it on the next page`,
);
};
const breakableViewChild = (children, height, path = '') => {
for (let i = 0; i < children.length; i += 1) {
if (children[i].type !== 'VIEW') continue;

if (shouldNodeBreak(children[i], children.slice(i + 1, height))) {
return {
child: children[i],
path: `${path}/${i}`,
};
}

if (children[i].children && children[i].children.length > 0) {
const breakable = breakableViewChild(
children[i].children,
height,
`${path}/${i}`,
);
if (breakable) return breakable;
}
}
return null;
};

const splitNodes = (height, contentArea, nodes) => {
const currentChildren = [];
const nextChildren = [];
Expand All @@ -55,7 +83,14 @@ const splitNodes = (height, contentArea, nodes) => {
const nodeHeight = child.box.height;
const isOutside = height <= nodeTop;
const shouldBreak = shouldNodeBreak(child, futureNodes, height);

const firstBreakableViewChild =
child.children &&
child.children.length > 0 &&
breakableViewChild(child.children, height, '');

const shouldSplit = height + SAFTY_THRESHOLD < nodeTop + nodeHeight;

const canWrap = canNodeWrap(child);
const fitsInsidePage = nodeHeight <= contentArea;

Expand All @@ -73,10 +108,29 @@ const splitNodes = (height, contentArea, nodes) => {
}

if (!fitsInsidePage && !canWrap) {
currentChildren.push(child);
nextChildren.push(...futureNodes);
warnUnavailableSpace(child);
break;
if (NON_WRAP_TYPES.includes(child.type)) {
// We don't want to break non wrapable nodes, so we just let them be.
// They will be cropped, user will need to fix their ~image usage?
currentChildren.push(child);
nextChildren.push(...futureNodes);
warnUnavailableSpace(child);
break;
} else {
// We don't want to break non wrapable nodes, so we just let them be.
warnFallbackSpace(child);
const box = Object.assign({}, child.box, {
top: child.box.top - height,
});
const props = Object.assign({}, child.props, {
wrap: true,
break: false,
});
const next = Object.assign({}, child, { box, props });

currentChildren.push(...futureFixedNodes);
nextChildren.push(next, ...futureNodes);
break;
}
}

if (shouldBreak) {
Expand All @@ -92,15 +146,16 @@ const splitNodes = (height, contentArea, nodes) => {
break;
}

if (shouldSplit) {
if (shouldSplit || firstBreakableViewChild) {
const [currentChild, nextChild] = split(child, height, contentArea);

// All children are moved to the next page, it doesn't make sense to show the parent on the current page
// This was causing an infinite loop parent will now be discarded when it has no content
if (child.children.length > 0 && currentChild.children.length === 0) {
const box = Object.assign({}, child.box, {
top: child.box.top - height,
const box = Object.assign({}, nextChild.box, {
top: nextChild.box.top - height,
});
const next = Object.assign({}, child, { box });
const next = Object.assign({}, nextChild, { box });

currentChildren.push(...futureFixedNodes);
nextChildren.push(next, ...futureNodes);
Expand All @@ -121,16 +176,19 @@ const splitNodes = (height, contentArea, nodes) => {

const splitChildren = (height, contentArea, node) => {
const children = node.children || [];

const availableHeight = height - getTop(node);

return splitNodes(availableHeight, contentArea, children);
};

const splitView = (node, height, contentArea) => {
const splitView = (node, height, contentArea, foundBreakableViewChild) => {
const [currentNode, nextNode] = splitNode(node, height);
const [currentChilds, nextChildren] = splitChildren(
height,
contentArea,
node,
foundBreakableViewChild,
);

return [
Expand All @@ -139,8 +197,10 @@ const splitView = (node, height, contentArea) => {
];
};

const split = (node, height, contentArea) =>
isText(node) ? splitText(node, height) : splitView(node, height, contentArea);
const split = (node, height, contentArea, foundBreakableViewChild) =>
isText(node)
? splitText(node, height)
: splitView(node, height, contentArea, foundBreakableViewChild);

const shouldResolveDynamicNodes = node => {
const children = node.children || [];
Expand Down
177 changes: 177 additions & 0 deletions packages/renderer/tests/pageBreak.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/* eslint-disable react/no-array-index-key */
import renderToImage from './renderComponent';
import { Document, Font, Page, Text, View, StyleSheet } from '..';

const styles = StyleSheet.create({
body: {
paddingTop: 35,
paddingBottom: 65,
paddingHorizontal: 35,
},
title: {
fontSize: 24,
textAlign: 'center',
fontFamily: 'Oswald',
},
author: {
fontSize: 12,
textAlign: 'center',
marginBottom: 40,
},
subtitle: {
fontSize: 18,
margin: 12,
fontFamily: 'Oswald',
},
text: {
fontFamily: 'Open Sans',
margin: 12,
fontSize: 14,
textAlign: 'justify',
},
image: {
marginVertical: 15,
marginHorizontal: 100,
},
header: {
fontFamily: 'Open Sans',
fontSize: 12,
marginBottom: 20,
textAlign: 'center',
color: 'grey',
},
footer: {
fontFamily: 'Open Sans',
fontSize: 12,
marginBottom: 20,
textAlign: 'center',
color: 'grey',
},
pageNumber: {
fontFamily: 'Open Sans',
position: 'absolute',
fontSize: 12,
bottom: 30,
left: 0,
right: 0,
textAlign: 'center',
color: 'grey',
},
});

Font.register({
family: 'Oswald',
src: 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf',
});

Font.register({
family: 'Open Sans',
src: 'https://fonts.gstatic.com/s/opensans/v17/mem8YaGs126MiZpBA-UFVZ0e.ttf',
});

const PageWrap = () => (
<Document>
<Page style={styles.body} wrap>
<Text style={styles.header} fixed>
~ Created with react-pdf ~
</Text>
<Text style={styles.text}>New Page 1</Text>

<Text style={styles.text}>
<Text style={styles.text}>
{'New Page 1 Content'.repeat(800)}
End of Page 1 content
</Text>
</Text>
<Text style={styles.text} break>
New Page 2
</Text>
<Text style={styles.text}>New Page 2 Content</Text>
<>
<View>
<Text style={styles.text} break>
New Page 3
</Text>
<Text style={styles.text}>
{'New Page 3 Content'.repeat(1000)}
End of Page 3 content
</Text>
<>
<Text style={styles.text} break>
New Page 4
<Text style={styles.text}>
{'New Page 4 Content'.repeat(1000)}
End of Page 4 content
</Text>
<Text style={styles.text} break>
New Page 5
</Text>
<Text style={styles.text}>New Page 5 Content</Text>
</Text>
</>
</View>
</>
<View break>
<Text style={styles.text}>New Page 6</Text>
<Text style={styles.text}>New Page 6 Content</Text>
</View>
<View break>
<Text style={styles.text}>New Page 7</Text>
<Text style={styles.text}>New Page 7 Content</Text>
<Text break style={styles.text}>
New Page 8
</Text>
<Text style={styles.text}>New Page 8 Content</Text>
<View break>
<Text style={styles.text}>
{`New Page ${9}`}
<Text style={styles.text}>New Page 9 Content</Text>
</Text>
<View>
<View break>
<Text style={styles.text}>
New Page 10
<Text style={styles.text}>New Page 10 Content</Text>
</Text>
</View>
<Text style={styles.text} break>
New Page 11
<Text style={styles.text}>New Page 11 Content</Text>
</Text>
<Text style={styles.text} break>
New Page 12
<Text style={styles.text}>New Page 12 Content</Text>
<Text style={styles.text} break>
New Page 13
<Text style={styles.text}>New Page 13 Content</Text>
<Text style={styles.text} break>
New Page 14
<Text style={styles.text}>New Page 14 Content</Text>
</Text>
</Text>
</Text>
<View break>
<Text style={styles.text}>
New Page 15
<Text style={styles.text}>New Page 15 Content</Text>
</Text>
</View>
</View>
</View>
</View>
<Text
style={styles.pageNumber}
render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`}
fixed
/>
</Page>
</Document>
);

describe('pageBreak', () => {
test('should put every break instance on a new page', async () => {
const image = await renderToImage(<PageWrap />);

expect(image).toMatchImageSnapshot();
}, 30_000);
});
Loading