Skip to content

Commit 0d88d34

Browse files
authored
Merge pull request #153 from eKoopmans/feature/pagebreaks
Page-breaks: Add modes and ability to specify elements
2 parents ade6f97 + 6028322 commit 0d88d34

File tree

6 files changed

+356
-37
lines changed

6 files changed

+356
-37
lines changed

README.md

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -98,33 +98,64 @@ var opt = {
9898
};
9999

100100
// New Promise-based usage:
101-
html2pdf().from(element).set(opt).save();
101+
html2pdf().set(opt).from(element).save();
102102

103103
// Old monolithic-style usage:
104104
html2pdf(element, opt);
105105
```
106106

107107
The `opt` parameter has the following optional fields:
108108

109-
|Name |Type |Default |Description |
110-
|------------|----------------|------------------------------|------------------------------------------------------------------------------------------------------------|
111-
|margin |number or array |0 |PDF margin (in jsPDF units). Can be a single number, `[vMargin, hMargin]`, or `[top, left, bottom, right]`. |
112-
|filename |string |'file.pdf' |The default filename of the exported PDF. |
113-
|image |object |{type: 'jpeg', quality: 0.95} |The image type and quality used to generate the PDF. See the Extra Features section below. |
114-
|enableLinks |boolean |true |If enabled, PDF hyperlinks are automatically added ontop of all anchor tags. |
115-
|html2canvas |object |{ } |Configuration options sent directly to `html2canvas` ([see here](https://html2canvas.hertzen.com/configuration) for usage).|
116-
|jsPDF |object |{ } |Configuration options sent directly to `jsPDF` ([see here](http://rawgit.com/MrRio/jsPDF/master/docs/jsPDF.html) for usage).|
109+
|Name |Type |Default |Description |
110+
|------------|----------------|--------------------------------|------------------------------------------------------------------------------------------------------------|
111+
|margin |number or array |`0` |PDF margin (in jsPDF units). Can be a single number, `[vMargin, hMargin]`, or `[top, left, bottom, right]`. |
112+
|filename |string |`'file.pdf'` |The default filename of the exported PDF. |
113+
|pagebreak |object |`{mode: ['css', 'legacy']}` |Controls the pagebreak behaviour on the page. See [Page-breaks](#page-breaks) below. |
114+
|image |object |`{type: 'jpeg', quality: 0.95}` |The image type and quality used to generate the PDF. See [Image type and quality](#image-type-and-quality) below.|
115+
|enableLinks |boolean |`true` |If enabled, PDF hyperlinks are automatically added ontop of all anchor tags. |
116+
|html2canvas |object |`{ }` |Configuration options sent directly to `html2canvas` ([see here](https://html2canvas.hertzen.com/configuration) for usage).|
117+
|jsPDF |object |`{ }` |Configuration options sent directly to `jsPDF` ([see here](http://rawgit.com/MrRio/jsPDF/master/docs/jsPDF.html) for usage).|
117118

118119
### Page-breaks
119120

120-
You may add `html2pdf`-specific page-breaks to your document by adding the CSS class `html2pdf__page-break` to any element (normally an empty `div`). For React elements, use `className=html2pdf__page-break`. During PDF creation, these elements will be given a height calculated to fill the remainder of the PDF page that they are on. Example usage:
121+
html2pdf has the ability to automatically add page-breaks to clean up your document. Page-breaks can be added by CSS styles, set on individual elements using selectors, or avoided from breaking inside all elements (`avoid-all` mode).
121122

122-
```html
123-
<div id="element-to-print">
124-
<span>I'm on page 1!</span>
125-
<div class="html2pdf__page-break"></div>
126-
<span>I'm on page 2!</span>
127-
</div>
123+
By default, html2pdf will respect most CSS [`break-before`](https://developer.mozilla.org/en-US/docs/Web/CSS/break-before), [`break-after`](https://developer.mozilla.org/en-US/docs/Web/CSS/break-after), and [`break-inside`](https://developer.mozilla.org/en-US/docs/Web/CSS/break-inside) rules, and also add page-breaks after any element with class `html2pdf__page-break` (for legacy purposes).
124+
125+
#### Page-break settings
126+
127+
|Setting |Type |Default |Description |
128+
|----------|----------------|--------------------|------------|
129+
|mode |string or array |`['css', 'legacy']` |The mode(s) on which to automatically add page-breaks. One or more of `'avoid-all'`, `'css'`, and `'legacy'`. |
130+
|before |string or array |`[]` |CSS selectors for which to add page-breaks before each element. Can be a specific element with an ID (`'#myID'`), all elements of a type (e.g. `'img'`), all of a class (`'.myClass'`), or even `'*'` to match every element. |
131+
|after |string or array |`[]` |Like 'before', but adds a page-break immediately after the element. |
132+
|avoid |string or array |`[]` |Like 'before', but avoids page-breaks on these elements. You can enable this feature on every element using the 'avoid-all' mode. |
133+
134+
#### Page-break modes
135+
136+
| Mode | Description |
137+
|-----------|-------------|
138+
| avoid-all | Automatically adds page-breaks to avoid splitting any elements across pages. |
139+
| css | Adds page-breaks according to the CSS `break-before`, `break-after`, and `break-inside` properties. Only recognizes `always/left/right` for before/after, and `avoid` for inside. |
140+
| legacy | Adds page-breaks after elements with class `html2pdf__page-break`. This feature may be removed in the future. |
141+
142+
#### Example usage
143+
144+
```js
145+
// Avoid page-breaks on all elements, and add one before #page2el.
146+
html2pdf().set({
147+
pagebreak: { mode: 'avoid-all', before: '#page2el' }
148+
});
149+
150+
// Enable all 'modes', with no explicit elements.
151+
html2pdf().set({
152+
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
153+
});
154+
155+
// No modes, only explicit elements.
156+
html2pdf().set({
157+
pagebreak: { before: '.beforeClass', after: ['#after1', '#after2'], avoid: 'img' }
158+
});
128159
```
129160

130161
### Image type and quality

src/plugin/pagebreaks.js

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,134 @@
11
import Worker from '../worker.js';
2+
import { objType, createElement } from '../utils.js';
23

4+
/* Pagebreak plugin:
5+
6+
Adds page-break functionality to the html2pdf library. Page-breaks can be
7+
enabled by CSS styles, set on individual elements using selectors, or
8+
avoided from breaking inside all elements.
9+
10+
Options on the `opt.pagebreak` object:
11+
12+
mode: String or array of strings: 'avoid-all', 'css', and/or 'legacy'
13+
Default: ['css', 'legacy']
14+
15+
before: String or array of CSS selectors for which to add page-breaks
16+
before each element. Can be a specific element with an ID
17+
('#myID'), all elements of a type (e.g. 'img'), all of a class
18+
('.myClass'), or even '*' to match every element.
19+
20+
after: Like 'before', but adds a page-break immediately after the element.
21+
22+
avoid: Like 'before', but avoids page-breaks on these elements. You can
23+
enable this feature on every element using the 'avoid-all' mode.
24+
*/
25+
26+
// Refs to original functions.
327
var orig = {
428
toContainer: Worker.prototype.toContainer
529
};
630

31+
// Add pagebreak default options to the Worker template.
32+
Worker.template.opt.pagebreak = {
33+
mode: ['css', 'legacy'],
34+
before: [],
35+
after: [],
36+
avoid: []
37+
};
38+
739
Worker.prototype.toContainer = function toContainer() {
840
return orig.toContainer.call(this).then(function toContainer_pagebreak() {
9-
// Enable page-breaks.
10-
var pageBreaks = this.prop.container.querySelectorAll('.html2pdf__page-break');
41+
// Setup root element and inner page height.
42+
var root = this.prop.container;
1143
var pxPageHeight = this.prop.pageSize.inner.px.height;
12-
Array.prototype.forEach.call(pageBreaks, function pageBreak_loop(el) {
13-
el.style.display = 'block';
44+
45+
// Check all requested modes.
46+
var modeSrc = [].concat(this.opt.pagebreak.mode);
47+
var mode = {
48+
avoidAll: modeSrc.indexOf('avoid-all') !== -1,
49+
css: modeSrc.indexOf('css') !== -1,
50+
legacy: modeSrc.indexOf('legacy') !== -1
51+
};
52+
53+
// Get arrays of all explicitly requested elements.
54+
var select = {};
55+
var self = this;
56+
['before', 'after', 'avoid'].forEach(function(key) {
57+
var all = mode.avoidAll && key === 'avoid';
58+
select[key] = all ? [] : [].concat(self.opt.pagebreak[key] || []);
59+
if (select[key].length > 0) {
60+
select[key] = Array.prototype.slice.call(
61+
root.querySelectorAll(select[key].join(', ')));
62+
}
63+
});
64+
65+
// Get all legacy page-break elements.
66+
var legacyEls = root.querySelectorAll('.html2pdf__page-break');
67+
legacyEls = Array.prototype.slice.call(legacyEls);
68+
69+
// Loop through all elements.
70+
var els = root.querySelectorAll('*');
71+
Array.prototype.forEach.call(els, function pagebreak_loop(el) {
72+
// Setup pagebreak rules based on legacy and avoidAll modes.
73+
var rules = {
74+
before: false,
75+
after: mode.legacy && legacyEls.indexOf(el) !== -1,
76+
avoid: mode.avoidAll
77+
};
78+
79+
// Add rules for css mode.
80+
if (mode.css) {
81+
// TODO: Check if this is valid with iFrames.
82+
var style = window.getComputedStyle(el);
83+
// TODO: Handle 'left' and 'right' correctly.
84+
// TODO: Add support for 'avoid' on breakBefore/After.
85+
var breakOpt = ['always', 'page', 'left', 'right'];
86+
var avoidOpt = ['avoid', 'avoid-page'];
87+
rules = {
88+
before: rules.before || breakOpt.indexOf(style.breakBefore || style.pageBreakBefore) !== -1,
89+
after: rules.after || breakOpt.indexOf(style.breakAfter || style.pageBreakAfter) !== -1,
90+
avoid: rules.avoid || avoidOpt.indexOf(style.breakInside || style.pageBreakInside) !== -1
91+
};
92+
}
93+
94+
// Add rules for explicit requests.
95+
Object.keys(rules).forEach(function(key) {
96+
rules[key] = rules[key] || select[key].indexOf(el) !== -1;
97+
});
98+
99+
// Get element position on the screen.
100+
// TODO: Subtract the top of the container from clientRect.top/bottom?
14101
var clientRect = el.getBoundingClientRect();
15-
el.style.height = pxPageHeight - (clientRect.top % pxPageHeight) + 'px';
16-
}, this);
102+
103+
// Avoid: Check if a break happens mid-element.
104+
if (rules.avoid && !rules.before) {
105+
var startPage = Math.floor(clientRect.top / pxPageHeight);
106+
var endPage = Math.floor(clientRect.bottom / pxPageHeight);
107+
var nPages = Math.abs(clientRect.bottom - clientRect.top) / pxPageHeight;
108+
109+
// Turn on rules.before if the el is broken and is at most one page long.
110+
if (endPage !== startPage && nPages <= 1) {
111+
rules.before = true;
112+
}
113+
}
114+
115+
// Before: Create a padding div to push the element to the next page.
116+
if (rules.before) {
117+
var pad = createElement('div', {style: {
118+
display: 'block',
119+
height: pxPageHeight - (clientRect.top % pxPageHeight) + 'px'
120+
}});
121+
el.parentNode.insertBefore(pad, el);
122+
}
123+
124+
// After: Create a padding div to fill the remaining page.
125+
if (rules.after) {
126+
var pad = createElement('div', {style: {
127+
display: 'block',
128+
height: pxPageHeight - (clientRect.bottom % pxPageHeight) + 'px'
129+
}});
130+
el.parentNode.insertBefore(pad, el.nextSibling);
131+
}
132+
});
17133
});
18134
};

src/utils.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Determine the type of a variable/object.
2-
export const objType = function(obj) {
2+
export const objType = function objType(obj) {
33
var type = typeof obj;
44
if (type === 'undefined') return 'undefined';
55
else if (type === 'string' || obj instanceof String) return 'string';
@@ -12,7 +12,7 @@ export const objType = function(obj) {
1212
};
1313

1414
// Create an HTML element with optional className, innerHTML, and style.
15-
export const createElement = function(tagName, opt) {
15+
export const createElement = function createElement(tagName, opt) {
1616
var el = document.createElement(tagName);
1717
if (opt.className) el.className = opt.className;
1818
if (opt.innerHTML) {
@@ -29,7 +29,7 @@ export const createElement = function(tagName, opt) {
2929
};
3030

3131
// Deep-clone a node and preserve contents/properties.
32-
export const cloneNode = function(node, javascriptEnabled) {
32+
export const cloneNode = function cloneNode(node, javascriptEnabled) {
3333
// Recursively clone the node.
3434
var clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false);
3535
for (var child = node.firstChild; child; child = child.nextSibling) {
@@ -59,11 +59,20 @@ export const cloneNode = function(node, javascriptEnabled) {
5959
return clone;
6060
}
6161

62-
// Convert units using the conversion value 'k' from jsPDF.
63-
export const unitConvert = function(obj, k) {
64-
var newObj = {};
65-
for (var key in obj) {
66-
newObj[key] = obj[key] * 72 / 96 / k;
62+
// Convert units from px using the conversion value 'k' from jsPDF.
63+
export const unitConvert = function unitConvert(obj, k) {
64+
if (objType(obj) === 'number') {
65+
return obj * 72 / 96 / k;
66+
} else {
67+
var newObj = {};
68+
for (var key in obj) {
69+
newObj[key] = obj[key] * 72 / 96 / k;
70+
}
71+
return newObj;
6772
}
68-
return newObj;
6973
};
74+
75+
// Convert units to px using the conversion value 'k' from jsPDF.
76+
export const toPx = function toPx(val, k) {
77+
return Math.floor(val * k / 72 * 96);
78+
}

src/worker.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import jsPDF from 'jspdf';
22
import html2canvas from 'html2canvas';
3-
import { objType, createElement, cloneNode, unitConvert } from './utils.js';
3+
import { objType, createElement, cloneNode, toPx } from './utils.js';
44

55
/* ----- CONSTRUCTOR ----- */
66

@@ -330,7 +330,7 @@ Worker.prototype.get = function get(key, cbk) {
330330

331331
Worker.prototype.setMargin = function setMargin(margin) {
332332
return this.then(function setMargin_main() {
333-
// Parse the margin property.
333+
// Parse the margin property: [top, left, bottom, right].
334334
switch (objType(margin)) {
335335
case 'number':
336336
margin = [margin, margin, margin, margin];
@@ -351,10 +351,6 @@ Worker.prototype.setMargin = function setMargin(margin) {
351351
}
352352

353353
Worker.prototype.setPageSize = function setPageSize(pageSize) {
354-
function toPx(val, k) {
355-
return Math.floor(val * k / 72 * 96);
356-
}
357-
358354
return this.then(function setPageSize_main() {
359355
// Retrieve page-size based on jsPDF settings, if not explicitly provided.
360356
pageSize = pageSize || jsPDF.getPageSize(this.opt.jsPDF);

0 commit comments

Comments
 (0)