482 lines
16 KiB
JavaScript
Executable File
482 lines
16 KiB
JavaScript
Executable File
import { jsPDF } from 'jspdf';
|
|
import * as html2canvas from 'html2canvas';
|
|
import { objType, createElement, cloneNode, toPx } from './utils.js';
|
|
import es6promise from 'es6-promise';
|
|
var Promise = es6promise.Promise;
|
|
|
|
/* ----- CONSTRUCTOR ----- */
|
|
|
|
var Worker = function Worker(opt) {
|
|
// Create the root parent for the proto chain, and the starting Worker.
|
|
var root = Object.assign(Worker.convert(Promise.resolve()),
|
|
JSON.parse(JSON.stringify(Worker.template)));
|
|
var self = Worker.convert(Promise.resolve(), root);
|
|
|
|
// Set progress, optional settings, and return.
|
|
self = self.setProgress(1, Worker, 1, [Worker]);
|
|
self = self.set(opt);
|
|
return self;
|
|
};
|
|
|
|
// Boilerplate for subclassing Promise.
|
|
Worker.prototype = Object.create(Promise.prototype);
|
|
Worker.prototype.constructor = Worker;
|
|
|
|
// Converts/casts promises into Workers.
|
|
Worker.convert = function convert(promise, inherit) {
|
|
// Uses prototypal inheritance to receive changes made to ancestors' properties.
|
|
promise.__proto__ = inherit || Worker.prototype;
|
|
return promise;
|
|
};
|
|
|
|
Worker.template = {
|
|
prop: {
|
|
src: null,
|
|
container: null,
|
|
overlay: null,
|
|
canvas: null,
|
|
img: null,
|
|
pdf: null,
|
|
pageSize: null
|
|
},
|
|
progress: {
|
|
val: 0,
|
|
state: null,
|
|
n: 0,
|
|
stack: []
|
|
},
|
|
opt: {
|
|
filename: 'file.pdf',
|
|
margin: [0,0,0,0],
|
|
image: { type: 'jpeg', quality: 0.95 },
|
|
enableLinks: true,
|
|
html2canvas: {},
|
|
jsPDF: {}
|
|
}
|
|
};
|
|
|
|
/* ----- FROM / TO ----- */
|
|
|
|
Worker.prototype.from = function from(src, type) {
|
|
function getType(src) {
|
|
switch (objType(src)) {
|
|
case 'string': return 'string';
|
|
case 'element': return src.nodeName.toLowerCase === 'canvas' ? 'canvas' : 'element';
|
|
default: return 'unknown';
|
|
}
|
|
}
|
|
|
|
return this.then(function from_main() {
|
|
type = type || getType(src);
|
|
switch (type) {
|
|
case 'string': return this.set({ src: createElement('div', {innerHTML: src}) });
|
|
case 'element': return this.set({ src: src });
|
|
case 'canvas': return this.set({ canvas: src });
|
|
case 'img': return this.set({ img: src });
|
|
default: return this.error('Unknown source type.');
|
|
}
|
|
});
|
|
};
|
|
|
|
Worker.prototype.to = function to(target) {
|
|
// Route the 'to' request to the appropriate method.
|
|
switch (target) {
|
|
case 'container':
|
|
return this.toContainer();
|
|
case 'canvas':
|
|
return this.toCanvas();
|
|
case 'img':
|
|
return this.toImg();
|
|
case 'pdf':
|
|
return this.toPdf();
|
|
default:
|
|
return this.error('Invalid target.');
|
|
}
|
|
};
|
|
|
|
Worker.prototype.toContainer = function toContainer() {
|
|
// Set up function prerequisites.
|
|
var prereqs = [
|
|
function checkSrc() { return this.prop.src || this.error('Cannot duplicate - no source HTML.'); },
|
|
function checkPageSize() { return this.prop.pageSize || this.setPageSize(); }
|
|
];
|
|
|
|
return this.thenList(prereqs).then(function toContainer_main() {
|
|
// Define the CSS styles for the container and its overlay parent.
|
|
var overlayCSS = {
|
|
position: 'fixed', overflow: 'hidden', zIndex: 1000,
|
|
left: 0, right: 0, bottom: 0, top: 0,
|
|
backgroundColor: 'rgba(0,0,0,0.8)'
|
|
};
|
|
var containerCSS = {
|
|
position: 'absolute', width: this.prop.pageSize.inner.width + this.prop.pageSize.unit,
|
|
left: 0, right: 0, top: 0, height: 'auto', margin: 'auto',
|
|
backgroundColor: 'white'
|
|
};
|
|
|
|
// Set the overlay to hidden (could be changed in the future to provide a print preview).
|
|
overlayCSS.opacity = 0;
|
|
|
|
// Create and attach the elements.
|
|
var source = cloneNode(this.prop.src, this.opt.html2canvas.javascriptEnabled);
|
|
this.prop.overlay = createElement('div', { className: 'html2pdf__overlay', style: overlayCSS });
|
|
this.prop.container = createElement('div', { className: 'html2pdf__container', style: containerCSS });
|
|
this.prop.container.appendChild(source);
|
|
this.prop.overlay.appendChild(this.prop.container);
|
|
document.body.appendChild(this.prop.overlay);
|
|
});
|
|
};
|
|
|
|
Worker.prototype.toCanvas = function toCanvas() {
|
|
// Set up function prerequisites.
|
|
var prereqs = [
|
|
function checkContainer() { return document.body.contains(this.prop.container)
|
|
|| this.toContainer(); }
|
|
];
|
|
|
|
// Fulfill prereqs then create the canvas.
|
|
return this.thenList(prereqs).then(function toCanvas_main() {
|
|
// Handle old-fashioned 'onrendered' argument.
|
|
var options = Object.assign({}, this.opt.html2canvas);
|
|
delete options.onrendered;
|
|
|
|
return html2canvas(this.prop.container, options);
|
|
}).then(function toCanvas_post(canvas) {
|
|
// Handle old-fashioned 'onrendered' argument.
|
|
var onRendered = this.opt.html2canvas.onrendered || function () {};
|
|
onRendered(canvas);
|
|
|
|
this.prop.canvas = canvas;
|
|
document.body.removeChild(this.prop.overlay);
|
|
});
|
|
};
|
|
|
|
Worker.prototype.toImg = function toImg() {
|
|
// Set up function prerequisites.
|
|
var prereqs = [
|
|
function checkCanvas() { return this.prop.canvas || this.toCanvas(); }
|
|
];
|
|
|
|
// Fulfill prereqs then create the image.
|
|
return this.thenList(prereqs).then(function toImg_main() {
|
|
var imgData = this.prop.canvas.toDataURL('image/' + this.opt.image.type, this.opt.image.quality);
|
|
this.prop.img = document.createElement('img');
|
|
this.prop.img.src = imgData;
|
|
});
|
|
};
|
|
|
|
Worker.prototype.toPdf = function toPdf() {
|
|
// Set up function prerequisites.
|
|
var prereqs = [
|
|
function checkCanvas() { return this.prop.canvas || this.toCanvas(); }
|
|
];
|
|
|
|
// Fulfill prereqs then create the image.
|
|
return this.thenList(prereqs).then(function toPdf_main() {
|
|
// Create local copies of frequently used properties.
|
|
var canvas = this.prop.canvas;
|
|
var opt = this.opt;
|
|
|
|
// Calculate the number of pages.
|
|
var pxFullHeight = canvas.height;
|
|
var pxPageHeight = Math.floor(canvas.width * this.prop.pageSize.inner.ratio);
|
|
var nPages = Math.ceil(pxFullHeight / pxPageHeight);
|
|
|
|
// Define pageHeight separately so it can be trimmed on the final page.
|
|
var pageHeight = this.prop.pageSize.inner.height;
|
|
|
|
// Create a one-page canvas to split up the full image.
|
|
var pageCanvas = document.createElement('canvas');
|
|
var pageCtx = pageCanvas.getContext('2d');
|
|
pageCanvas.width = canvas.width;
|
|
pageCanvas.height = pxPageHeight;
|
|
|
|
// Initialize the PDF.
|
|
this.prop.pdf = this.prop.pdf || new jsPDF(opt.jsPDF);
|
|
|
|
for (var page=0; page<nPages; page++) {
|
|
// Trim the final page to reduce file size.
|
|
if (page === nPages-1 && pxFullHeight % pxPageHeight !== 0) {
|
|
pageCanvas.height = pxFullHeight % pxPageHeight;
|
|
pageHeight = pageCanvas.height * this.prop.pageSize.inner.width / pageCanvas.width;
|
|
}
|
|
|
|
// Display the page.
|
|
var w = pageCanvas.width;
|
|
var h = pageCanvas.height;
|
|
pageCtx.fillStyle = 'white';
|
|
pageCtx.fillRect(0, 0, w, h);
|
|
pageCtx.drawImage(canvas, 0, page*pxPageHeight, w, h, 0, 0, w, h);
|
|
|
|
// Add the page to the PDF.
|
|
if (page) this.prop.pdf.addPage();
|
|
var imgData = pageCanvas.toDataURL('image/' + opt.image.type, opt.image.quality);
|
|
this.prop.pdf.addImage(imgData, opt.image.type, opt.margin[1], opt.margin[0],
|
|
this.prop.pageSize.inner.width, pageHeight);
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
/* ----- OUTPUT / SAVE ----- */
|
|
|
|
Worker.prototype.output = function output(type, options, src) {
|
|
// Redirect requests to the correct function (outputPdf / outputImg).
|
|
src = src || 'pdf';
|
|
if (src.toLowerCase() === 'img' || src.toLowerCase() === 'image') {
|
|
return this.outputImg(type, options);
|
|
} else {
|
|
return this.outputPdf(type, options);
|
|
}
|
|
};
|
|
|
|
Worker.prototype.outputPdf = function outputPdf(type, options) {
|
|
// Set up function prerequisites.
|
|
var prereqs = [
|
|
function checkPdf() { return this.prop.pdf || this.toPdf(); }
|
|
];
|
|
|
|
// Fulfill prereqs then perform the appropriate output.
|
|
return this.thenList(prereqs).then(function outputPdf_main() {
|
|
/* Currently implemented output types:
|
|
* https://rawgit.com/MrRio/jsPDF/master/docs/jspdf.js.html#line992
|
|
* save(options), arraybuffer, blob, bloburi/bloburl,
|
|
* datauristring/dataurlstring, dataurlnewwindow, datauri/dataurl
|
|
*/
|
|
return this.prop.pdf.output(type, options);
|
|
});
|
|
};
|
|
|
|
Worker.prototype.outputImg = function outputImg(type, options) {
|
|
// Set up function prerequisites.
|
|
var prereqs = [
|
|
function checkImg() { return this.prop.img || this.toImg(); }
|
|
];
|
|
|
|
// Fulfill prereqs then perform the appropriate output.
|
|
return this.thenList(prereqs).then(function outputImg_main() {
|
|
switch (type) {
|
|
case undefined:
|
|
case 'img':
|
|
return this.prop.img;
|
|
case 'datauristring':
|
|
case 'dataurlstring':
|
|
return this.prop.img.src;
|
|
case 'datauri':
|
|
case 'dataurl':
|
|
return document.location.href = this.prop.img.src;
|
|
default:
|
|
throw 'Image output type "' + type + '" is not supported.';
|
|
}
|
|
});
|
|
};
|
|
|
|
Worker.prototype.save = function save(filename) {
|
|
// Set up function prerequisites.
|
|
var prereqs = [
|
|
function checkPdf() { return this.prop.pdf || this.toPdf(); }
|
|
];
|
|
|
|
// Fulfill prereqs, update the filename (if provided), and save the PDF.
|
|
return this.thenList(prereqs).set(
|
|
filename ? { filename: filename } : null
|
|
).then(function save_main() {
|
|
this.prop.pdf.save(this.opt.filename);
|
|
});
|
|
};
|
|
|
|
/* ----- SET / GET ----- */
|
|
|
|
Worker.prototype.set = function set(opt) {
|
|
// TODO: Implement ordered pairs?
|
|
|
|
// Silently ignore invalid or empty input.
|
|
if (objType(opt) !== 'object') {
|
|
return this;
|
|
}
|
|
|
|
// Build an array of setter functions to queue.
|
|
var fns = Object.keys(opt || {}).map(function (key) {
|
|
switch (key) {
|
|
case 'margin':
|
|
return this.setMargin.bind(this, opt.margin);
|
|
case 'jsPDF':
|
|
return function set_jsPDF() { this.opt.jsPDF = opt.jsPDF; return this.setPageSize(); }
|
|
case 'pageSize':
|
|
return this.setPageSize.bind(this, opt.pageSize);
|
|
default:
|
|
if (key in Worker.template.prop) {
|
|
// Set pre-defined properties in prop.
|
|
return function set_prop() { this.prop[key] = opt[key]; }
|
|
} else {
|
|
// Set any other properties in opt.
|
|
return function set_opt() { this.opt[key] = opt[key] };
|
|
}
|
|
}
|
|
}, this);
|
|
|
|
// Set properties within the promise chain.
|
|
return this.then(function set_main() {
|
|
return this.thenList(fns);
|
|
});
|
|
};
|
|
|
|
Worker.prototype.get = function get(key, cbk) {
|
|
return this.then(function get_main() {
|
|
// Fetch the requested property, either as a predefined prop or in opt.
|
|
var val = (key in Worker.template.prop) ? this.prop[key] : this.opt[key];
|
|
return cbk ? cbk(val) : val;
|
|
});
|
|
};
|
|
|
|
Worker.prototype.setMargin = function setMargin(margin) {
|
|
return this.then(function setMargin_main() {
|
|
// Parse the margin property: [top, left, bottom, right].
|
|
switch (objType(margin)) {
|
|
case 'number':
|
|
margin = [margin, margin, margin, margin];
|
|
case 'array':
|
|
if (margin.length === 2) {
|
|
margin = [margin[0], margin[1], margin[0], margin[1]];
|
|
}
|
|
if (margin.length === 4) {
|
|
break;
|
|
}
|
|
default:
|
|
return this.error('Invalid margin array.');
|
|
}
|
|
|
|
// Set the margin property, then update pageSize.
|
|
this.opt.margin = margin;
|
|
}).then(this.setPageSize);
|
|
}
|
|
|
|
Worker.prototype.setPageSize = function setPageSize(pageSize) {
|
|
return this.then(function setPageSize_main() {
|
|
// Retrieve page-size based on jsPDF settings, if not explicitly provided.
|
|
pageSize = pageSize || jsPDF.getPageSize(this.opt.jsPDF);
|
|
|
|
// Add 'inner' field if not present.
|
|
if (!pageSize.hasOwnProperty('inner')) {
|
|
pageSize.inner = {
|
|
width: pageSize.width - this.opt.margin[1] - this.opt.margin[3],
|
|
height: pageSize.height - this.opt.margin[0] - this.opt.margin[2]
|
|
};
|
|
pageSize.inner.px = {
|
|
width: toPx(pageSize.inner.width, pageSize.k),
|
|
height: toPx(pageSize.inner.height, pageSize.k)
|
|
};
|
|
pageSize.inner.ratio = pageSize.inner.height / pageSize.inner.width;
|
|
}
|
|
|
|
// Attach pageSize to this.
|
|
this.prop.pageSize = pageSize;
|
|
});
|
|
}
|
|
|
|
Worker.prototype.setProgress = function setProgress(val, state, n, stack) {
|
|
// Immediately update all progress values.
|
|
if (val != null) this.progress.val = val;
|
|
if (state != null) this.progress.state = state;
|
|
if (n != null) this.progress.n = n;
|
|
if (stack != null) this.progress.stack = stack;
|
|
this.progress.ratio = this.progress.val / this.progress.state;
|
|
|
|
// Return this for command chaining.
|
|
return this;
|
|
};
|
|
|
|
Worker.prototype.updateProgress = function updateProgress(val, state, n, stack) {
|
|
// Immediately update all progress values, using setProgress.
|
|
return this.setProgress(
|
|
val ? this.progress.val + val : null,
|
|
state ? state : null,
|
|
n ? this.progress.n + n : null,
|
|
stack ? this.progress.stack.concat(stack) : null
|
|
);
|
|
};
|
|
|
|
/* ----- PROMISE MAPPING ----- */
|
|
|
|
Worker.prototype.then = function then(onFulfilled, onRejected) {
|
|
// Wrap `this` for encapsulation.
|
|
var self = this;
|
|
|
|
return this.thenCore(onFulfilled, onRejected, function then_main(onFulfilled, onRejected) {
|
|
// Update progress while queuing, calling, and resolving `then`.
|
|
self.updateProgress(null, null, 1, [onFulfilled]);
|
|
return Promise.prototype.then.call(this, function then_pre(val) {
|
|
self.updateProgress(null, onFulfilled);
|
|
return val;
|
|
}).then(onFulfilled, onRejected).then(function then_post(val) {
|
|
self.updateProgress(1);
|
|
return val;
|
|
});
|
|
});
|
|
};
|
|
|
|
Worker.prototype.thenCore = function thenCore(onFulfilled, onRejected, thenBase) {
|
|
// Handle optional thenBase parameter.
|
|
thenBase = thenBase || Promise.prototype.then;
|
|
|
|
// Wrap `this` for encapsulation and bind it to the promise handlers.
|
|
var self = this;
|
|
if (onFulfilled) { onFulfilled = onFulfilled.bind(self); }
|
|
if (onRejected) { onRejected = onRejected.bind(self); }
|
|
|
|
// Cast self into a Promise to avoid polyfills recursively defining `then`.
|
|
var isNative = Promise.toString().indexOf('[native code]') !== -1 && Promise.name === 'Promise';
|
|
var selfPromise = isNative ? self : Worker.convert(Object.assign({}, self), Promise.prototype);
|
|
|
|
// Return the promise, after casting it into a Worker and preserving props.
|
|
var returnVal = thenBase.call(selfPromise, onFulfilled, onRejected);
|
|
return Worker.convert(returnVal, self.__proto__);
|
|
};
|
|
|
|
Worker.prototype.thenExternal = function thenExternal(onFulfilled, onRejected) {
|
|
// Call `then` and return a standard promise (exits the Worker chain).
|
|
return Promise.prototype.then.call(this, onFulfilled, onRejected);
|
|
};
|
|
|
|
Worker.prototype.thenList = function thenList(fns) {
|
|
// Queue a series of promise 'factories' into the promise chain.
|
|
var self = this;
|
|
fns.forEach(function thenList_forEach(fn) {
|
|
self = self.thenCore(fn);
|
|
});
|
|
return self;
|
|
};
|
|
|
|
Worker.prototype['catch'] = function (onRejected) {
|
|
// Bind `this` to the promise handler, call `catch`, and return a Worker.
|
|
if (onRejected) { onRejected = onRejected.bind(this); }
|
|
var returnVal = Promise.prototype['catch'].call(this, onRejected);
|
|
return Worker.convert(returnVal, this);
|
|
};
|
|
|
|
Worker.prototype.catchExternal = function catchExternal(onRejected) {
|
|
// Call `catch` and return a standard promise (exits the Worker chain).
|
|
return Promise.prototype['catch'].call(this, onRejected);
|
|
};
|
|
|
|
Worker.prototype.error = function error(msg) {
|
|
// Throw the error in the Promise chain.
|
|
return this.then(function error_main() {
|
|
throw new Error(msg);
|
|
});
|
|
};
|
|
|
|
|
|
/* ----- ALIASES ----- */
|
|
|
|
Worker.prototype.using = Worker.prototype.set;
|
|
Worker.prototype.saveAs = Worker.prototype.save;
|
|
Worker.prototype.export = Worker.prototype.output;
|
|
Worker.prototype.run = Worker.prototype.then;
|
|
|
|
|
|
/* ----- FINISHING ----- */
|
|
|
|
// Expose the Worker class.
|
|
export default Worker;
|