@marcusumn/html2pdf.js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/worker.js ADDED
@@ -0,0 +1,484 @@
1
+ import jsPDF from 'jspdf/dist/jspdf.es.min.js';
2
+ import html2canvas from 'html2canvas';
3
+ import { deepCloneBasic } from './snapdom/clone.js';
4
+ import { objType, createElement, toPx } from './utils.js';
5
+
6
+ /* ----- CONSTRUCTOR ----- */
7
+
8
+ var Worker = function Worker(opt) {
9
+ // Create the root parent for the proto chain, and the starting Worker.
10
+ var root = Object.assign(Worker.convert(Promise.resolve()),
11
+ JSON.parse(JSON.stringify(Worker.template)));
12
+ var self = Worker.convert(Promise.resolve(), root);
13
+
14
+ // Set progress, optional settings, and return.
15
+ self = self.setProgress(1, Worker, 1, [Worker]);
16
+ self = self.set(opt);
17
+ return self;
18
+ };
19
+
20
+ // Boilerplate for subclassing Promise.
21
+ Worker.prototype = Object.create(Promise.prototype);
22
+ Worker.prototype.constructor = Worker;
23
+
24
+ // Converts/casts promises into Workers.
25
+ Worker.convert = function convert(promise, inherit) {
26
+ // Uses prototypal inheritance to receive changes made to ancestors' properties.
27
+ promise.__proto__ = inherit || Worker.prototype;
28
+ return promise;
29
+ };
30
+
31
+ Worker.template = {
32
+ prop: {
33
+ src: null,
34
+ container: null,
35
+ overlay: null,
36
+ canvas: null,
37
+ img: null,
38
+ pdf: null,
39
+ pageSize: null
40
+ },
41
+ progress: {
42
+ val: 0,
43
+ state: null,
44
+ n: 0,
45
+ stack: []
46
+ },
47
+ opt: {
48
+ filename: 'file.pdf',
49
+ margin: [0,0,0,0],
50
+ image: { type: 'jpeg', quality: 0.95 },
51
+ enableLinks: true,
52
+ html2canvas: {},
53
+ jsPDF: {}
54
+ }
55
+ };
56
+
57
+ /* ----- FROM / TO ----- */
58
+
59
+ Worker.prototype.from = function from(src, type) {
60
+ function getType(src) {
61
+ switch (objType(src)) {
62
+ case 'string': return 'string';
63
+ case 'element': return src.nodeName.toLowerCase && src.nodeName.toLowerCase() === 'canvas' ? 'canvas' : 'element';
64
+ default: return 'unknown';
65
+ }
66
+ }
67
+
68
+ return this.then(function from_main() {
69
+ type = type || getType(src);
70
+ switch (type) {
71
+ case 'string': return this.set({ src: createElement('div', {innerHTML: src}) });
72
+ case 'element': return this.set({ src: src });
73
+ case 'canvas': return this.set({ canvas: src });
74
+ case 'img': return this.set({ img: src });
75
+ default: return this.error('Unknown source type.');
76
+ }
77
+ });
78
+ };
79
+
80
+ Worker.prototype.to = function to(target) {
81
+ // Route the 'to' request to the appropriate method.
82
+ switch (target) {
83
+ case 'container':
84
+ return this.toContainer();
85
+ case 'canvas':
86
+ return this.toCanvas();
87
+ case 'img':
88
+ return this.toImg();
89
+ case 'pdf':
90
+ return this.toPdf();
91
+ default:
92
+ return this.error('Invalid target.');
93
+ }
94
+ };
95
+
96
+ Worker.prototype.toContainer = function toContainer() {
97
+ // Set up function prerequisites.
98
+ var prereqs = [
99
+ function checkSrc() { return this.prop.src || this.error('Cannot duplicate - no source HTML.'); },
100
+ function checkPageSize() { return this.prop.pageSize || this.setPageSize(); }
101
+ ];
102
+
103
+ return this.thenList(prereqs).then(function toContainer_main() {
104
+ // Define the CSS styles for the container and its overlay parent.
105
+ var overlayCSS = {
106
+ position: 'fixed', overflow: 'hidden', zIndex: 1000,
107
+ left: 0, right: 0, bottom: 0, top: 0,
108
+ backgroundColor: 'rgba(0,0,0,0.8)'
109
+ };
110
+ var containerCSS = {
111
+ position: 'absolute', width: this.prop.pageSize.inner.width + this.prop.pageSize.unit,
112
+ left: 0, right: 0, top: 0, height: 'auto', margin: 'auto',
113
+ backgroundColor: 'white'
114
+ };
115
+
116
+ // Set the overlay to hidden (could be changed in the future to provide a print preview).
117
+ overlayCSS.opacity = 0;
118
+
119
+ // Create and attach the elements.
120
+ var source = deepCloneBasic(this.prop.src);
121
+ this.prop.overlay = createElement('div', { className: 'html2pdf__overlay', style: overlayCSS });
122
+ this.prop.container = createElement('div', { className: 'html2pdf__container', style: containerCSS });
123
+ this.prop.container.appendChild(source);
124
+ this.prop.overlay.appendChild(this.prop.container);
125
+ document.body.appendChild(this.prop.overlay);
126
+
127
+ // Delay to better ensure content is fully cloned and rendering before capturing.
128
+ return new Promise(resolve => setTimeout(resolve, 10));
129
+ });
130
+ };
131
+
132
+ Worker.prototype.toCanvas = function toCanvas() {
133
+ // Set up function prerequisites.
134
+ var prereqs = [
135
+ function checkContainer() { return document.body.contains(this.prop.container)
136
+ || this.toContainer(); }
137
+ ];
138
+
139
+ // Fulfill prereqs then create the canvas.
140
+ return this.thenList(prereqs).then(function toCanvas_main() {
141
+ // Handle old-fashioned 'onrendered' argument.
142
+ var options = Object.assign({}, this.opt.html2canvas);
143
+ delete options.onrendered;
144
+
145
+ return html2canvas(this.prop.container, options);
146
+ }).then(function toCanvas_post(canvas) {
147
+ // Handle old-fashioned 'onrendered' argument.
148
+ var onRendered = this.opt.html2canvas.onrendered || function () {};
149
+ onRendered(canvas);
150
+
151
+ this.prop.canvas = canvas;
152
+ document.body.removeChild(this.prop.overlay);
153
+ });
154
+ };
155
+
156
+ Worker.prototype.toImg = function toImg() {
157
+ // Set up function prerequisites.
158
+ var prereqs = [
159
+ function checkCanvas() { return this.prop.canvas || this.toCanvas(); }
160
+ ];
161
+
162
+ // Fulfill prereqs then create the image.
163
+ return this.thenList(prereqs).then(function toImg_main() {
164
+ var imgData = this.prop.canvas.toDataURL('image/' + this.opt.image.type, this.opt.image.quality);
165
+ this.prop.img = document.createElement('img');
166
+ this.prop.img.src = imgData;
167
+ });
168
+ };
169
+
170
+ Worker.prototype.toPdf = function toPdf() {
171
+ // Set up function prerequisites.
172
+ var prereqs = [
173
+ function checkCanvas() { return this.prop.canvas || this.toCanvas(); },
174
+ function checkPageSize() { return this.prop.pageSize || this.setPageSize(); }
175
+ ];
176
+
177
+ // Fulfill prereqs then create the image.
178
+ return this.thenList(prereqs).then(function toPdf_main() {
179
+ // Create local copies of frequently used properties.
180
+ var canvas = this.prop.canvas;
181
+ var opt = this.opt;
182
+
183
+ // Calculate the number of pages.
184
+ var pxFullHeight = canvas.height;
185
+ var pxPageHeight = Math.floor(canvas.width * this.prop.pageSize.inner.ratio);
186
+ var nPages = Math.ceil(pxFullHeight / pxPageHeight);
187
+
188
+ // Define pageHeight separately so it can be trimmed on the final page.
189
+ var pageHeight = this.prop.pageSize.inner.height;
190
+
191
+ // Create a one-page canvas to split up the full image.
192
+ var pageCanvas = document.createElement('canvas');
193
+ var pageCtx = pageCanvas.getContext('2d');
194
+ pageCanvas.width = canvas.width;
195
+ pageCanvas.height = pxPageHeight;
196
+
197
+ // Initialize the PDF.
198
+ this.prop.pdf = this.prop.pdf || new jsPDF(opt.jsPDF);
199
+
200
+ for (var page=0; page<nPages; page++) {
201
+ // Trim the final page to reduce file size.
202
+ if (page === nPages-1 && pxFullHeight % pxPageHeight !== 0) {
203
+ pageCanvas.height = pxFullHeight % pxPageHeight;
204
+ pageHeight = pageCanvas.height * this.prop.pageSize.inner.width / pageCanvas.width;
205
+ }
206
+
207
+ // Display the page.
208
+ var w = pageCanvas.width;
209
+ var h = pageCanvas.height;
210
+ pageCtx.fillStyle = 'white';
211
+ pageCtx.fillRect(0, 0, w, h);
212
+ pageCtx.drawImage(canvas, 0, page*pxPageHeight, w, h, 0, 0, w, h);
213
+
214
+ // Add the page to the PDF.
215
+ if (page) this.prop.pdf.addPage();
216
+ var imgData = pageCanvas.toDataURL('image/' + opt.image.type, opt.image.quality);
217
+ this.prop.pdf.addImage(imgData, opt.image.type, opt.margin[1], opt.margin[0],
218
+ this.prop.pageSize.inner.width, pageHeight);
219
+ }
220
+ });
221
+ };
222
+
223
+
224
+ /* ----- OUTPUT / SAVE ----- */
225
+
226
+ Worker.prototype.output = function output(type, options, src) {
227
+ // Redirect requests to the correct function (outputPdf / outputImg).
228
+ src = src || 'pdf';
229
+ if (src.toLowerCase() === 'img' || src.toLowerCase() === 'image') {
230
+ return this.outputImg(type, options);
231
+ } else {
232
+ return this.outputPdf(type, options);
233
+ }
234
+ };
235
+
236
+ Worker.prototype.outputPdf = function outputPdf(type, options) {
237
+ // Set up function prerequisites.
238
+ var prereqs = [
239
+ function checkPdf() { return this.prop.pdf || this.toPdf(); }
240
+ ];
241
+
242
+ // Fulfill prereqs then perform the appropriate output.
243
+ return this.thenList(prereqs).then(function outputPdf_main() {
244
+ /* Currently implemented output types:
245
+ * https://rawgit.com/MrRio/jsPDF/master/docs/jspdf.js.html#line992
246
+ * save(options), arraybuffer, blob, bloburi/bloburl,
247
+ * datauristring/dataurlstring, dataurlnewwindow, datauri/dataurl
248
+ */
249
+ return this.prop.pdf.output(type, options);
250
+ });
251
+ };
252
+
253
+ Worker.prototype.outputImg = function outputImg(type, options) {
254
+ // Set up function prerequisites.
255
+ var prereqs = [
256
+ function checkImg() { return this.prop.img || this.toImg(); }
257
+ ];
258
+
259
+ // Fulfill prereqs then perform the appropriate output.
260
+ return this.thenList(prereqs).then(function outputImg_main() {
261
+ switch (type) {
262
+ case undefined:
263
+ case 'img':
264
+ return this.prop.img;
265
+ case 'datauristring':
266
+ case 'dataurlstring':
267
+ return this.prop.img.src;
268
+ case 'datauri':
269
+ case 'dataurl':
270
+ return document.location.href = this.prop.img.src;
271
+ default:
272
+ throw 'Image output type "' + type + '" is not supported.';
273
+ }
274
+ });
275
+ };
276
+
277
+ Worker.prototype.save = function save(filename) {
278
+ // Set up function prerequisites.
279
+ var prereqs = [
280
+ function checkPdf() { return this.prop.pdf || this.toPdf(); }
281
+ ];
282
+
283
+ // Fulfill prereqs, update the filename (if provided), and save the PDF.
284
+ return this.thenList(prereqs).set(
285
+ filename ? { filename: filename } : null
286
+ ).then(function save_main() {
287
+ this.prop.pdf.save(this.opt.filename);
288
+ });
289
+ };
290
+
291
+ /* ----- SET / GET ----- */
292
+
293
+ Worker.prototype.set = function set(opt) {
294
+ // TODO: Implement ordered pairs?
295
+
296
+ // Silently ignore invalid or empty input.
297
+ if (objType(opt) !== 'object') {
298
+ return this;
299
+ }
300
+
301
+ // Build an array of setter functions to queue.
302
+ var fns = Object.keys(opt || {}).map(function (key) {
303
+ switch (key) {
304
+ case 'margin':
305
+ return this.setMargin.bind(this, opt.margin);
306
+ case 'jsPDF':
307
+ return function set_jsPDF() { this.opt.jsPDF = opt.jsPDF; return this.setPageSize(); }
308
+ case 'pageSize':
309
+ return this.setPageSize.bind(this, opt.pageSize);
310
+ default:
311
+ if (key in Worker.template.prop) {
312
+ // Set pre-defined properties in prop.
313
+ return function set_prop() { this.prop[key] = opt[key]; }
314
+ } else {
315
+ // Set any other properties in opt.
316
+ return function set_opt() { this.opt[key] = opt[key] };
317
+ }
318
+ }
319
+ }, this);
320
+
321
+ // Set properties within the promise chain.
322
+ return this.then(function set_main() {
323
+ return this.thenList(fns);
324
+ });
325
+ };
326
+
327
+ Worker.prototype.get = function get(key, cbk) {
328
+ return this.then(function get_main() {
329
+ // Fetch the requested property, either as a predefined prop or in opt.
330
+ var val = (key in Worker.template.prop) ? this.prop[key] : this.opt[key];
331
+ return cbk ? cbk(val) : val;
332
+ });
333
+ };
334
+
335
+ Worker.prototype.setMargin = function setMargin(margin) {
336
+ return this.then(function setMargin_main() {
337
+ // Parse the margin property: [top, left, bottom, right].
338
+ switch (objType(margin)) {
339
+ case 'number':
340
+ margin = [margin, margin, margin, margin];
341
+ case 'array':
342
+ if (margin.length === 2) {
343
+ margin = [margin[0], margin[1], margin[0], margin[1]];
344
+ }
345
+ if (margin.length === 4) {
346
+ break;
347
+ }
348
+ default:
349
+ return this.error('Invalid margin array.');
350
+ }
351
+
352
+ // Set the margin property, then update pageSize.
353
+ this.opt.margin = margin;
354
+ }).then(this.setPageSize);
355
+ }
356
+
357
+ Worker.prototype.setPageSize = function setPageSize(pageSize) {
358
+ return this.then(function setPageSize_main() {
359
+ // Retrieve page-size based on jsPDF settings, if not explicitly provided.
360
+ pageSize = pageSize || jsPDF.getPageSize(this.opt.jsPDF);
361
+
362
+ // Add 'inner' field if not present.
363
+ if (!pageSize.hasOwnProperty('inner')) {
364
+ pageSize.inner = {
365
+ width: pageSize.width - this.opt.margin[1] - this.opt.margin[3],
366
+ height: pageSize.height - this.opt.margin[0] - this.opt.margin[2]
367
+ };
368
+ pageSize.inner.px = {
369
+ width: toPx(pageSize.inner.width, pageSize.k),
370
+ height: toPx(pageSize.inner.height, pageSize.k)
371
+ };
372
+ pageSize.inner.ratio = pageSize.inner.height / pageSize.inner.width;
373
+ }
374
+
375
+ // Attach pageSize to this.
376
+ this.prop.pageSize = pageSize;
377
+ });
378
+ }
379
+
380
+ Worker.prototype.setProgress = function setProgress(val, state, n, stack) {
381
+ // Immediately update all progress values.
382
+ if (val != null) this.progress.val = val;
383
+ if (state != null) this.progress.state = state;
384
+ if (n != null) this.progress.n = n;
385
+ if (stack != null) this.progress.stack = stack;
386
+ this.progress.ratio = this.progress.val / this.progress.state;
387
+
388
+ // Return this for command chaining.
389
+ return this;
390
+ };
391
+
392
+ Worker.prototype.updateProgress = function updateProgress(val, state, n, stack) {
393
+ // Immediately update all progress values, using setProgress.
394
+ return this.setProgress(
395
+ val ? this.progress.val + val : null,
396
+ state ? state : null,
397
+ n ? this.progress.n + n : null,
398
+ stack ? this.progress.stack.concat(stack) : null
399
+ );
400
+ };
401
+
402
+ /* ----- PROMISE MAPPING ----- */
403
+
404
+ Worker.prototype.then = function then(onFulfilled, onRejected) {
405
+ // Wrap `this` for encapsulation.
406
+ var self = this;
407
+
408
+ return this.thenCore(onFulfilled, onRejected, function then_main(onFulfilled, onRejected) {
409
+ // Update progress while queuing, calling, and resolving `then`.
410
+ self.updateProgress(null, null, 1, [onFulfilled]);
411
+ return Promise.prototype.then.call(this, function then_pre(val) {
412
+ self.updateProgress(null, onFulfilled);
413
+ return val;
414
+ }).then(onFulfilled, onRejected).then(function then_post(val) {
415
+ self.updateProgress(1);
416
+ return val;
417
+ });
418
+ });
419
+ };
420
+
421
+ Worker.prototype.thenCore = function thenCore(onFulfilled, onRejected, thenBase) {
422
+ // Handle optional thenBase parameter.
423
+ thenBase = thenBase || Promise.prototype.then;
424
+
425
+ // Wrap `this` for encapsulation and bind it to the promise handlers.
426
+ var self = this;
427
+ if (onFulfilled) { onFulfilled = onFulfilled.bind(self); }
428
+ if (onRejected) { onRejected = onRejected.bind(self); }
429
+
430
+ // Cast self into a Promise to avoid polyfills recursively defining `then`.
431
+ var isNative = Promise.toString().indexOf('[native code]') !== -1 && Promise.name === 'Promise';
432
+ var selfPromise = isNative ? self : Worker.convert(Object.assign({}, self), Promise.prototype);
433
+
434
+ // Return the promise, after casting it into a Worker and preserving props.
435
+ var returnVal = thenBase.call(selfPromise, onFulfilled, onRejected);
436
+ return Worker.convert(returnVal, self.__proto__);
437
+ };
438
+
439
+ Worker.prototype.thenExternal = function thenExternal(onFulfilled, onRejected) {
440
+ // Call `then` and return a standard promise (exits the Worker chain).
441
+ return Promise.prototype.then.call(this, onFulfilled, onRejected);
442
+ };
443
+
444
+ Worker.prototype.thenList = function thenList(fns) {
445
+ // Queue a series of promise 'factories' into the promise chain.
446
+ var self = this;
447
+ fns.forEach(function thenList_forEach(fn) {
448
+ self = self.thenCore(fn);
449
+ });
450
+ return self;
451
+ };
452
+
453
+ Worker.prototype['catch'] = function (onRejected) {
454
+ // Bind `this` to the promise handler, call `catch`, and return a Worker.
455
+ if (onRejected) { onRejected = onRejected.bind(this); }
456
+ var returnVal = Promise.prototype['catch'].call(this, onRejected);
457
+ return Worker.convert(returnVal, this);
458
+ };
459
+
460
+ Worker.prototype.catchExternal = function catchExternal(onRejected) {
461
+ // Call `catch` and return a standard promise (exits the Worker chain).
462
+ return Promise.prototype['catch'].call(this, onRejected);
463
+ };
464
+
465
+ Worker.prototype.error = function error(msg) {
466
+ // Throw the error in the Promise chain.
467
+ return this.then(function error_main() {
468
+ throw new Error(msg);
469
+ });
470
+ };
471
+
472
+
473
+ /* ----- ALIASES ----- */
474
+
475
+ Worker.prototype.using = Worker.prototype.set;
476
+ Worker.prototype.saveAs = Worker.prototype.save;
477
+ Worker.prototype.export = Worker.prototype.output;
478
+ Worker.prototype.run = Worker.prototype.then;
479
+
480
+
481
+ /* ----- FINISHING ----- */
482
+
483
+ // Expose the Worker class.
484
+ export default Worker;
package/type.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ declare module "html2pdf.js" {
2
+ interface Html2PdfOptions {
3
+ margin?: number | [number, number] | [number, number, number, number];
4
+ filename?: string;
5
+ image?: {
6
+ type?: "jpeg" | "png" | "webp";
7
+ quality?: number;
8
+ };
9
+ enableLinks?: boolean;
10
+ html2canvas?: object;
11
+ jsPDF?: {
12
+ unit?: string;
13
+ format?: string | [number, number];
14
+ orientation?: "portrait" | "landscape";
15
+ };
16
+ }
17
+
18
+ interface Html2PdfWorker {
19
+ from(src: HTMLElement | string | HTMLCanvasElement | HTMLImageElement): this;
20
+ to(target: "container" | "canvas" | "img" | "pdf"): this;
21
+ toContainer(): this;
22
+ toCanvas(): this;
23
+ toImg(): this;
24
+ toPdf(): this;
25
+ output(type?: string, options?: any, src?: "pdf" | "img"): Promise<any>;
26
+ outputPdf(type?: string, options?: any): Promise<any>;
27
+ outputImg(type?: string, options?: any): Promise<any>;
28
+ save(filename?: string): Promise<void>;
29
+ set(options: Html2PdfOptions): this;
30
+ get(key: string, cbk?: (value: any) => void): Promise<any>;
31
+ then<T>(onFulfilled?: (value: any) => T | PromiseLike<T>, onRejected?: (reason: any) => any): Promise<T>;
32
+ thenCore<T>(onFulfilled?: (value: any) => T | PromiseLike<T>, onRejected?: (reason: any) => any): Promise<T>;
33
+ thenExternal<T>(onFulfilled?: (value: any) => T | PromiseLike<T>, onRejected?: (reason: any) => any): Promise<T>;
34
+ catch<T>(onRejected?: (reason: any) => T | PromiseLike<T>): Promise<T>;
35
+ catchExternal<T>(onRejected?: (reason: any) => T | PromiseLike<T>): Promise<T>;
36
+ error(msg: string): void;
37
+ }
38
+
39
+ interface Html2PdfStatic {
40
+ (): Html2PdfWorker;
41
+ new (): Html2PdfWorker;
42
+ (element: HTMLElement, options?: Html2PdfOptions): Promise<void>;
43
+ new (element: HTMLElement, options?: Html2PdfOptions): Promise<void>;
44
+ Worker: new () => Html2PdfWorker;
45
+ }
46
+
47
+ const html2pdf: Html2PdfStatic;
48
+ export default html2pdf;
49
+ }