diff --git a/README.md b/README.md
index e826899..138226a 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ var opt = {
};
// New Promise-based usage:
-html2pdf().from(element).set(opt).save();
+html2pdf().set(opt).from(element).save();
// Old monolithic-style usage:
html2pdf(element, opt);
@@ -106,25 +106,56 @@ html2pdf(element, opt);
The `opt` parameter has the following optional fields:
-|Name |Type |Default |Description |
-|------------|----------------|------------------------------|------------------------------------------------------------------------------------------------------------|
-|margin |number or array |0 |PDF margin (in jsPDF units). Can be a single number, `[vMargin, hMargin]`, or `[top, left, bottom, right]`. |
-|filename |string |'file.pdf' |The default filename of the exported PDF. |
-|image |object |{type: 'jpeg', quality: 0.95} |The image type and quality used to generate the PDF. See the Extra Features section below. |
-|enableLinks |boolean |true |If enabled, PDF hyperlinks are automatically added ontop of all anchor tags. |
-|html2canvas |object |{ } |Configuration options sent directly to `html2canvas` ([see here](https://html2canvas.hertzen.com/configuration) for usage).|
-|jsPDF |object |{ } |Configuration options sent directly to `jsPDF` ([see here](http://rawgit.com/MrRio/jsPDF/master/docs/jsPDF.html) for usage).|
+|Name |Type |Default |Description |
+|------------|----------------|--------------------------------|------------------------------------------------------------------------------------------------------------|
+|margin |number or array |`0` |PDF margin (in jsPDF units). Can be a single number, `[vMargin, hMargin]`, or `[top, left, bottom, right]`. |
+|filename |string |`'file.pdf'` |The default filename of the exported PDF. |
+|pagebreak |object |`{mode: ['css', 'legacy']}` |Controls the pagebreak behaviour on the page. See [Page-breaks](#page-breaks) below. |
+|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.|
+|enableLinks |boolean |`true` |If enabled, PDF hyperlinks are automatically added ontop of all anchor tags. |
+|html2canvas |object |`{ }` |Configuration options sent directly to `html2canvas` ([see here](https://html2canvas.hertzen.com/configuration) for usage).|
+|jsPDF |object |`{ }` |Configuration options sent directly to `jsPDF` ([see here](http://rawgit.com/MrRio/jsPDF/master/docs/jsPDF.html) for usage).|
### Page-breaks
-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:
+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).
-```html
-
- I'm on page 1!
-
- I'm on page 2!
-
+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).
+
+#### Page-break settings
+
+|Setting |Type |Default |Description |
+|----------|----------------|--------------------|------------|
+|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'`. |
+|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. |
+|after |string or array |`[]` |Like 'before', but adds a page-break immediately after the element. |
+|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. |
+
+#### Page-break modes
+
+| Mode | Description |
+|-----------|-------------|
+| avoid-all | Automatically adds page-breaks to avoid splitting any elements across pages. |
+| 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. |
+| legacy | Adds page-breaks after elements with class `html2pdf__page-break`. This feature may be removed in the future. |
+
+#### Example usage
+
+```js
+// Avoid page-breaks on all elements, and add one before #page2el.
+html2pdf().set({
+ pagebreak: { mode: 'avoid-all', before: '#page2el' }
+});
+
+// Enable all 'modes', with no explicit elements.
+html2pdf().set({
+ pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
+});
+
+// No modes, only explicit elements.
+html2pdf().set({
+ pagebreak: { before: '.beforeClass', after: ['#after1', '#after2'], avoid: 'img' }
+});
```
### Image type and quality
diff --git a/src/plugin/pagebreaks.js b/src/plugin/pagebreaks.js
index b620688..5a4dc2b 100644
--- a/src/plugin/pagebreaks.js
+++ b/src/plugin/pagebreaks.js
@@ -1,18 +1,134 @@
import Worker from '../worker.js';
+import { objType, createElement } from '../utils.js';
+/* Pagebreak plugin:
+
+ Adds page-break functionality to the html2pdf library. Page-breaks can be
+ enabled by CSS styles, set on individual elements using selectors, or
+ avoided from breaking inside all elements.
+
+ Options on the `opt.pagebreak` object:
+
+ mode: String or array of strings: 'avoid-all', 'css', and/or 'legacy'
+ Default: ['css', 'legacy']
+
+ before: String or array of 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.
+
+ after: Like 'before', but adds a page-break immediately after the element.
+
+ avoid: Like 'before', but avoids page-breaks on these elements. You can
+ enable this feature on every element using the 'avoid-all' mode.
+*/
+
+// Refs to original functions.
var orig = {
toContainer: Worker.prototype.toContainer
};
+// Add pagebreak default options to the Worker template.
+Worker.template.opt.pagebreak = {
+ mode: ['css', 'legacy'],
+ before: [],
+ after: [],
+ avoid: []
+};
+
Worker.prototype.toContainer = function toContainer() {
return orig.toContainer.call(this).then(function toContainer_pagebreak() {
- // Enable page-breaks.
- var pageBreaks = this.prop.container.querySelectorAll('.html2pdf__page-break');
+ // Setup root element and inner page height.
+ var root = this.prop.container;
var pxPageHeight = this.prop.pageSize.inner.px.height;
- Array.prototype.forEach.call(pageBreaks, function pageBreak_loop(el) {
- el.style.display = 'block';
+
+ // Check all requested modes.
+ var modeSrc = [].concat(this.opt.pagebreak.mode);
+ var mode = {
+ avoidAll: modeSrc.indexOf('avoid-all') !== -1,
+ css: modeSrc.indexOf('css') !== -1,
+ legacy: modeSrc.indexOf('legacy') !== -1
+ };
+
+ // Get arrays of all explicitly requested elements.
+ var select = {};
+ var self = this;
+ ['before', 'after', 'avoid'].forEach(function(key) {
+ var all = mode.avoidAll && key === 'avoid';
+ select[key] = all ? [] : [].concat(self.opt.pagebreak[key] || []);
+ if (select[key].length > 0) {
+ select[key] = Array.prototype.slice.call(
+ root.querySelectorAll(select[key].join(', ')));
+ }
+ });
+
+ // Get all legacy page-break elements.
+ var legacyEls = root.querySelectorAll('.html2pdf__page-break');
+ legacyEls = Array.prototype.slice.call(legacyEls);
+
+ // Loop through all elements.
+ var els = root.querySelectorAll('*');
+ Array.prototype.forEach.call(els, function pagebreak_loop(el) {
+ // Setup pagebreak rules based on legacy and avoidAll modes.
+ var rules = {
+ before: false,
+ after: mode.legacy && legacyEls.indexOf(el) !== -1,
+ avoid: mode.avoidAll
+ };
+
+ // Add rules for css mode.
+ if (mode.css) {
+ // TODO: Check if this is valid with iFrames.
+ var style = window.getComputedStyle(el);
+ // TODO: Handle 'left' and 'right' correctly.
+ // TODO: Add support for 'avoid' on breakBefore/After.
+ var breakOpt = ['always', 'page', 'left', 'right'];
+ var avoidOpt = ['avoid', 'avoid-page'];
+ rules = {
+ before: rules.before || breakOpt.indexOf(style.breakBefore || style.pageBreakBefore) !== -1,
+ after: rules.after || breakOpt.indexOf(style.breakAfter || style.pageBreakAfter) !== -1,
+ avoid: rules.avoid || avoidOpt.indexOf(style.breakInside || style.pageBreakInside) !== -1
+ };
+ }
+
+ // Add rules for explicit requests.
+ Object.keys(rules).forEach(function(key) {
+ rules[key] = rules[key] || select[key].indexOf(el) !== -1;
+ });
+
+ // Get element position on the screen.
+ // TODO: Subtract the top of the container from clientRect.top/bottom?
var clientRect = el.getBoundingClientRect();
- el.style.height = pxPageHeight - (clientRect.top % pxPageHeight) + 'px';
- }, this);
+
+ // Avoid: Check if a break happens mid-element.
+ if (rules.avoid && !rules.before) {
+ var startPage = Math.floor(clientRect.top / pxPageHeight);
+ var endPage = Math.floor(clientRect.bottom / pxPageHeight);
+ var nPages = Math.abs(clientRect.bottom - clientRect.top) / pxPageHeight;
+
+ // Turn on rules.before if the el is broken and is at most one page long.
+ if (endPage !== startPage && nPages <= 1) {
+ rules.before = true;
+ }
+ }
+
+ // Before: Create a padding div to push the element to the next page.
+ if (rules.before) {
+ var pad = createElement('div', {style: {
+ display: 'block',
+ height: pxPageHeight - (clientRect.top % pxPageHeight) + 'px'
+ }});
+ el.parentNode.insertBefore(pad, el);
+ }
+
+ // After: Create a padding div to fill the remaining page.
+ if (rules.after) {
+ var pad = createElement('div', {style: {
+ display: 'block',
+ height: pxPageHeight - (clientRect.bottom % pxPageHeight) + 'px'
+ }});
+ el.parentNode.insertBefore(pad, el.nextSibling);
+ }
+ });
});
};
diff --git a/src/utils.js b/src/utils.js
index a0e9486..ce0c413 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,5 +1,5 @@
// Determine the type of a variable/object.
-export const objType = function(obj) {
+export const objType = function objType(obj) {
var type = typeof obj;
if (type === 'undefined') return 'undefined';
else if (type === 'string' || obj instanceof String) return 'string';
@@ -12,7 +12,7 @@ export const objType = function(obj) {
};
// Create an HTML element with optional className, innerHTML, and style.
-export const createElement = function(tagName, opt) {
+export const createElement = function createElement(tagName, opt) {
var el = document.createElement(tagName);
if (opt.className) el.className = opt.className;
if (opt.innerHTML) {
@@ -29,7 +29,7 @@ export const createElement = function(tagName, opt) {
};
// Deep-clone a node and preserve contents/properties.
-export const cloneNode = function(node, javascriptEnabled) {
+export const cloneNode = function cloneNode(node, javascriptEnabled) {
// Recursively clone the node.
var clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false);
for (var child = node.firstChild; child; child = child.nextSibling) {
@@ -59,11 +59,20 @@ export const cloneNode = function(node, javascriptEnabled) {
return clone;
}
-// Convert units using the conversion value 'k' from jsPDF.
-export const unitConvert = function(obj, k) {
- var newObj = {};
- for (var key in obj) {
- newObj[key] = obj[key] * 72 / 96 / k;
+// Convert units from px using the conversion value 'k' from jsPDF.
+export const unitConvert = function unitConvert(obj, k) {
+ if (objType(obj) === 'number') {
+ return obj * 72 / 96 / k;
+ } else {
+ var newObj = {};
+ for (var key in obj) {
+ newObj[key] = obj[key] * 72 / 96 / k;
+ }
+ return newObj;
}
- return newObj;
};
+
+// Convert units to px using the conversion value 'k' from jsPDF.
+export const toPx = function toPx(val, k) {
+ return Math.floor(val * k / 72 * 96);
+}
diff --git a/src/worker.js b/src/worker.js
index 657fd77..cbcb6fb 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -1,6 +1,6 @@
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';
-import { objType, createElement, cloneNode, unitConvert } from './utils.js';
+import { objType, createElement, cloneNode, toPx } from './utils.js';
/* ----- CONSTRUCTOR ----- */
@@ -330,7 +330,7 @@ Worker.prototype.get = function get(key, cbk) {
Worker.prototype.setMargin = function setMargin(margin) {
return this.then(function setMargin_main() {
- // Parse the margin property.
+ // Parse the margin property: [top, left, bottom, right].
switch (objType(margin)) {
case 'number':
margin = [margin, margin, margin, margin];
@@ -351,10 +351,6 @@ Worker.prototype.setMargin = function setMargin(margin) {
}
Worker.prototype.setPageSize = function setPageSize(pageSize) {
- function toPx(val, k) {
- return Math.floor(val * k / 72 * 96);
- }
-
return this.then(function setPageSize_main() {
// Retrieve page-size based on jsPDF settings, if not explicitly provided.
pageSize = pageSize || jsPDF.getPageSize(this.opt.jsPDF);
diff --git a/test/manual/pagebreaks.html b/test/manual/pagebreaks.html
new file mode 100644
index 0000000..6db20cb
--- /dev/null
+++ b/test/manual/pagebreaks.html
@@ -0,0 +1,123 @@
+
+
+
+ html2pdf Test - Pagebreaks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
First line
+
Break before
+
Break after
+
No effect (should be top of 3rd page, using css or specify).
+
Legacy (should create a break after).
+
No effect (should be top of 2nd page, using legacy).
+
Big element (should start on new page, using avoid-all/css/specify).
+
No effect (should start on next page *only* using avoid-all).
+
No effect (for spacing).
+
Full-page element (should start on new page using avoid-all/css/specify).
+
No effect (for spacing).
+
Even bigger element (should continue normally, because it's more than a page).
+
+
+
+
No effect inside parent div (testing avoid-all - no break yet because parent is more than a page).
+
Big element inside parent div (testing avoid-all - should have break before this).