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

Initial nested page break handling. And change to unwrappable content fallback behavior.. #2403

Closed
wants to merge 2 commits into from
Closed
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
5 changes: 5 additions & 0 deletions .changeset/orange-moons-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@react-pdf/layout': minor
---

Nested page break support. Unwrappable content to wrap if capable when it doesn't fit within a 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
158 changes: 151 additions & 7 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,102 @@ 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`,
);
};

const breakableChild = (children, height, path = '') => {
for (let i = 0; i < children.length; i += 1) {
if (children[i].type === 'TEXT_INSTANCE') 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 = breakableChild(
children[i].children,
height,
`${path}/${i}`,
);
if (breakable) return breakable;
}
}
};

const splitByFirstChildBreak = (
firstBreakableChild,
currentNode,
currentPath,
height,
) => {
const preBreakChildren = [];
const postBreakChildren = [];

const nextIndex = Number(currentPath.split('/').pop());

const [preBreakNode, postBreakNode] = splitNode(currentNode, height);

for (let i = 0; i < currentNode.children.length; i += 1) {
// force recompute of lines on relayout.
// as the structure has changed.
const subjectNode = Object.assign({}, currentNode.children[i], {
lines: null,
});

if (i < nextIndex) {
preBreakChildren.push(subjectNode);
} else if (i === nextIndex) {
if (currentPath === firstBreakableChild.path) {
const theBrokenChild = subjectNode;

const props = Object.assign({}, theBrokenChild.props, {
wrap: true,
break: false,
});

const next = Object.assign({}, theBrokenChild, {
props,
});

postBreakChildren.push(next);
} else {
const [
nestedPreBreakChild,
nestedPostBreakChild,
] = splitByFirstChildBreak(
firstBreakableChild,
subjectNode,
currentPath +
firstBreakableChild.path
.replace(currentPath, '')
.split('/')
.slice(0, 2)
.join('/'),
height,
);

if (nestedPreBreakChild) preBreakChildren.push(nestedPreBreakChild);
if (nestedPostBreakChild) postBreakChildren.push(nestedPostBreakChild);
}
} else {
postBreakChildren.push(subjectNode);
}
}

return [
preBreakChildren.length === 0
? null
: assingChildren(preBreakChildren, preBreakNode),
assingChildren(postBreakChildren, postBreakNode),
];
};

const splitNodes = (height, contentArea, nodes) => {
const currentChildren = [];
const nextChildren = [];
Expand All @@ -50,10 +146,10 @@ const splitNodes = (height, contentArea, nodes) => {
const child = nodes[i];
const futureNodes = nodes.slice(i + 1);
const futureFixedNodes = futureNodes.filter(isFixed);

const nodeTop = getTop(child);
const nodeHeight = child.box.height;
const isOutside = height <= nodeTop;

const shouldBreak = shouldNodeBreak(child, futureNodes, height);
const shouldSplit = height + SAFTY_THRESHOLD < nodeTop + nodeHeight;
const canWrap = canNodeWrap(child);
Expand All @@ -66,21 +162,41 @@ const splitNodes = (height, contentArea, nodes) => {
}

if (isOutside) {
const box = Object.assign({}, child.box, { top: child.box.top - height });
const box = Object.assign({}, child.box, {
top: child.box.top - height,
});
const next = Object.assign({}, child, { box });
nextChildren.push(next);
continue;
}

if (!fitsInsidePage && !canWrap) {
currentChildren.push(child);
nextChildren.push(...futureNodes);
warnUnavailableSpace(child);
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);
} else {
// This should fallback to allow minPresence ahead to dictate where we should break and such.
const props = Object.assign({}, child.props, {
wrap: true,
break: false,
});
const next = Object.assign({}, child, { props });

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

break;
}

if (shouldBreak) {
const box = Object.assign({}, child.box, { top: child.box.top - height });
const box = Object.assign({}, child.box, {
top: child.box.top - height,
});
const props = Object.assign({}, child.props, {
wrap: true,
break: false,
Expand All @@ -92,6 +208,34 @@ const splitNodes = (height, contentArea, nodes) => {
break;
}

const firstBreakableChild =
child.children &&
child.children.length > 0 &&
breakableChild(child.children, height);

if (firstBreakableChild) {
const [currentPageNode, nextPageNode] = splitByFirstChildBreak(
firstBreakableChild,
child,
firstBreakableChild.path
.split('/')
.slice(0, 2)
.join('/'),
height,
);

const box = Object.assign({}, nextPageNode.box, {
top: nextPageNode.box.top - height,
});
const next = Object.assign({}, nextPageNode, { box });

if (currentPageNode) currentChildren.push(currentPageNode);

nextChildren.push(next, ...futureNodes);

break;
}

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

Expand Down
135 changes: 135 additions & 0 deletions packages/renderer/tests/pageBreak.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/* 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',
},
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}>Page 1</Text>
<Text style={styles.text}>Page 1 Content</Text>
<Text style={styles.text} break>
Page 2
</Text>
<Text style={styles.text}>Page 2 Content</Text>
<View>
<Text style={styles.text} break>
Page 3
</Text>
<Text style={styles.text}>Page 3 Content</Text>
<Text style={styles.text} break>
Page 4<Text style={styles.text}>Page 4 Content</Text>
<Text style={styles.text} break>
Page 5
</Text>
<Text style={styles.text}>Page 5 Content</Text>
</Text>
</View>
<View break>
<Text style={styles.text}>Page 6</Text>
<Text style={styles.text}>Page 6 Content</Text>
</View>
<View break>
<Text style={styles.text}>Page 7</Text>
<Text style={styles.text}>Page 7 Content</Text>
<Text break style={styles.text}>
Page 8
</Text>
<Text style={styles.text}>Page 8 Content</Text>
<View break>
<Text style={styles.text}>
Page 9 <Text style={styles.text}>Page 9 Content</Text>
</Text>
<View>
<Text style={styles.text} break>
Page 10
<Text style={styles.text}>Page 10 Content</Text>
</Text>
<Text style={styles.text} break>
Page 11
<Text style={styles.text}>Page 11 Content</Text>
<Text style={styles.text} break>
Page 12
<Text style={styles.text}>Page 12 Content</Text>
<Text style={styles.text} break>
Page 13
<Text style={styles.text}>Page 13 Content</Text>
</Text>
</Text>
</Text>
</View>
</View>
</View>
</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