-
-
Notifications
You must be signed in to change notification settings - Fork 222
/
Copy pathindex.tsx
563 lines (488 loc) · 26.4 KB
/
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
import * as React from "react";
import { findDOMNode } from "react-dom";
const contextEnabled = Object.prototype.hasOwnProperty.call(React, "createContext");
const hooksEnabled = Object.prototype.hasOwnProperty.call(React, "useMemo") && Object.prototype.hasOwnProperty.call(React, "useCallback");
export interface IPrintContextProps {
handlePrint: () => void,
}
const PrintContext = contextEnabled ? React.createContext({} as IPrintContextProps) : null;
export const PrintContextConsumer = PrintContext ? PrintContext.Consumer : () => null;
export interface ITriggerProps<T> {
onClick: () => void;
ref: (v: T) => void;
}
// https://developer.mozilla.org/en-US/docs/Web/API/FontFace/FontFace
type Font = {
family: string;
source: string;
};
type PropertyFunction<T> = () => T;
// NOTE: https://github.com/Microsoft/TypeScript/issues/23812
const defaultProps = {
copyStyles: true,
pageStyle: "@page { size: auto; margin: 0mm; } @media print { body { -webkit-print-color-adjust: exact; } }", // remove date/time from top
removeAfterPrint: false,
suppressErrors: false,
};
export interface IReactToPrintProps {
/** Class to pass to the print window body */
bodyClass?: string;
/** Content to be printed */
content: () => React.ReactInstance | null;
/** Copy styles over into print window. default: true */
copyStyles?: boolean;
/** Set the title for printing when saving as a file */
documentTitle?: string;
/** */
fonts?: Font[];
/** Callback function to trigger after print */
onAfterPrint?: () => void;
/** Callback function to trigger before page content is retrieved for printing */
onBeforeGetContent?: () => void | Promise<any>;
/** Callback function to trigger before print */
onBeforePrint?: () => void | Promise<any>;
/** Callback function to listen for printing errors */
onPrintError?: (errorLocation: "onBeforeGetContent" | "onBeforePrint" | "print", error: Error) => void;
/** Override default print window styling */
pageStyle?: string | PropertyFunction<string>;
/** Override the default `window.print` method that is used for printing */
print?: (target: HTMLIFrameElement) => Promise<any>;
/** Remove the iframe after printing. */
removeAfterPrint?: boolean;
/** Suppress error messages */
suppressErrors?: boolean;
/** Trigger action used to open browser print */
trigger?: <T>() => React.ReactElement<ITriggerProps<T>>;
/** Set the nonce attribute for whitelisting script and style -elements for CSP (content security policy) */
nonce?: string;
}
export default class ReactToPrint extends React.Component<IReactToPrintProps> {
private linkTotal!: number;
private linksLoaded!: Element[];
private linksErrored!: Element[];
private fontsLoaded!: FontFace[];
private fontsErrored!: FontFace[];
static defaultProps = defaultProps;
public startPrint = (target: HTMLIFrameElement) => {
const {
onAfterPrint,
onPrintError,
print,
documentTitle,
} = this.props;
setTimeout(() => {
if (target.contentWindow) {
target.contentWindow.focus(); // Needed for IE 11
if (print) {
print(target)
.then(this.handleRemoveIframe)
.catch((error: Error) => {
if (onPrintError) {
onPrintError('print', error);
} else {
this.logMessages(["An error was thrown by the specified `print` function"]);
}
});
} else if (target.contentWindow.print) {
const tempContentDocumentTitle = target.contentDocument?.title ?? '';
const tempOwnerDocumentTitle = target.ownerDocument.title;
// Override page and various target content titles during print
// NOTE: some browsers seem to take the print title from the highest level
// title, while others take it from the lowest level title. So, we set the title
// in a few places and hope the current browser takes one of them :pray:
if (documentTitle) {
// Print filename in Chrome
target.ownerDocument.title = documentTitle;
// Print filename in Firefox, Safari
if (target.contentDocument) {
target.contentDocument.title = documentTitle;
}
}
target.contentWindow.print();
// Restore the page's original title information
if (documentTitle) {
target.ownerDocument.title = tempOwnerDocumentTitle;
if (target.contentDocument) {
target.contentDocument.title = tempContentDocumentTitle;
}
}
if (onAfterPrint) {
onAfterPrint();
}
} else {
// Some browsers, such as Firefox Android, do not support printing at all
// https://developer.mozilla.org/en-US/docs/Web/API/Window/print
this.logMessages(["Printing for this browser is not currently possible: the browser does not have a `print` method available for iframes."]);
}
this.handleRemoveIframe();
} else {
this.logMessages(["Printing failed because the `contentWindow` of the print iframe did not load. This is possibly an error with `react-to-print`. Please file an issue: https://github.com/gregnb/react-to-print/issues/"]);
}
}, 500);
}
public triggerPrint = (target: HTMLIFrameElement) => {
const {
onBeforePrint,
onPrintError,
} = this.props;
if (onBeforePrint) {
const onBeforePrintOutput = onBeforePrint();
if (onBeforePrintOutput && typeof onBeforePrintOutput.then === "function") {
onBeforePrintOutput
.then(() => {
this.startPrint(target);
})
.catch((error: Error) => {
if (onPrintError) {
onPrintError("onBeforePrint", error);
}
});
} else {
this.startPrint(target);
}
} else {
this.startPrint(target);
}
}
public handleClick = () => {
const {
onBeforeGetContent,
onPrintError,
} = this.props;
if (onBeforeGetContent) {
const onBeforeGetContentOutput = onBeforeGetContent();
if (onBeforeGetContentOutput && typeof onBeforeGetContentOutput.then === "function") {
onBeforeGetContentOutput
.then(this.handlePrint)
.catch((error: Error) => {
if (onPrintError) {
onPrintError("onBeforeGetContent", error);
}
});
} else {
this.handlePrint();
}
} else {
this.handlePrint();
}
}
public handlePrint = () => {
const {
bodyClass,
content,
copyStyles,
fonts,
pageStyle,
nonce,
} = this.props;
const contentEl = content();
if (contentEl === undefined) {
this.logMessages(['To print a functional component ensure it is wrapped with `React.forwardRef`, and ensure the forwarded ref is used. See the README for an example: https://github.com/gregnb/react-to-print#examples']); // eslint-disable-line max-len
return;
}
if (contentEl === null) {
this.logMessages(['There is nothing to print because the "content" prop returned "null". Please ensure "content" is renderable before allowing "react-to-print" to be called.']); // eslint-disable-line max-len
return;
}
const printWindow = document.createElement("iframe");
printWindow.style.position = "absolute";
printWindow.style.top = "-1000px";
printWindow.style.left = "-1000px";
printWindow.id = "printWindow";
// Ensure we set a DOCTYPE on the iframe's document
// https://github.com/gregnb/react-to-print/issues/459
printWindow.srcdoc = "<!DOCTYPE html>";
const contentNodes = findDOMNode(contentEl);
if (!contentNodes) {
this.logMessages(['"react-to-print" could not locate the DOM node corresponding with the `content` prop']); // eslint-disable-line max-len
return;
}
// React components can return a bare string as a valid JSX response
const clonedContentNodes = contentNodes.cloneNode(true);
const isText = clonedContentNodes instanceof Text;
const globalStyleLinkNodes = document.querySelectorAll("link[rel='stylesheet']");
const renderComponentImgNodes = isText ? [] : (clonedContentNodes as Element).querySelectorAll("img");
const renderComponentVideoNodes = isText ? [] : (clonedContentNodes as Element).querySelectorAll("video");
this.linkTotal =
globalStyleLinkNodes.length +
renderComponentImgNodes.length +
renderComponentVideoNodes.length;
this.linksLoaded = [];
this.linksErrored = [];
this.fontsLoaded = [];
this.fontsErrored = [];
const markLoaded = (linkNode: Element, loaded: boolean) => {
if (loaded) {
this.linksLoaded.push(linkNode);
} else {
this.logMessages(['"react-to-print" was unable to load a linked node. It may be invalid. "react-to-print" will continue attempting to print the page. The linked node that errored was:', linkNode]); // eslint-disable-line max-len
this.linksErrored.push(linkNode);
}
// We may have errors, but attempt to print anyways - maybe they are trivial and the
// user will be ok ignoring them
const numResourcesManaged =
this.linksLoaded.length +
this.linksErrored.length +
this.fontsLoaded.length +
this.fontsErrored.length;
if (numResourcesManaged === this.linkTotal) {
this.triggerPrint(printWindow);
}
};
printWindow.onload = () => {
// Some agents, such as IE11 and Enzyme (as of 2 Jun 2020) continuously call the
// `onload` callback. This ensures that it is only called once.
printWindow.onload = null;
const domDoc = printWindow.contentDocument || printWindow.contentWindow?.document;
if (domDoc) {
domDoc.body.appendChild(clonedContentNodes);
if (fonts) {
if (printWindow.contentDocument?.fonts && printWindow.contentWindow?.FontFace) {
fonts.forEach((font) => {
const fontFace = new FontFace(font.family, font.source);
printWindow.contentDocument!.fonts.add(fontFace);
fontFace.loaded
.then((loadedFontFace) => {
this.fontsLoaded.push(loadedFontFace);
})
.catch((error: SyntaxError) => {
this.fontsErrored.push(fontFace);
this.logMessages(['"react-to-print" was unable to load a font. "react-to-print" will continue attempting to print the page. The font that failed to load is:', fontFace, 'The error from loading the font is:', error]); // eslint-disable-line max-len
});
});
} else {
this.logMessages(['"react-to-print" is not able to load custom fonts because the browser does not support the FontFace API']); // eslint-disable-line max-len
}
}
const defaultPageStyle = typeof pageStyle === "function" ? pageStyle() : pageStyle;
if (typeof defaultPageStyle !== 'string') {
this.logMessages([`"react-to-print" expected a "string" from \`pageStyle\` but received "${typeof defaultPageStyle}". Styles from \`pageStyle\` will not be applied.`]); // eslint-disable-line max-len
} else {
const styleEl = domDoc.createElement("style");
if (nonce) {
styleEl.setAttribute("nonce", nonce);
domDoc.head.setAttribute("nonce", nonce);
}
styleEl.appendChild(domDoc.createTextNode(defaultPageStyle));
domDoc.head.appendChild(styleEl);
}
if (bodyClass) {
domDoc.body.classList.add(...bodyClass.split(" "));
}
if (!isText) {
// Copy canvases
// NOTE: must use data from `contentNodes` here as the canvass elements in
// `clonedContentNodes` will not have been redrawn properly yet
const srcCanvasEls = isText ? [] : (contentNodes as Element).querySelectorAll("canvas");
const targetCanvasEls = domDoc.querySelectorAll("canvas");
for (let i = 0; i < srcCanvasEls.length; ++i) {
const sourceCanvas = srcCanvasEls[i];
const targetCanvas = targetCanvasEls[i];
const targetCanvasContext = targetCanvas.getContext("2d");
if (targetCanvasContext) {
targetCanvasContext.drawImage(sourceCanvas, 0, 0);
}
}
// Pre-load images
for (let i = 0; i < renderComponentImgNodes.length; i++) {
const imgNode = renderComponentImgNodes[i];
const imgSrc = imgNode.getAttribute("src");
if (!imgSrc) {
this.logMessages(['"react-to-print" encountered an <img> tag with an empty "src" attribute. It will not attempt to pre-load it. The <img> is:', imgNode], 'warning'); // eslint-disable-line
markLoaded(imgNode, false);
} else {
// https://stackoverflow.com/questions/10240110/how-do-you-cache-an-image-in-javascript
const img = new Image();
img.onload = markLoaded.bind(null, imgNode, true);
img.onerror = markLoaded.bind(null, imgNode, false);
img.src = imgSrc;
}
}
// Pre-load videos
for (let i = 0; i < renderComponentVideoNodes.length; i++) {
const videoNode = renderComponentVideoNodes[i];
videoNode.preload = 'auto'; // Hint to the browser that it should load this resource
const videoPoster = videoNode.getAttribute('poster')
if (videoPoster) {
// If the video has a poster, pre-load the poster image
// https://stackoverflow.com/questions/10240110/how-do-you-cache-an-image-in-javascript
const img = new Image();
img.onload = markLoaded.bind(null, videoNode, true);
img.onerror = markLoaded.bind(null, videoNode, false);
img.src = videoPoster;
} else {
if (videoNode.readyState >= 2) { // Check if the video has already loaded a frame
markLoaded(videoNode, true);
} else {
videoNode.onloadeddata = markLoaded.bind(null, videoNode, true);
// TODO: if one if these is called is it possible for another to be called? If so we
// need to add guards to ensure `markLoaded` is only called once for the node
// TODO: why do `onabort` and `onstalled` seem to fire all the time even if
// there is no issue?
// videoNode.onabort = () => { console.log('Video with no poster abort'); markLoaded.bind(null, videoNode, false)(); }
videoNode.onerror = markLoaded.bind(null, videoNode, false);
// videoNode.onemptied = () => { console.log('Video with no poster emptied'); markLoaded.bind(null, videoNode, false)(); }
videoNode.onstalled = markLoaded.bind(null, videoNode, false);
}
}
}
// Copy input values
// This covers most input types, though some need additional work (further down)
const inputSelector = 'input';
const originalInputs = (contentNodes as HTMLElement).querySelectorAll(inputSelector); // eslint-disable-line max-len
const copiedInputs = domDoc.querySelectorAll(inputSelector);
for (let i = 0; i < originalInputs.length; i++) {
copiedInputs[i].value = originalInputs[i].value;
}
// Copy checkbox, radio checks
const checkedSelector = 'input[type=checkbox],input[type=radio]';
const originalCRs = (contentNodes as HTMLElement).querySelectorAll(checkedSelector); // eslint-disable-line max-len
const copiedCRs = domDoc.querySelectorAll(checkedSelector);
for (let i = 0; i < originalCRs.length; i++) {
(copiedCRs[i] as HTMLInputElement).checked =
(originalCRs[i] as HTMLInputElement).checked;
}
// Copy select states
const selectSelector = 'select';
const originalSelects = (contentNodes as HTMLElement).querySelectorAll(selectSelector); // eslint-disable-line max-len
const copiedSelects = domDoc.querySelectorAll(selectSelector);
for (let i = 0; i < originalSelects.length; i++) {
copiedSelects[i].value = originalSelects[i].value;
}
}
if (copyStyles) {
const headEls = document.querySelectorAll("style, link[rel='stylesheet']");
for (let i = 0, headElsLen = headEls.length; i < headElsLen; ++i) {
const node = headEls[i];
if (node.tagName === "STYLE") { // <style> nodes
const newHeadEl = domDoc.createElement(node.tagName);
const sheet = (node as HTMLStyleElement).sheet as CSSStyleSheet;
if (sheet) {
let styleCSS = "";
// NOTE: for-of is not supported by IE
try {
// Accessing `sheet.cssRules` on cross-origin sheets can throw
// security exceptions in some browsers, notably Firefox
// https://github.com/gregnb/react-to-print/issues/429
const cssLength = sheet.cssRules.length;
for (let j = 0; j < cssLength; ++j) {
if (typeof sheet.cssRules[j].cssText === "string") {
styleCSS += `${sheet.cssRules[j].cssText}\r\n`;
}
}
} catch (error) {
this.logMessages([`A stylesheet could not be accessed. This is likely due to the stylesheet having cross-origin imports, and many browsers block script access to cross-origin stylesheets. See https://github.com/gregnb/react-to-print/issues/429 for details. You may be able to load the sheet by both marking the stylesheet with the cross \`crossorigin\` attribute, and setting the \`Access-Control-Allow-Origin\` header on the server serving the stylesheet. Alternatively, host the stylesheet on your domain to avoid this issue entirely.`, node], 'warning');
}
newHeadEl.setAttribute("id", `react-to-print-${i}`);
if (nonce) {
newHeadEl.setAttribute("nonce", nonce);
}
newHeadEl.appendChild(domDoc.createTextNode(styleCSS));
domDoc.head.appendChild(newHeadEl);
}
} else { // <link> nodes, and any others
// Many browsers will do all sorts of weird things if they encounter an
// empty `href` tag (which is invalid HTML). Some will attempt to load
// the current page. Some will attempt to load the page"s parent
// directory. These problems can cause `react-to-print` to stop without
// any error being thrown. To avoid such problems we simply do not
// attempt to load these links.
if (node.getAttribute("href")) {
const newHeadEl = domDoc.createElement(node.tagName);
// Manually re-create the node
// TODO: document why cloning the node won't work? I don't recall
// the reasoning behind why we do it this way
// NOTE: node.attributes has NamedNodeMap type that is not an Array
// and can be iterated only via direct [i] access
for (let j = 0, attrLen = node.attributes.length; j < attrLen; ++j) { // eslint-disable-line max-len
const attr = node.attributes[j];
if (attr) {
newHeadEl.setAttribute(attr.nodeName, attr.nodeValue || "");
}
}
newHeadEl.onload = markLoaded.bind(null, newHeadEl, true);
newHeadEl.onerror = markLoaded.bind(null, newHeadEl, false);
if (nonce) {
newHeadEl.setAttribute("nonce", nonce);
}
domDoc.head.appendChild(newHeadEl);
} else {
this.logMessages(['"react-to-print" encountered a <link> tag with an empty "href" attribute. In addition to being invalid HTML, this can cause problems in many browsers, and so the <link> was not loaded. The <link> is:', node], 'warning')
// `true` because we"ve already shown a warning for this
markLoaded(node, true);
}
}
}
}
}
if (this.linkTotal === 0 || !copyStyles) {
this.triggerPrint(printWindow);
}
};
this.handleRemoveIframe(true);
document.body.appendChild(printWindow);
}
public handleRemoveIframe = (force?: boolean) => {
const {
removeAfterPrint,
} = this.props;
if (force || removeAfterPrint) {
// The user may have removed the iframe in `onAfterPrint`
const documentPrintWindow = document.getElementById("printWindow");
if (documentPrintWindow) {
document.body.removeChild(documentPrintWindow);
}
}
}
public logMessages = (messages: unknown[], level: 'error' | 'warning' = 'error') => {
const {
suppressErrors,
} = this.props;
if (!suppressErrors) {
if (level === 'error') {
console.error(messages); // eslint-disable-line no-console
} else if (level === 'warning') {
console.warn(messages); // eslint-disable-line no-console
}
}
}
public render() {
const {
children,
trigger,
} = this.props;
if (trigger) {
return React.cloneElement(trigger(), {
onClick: this.handleClick,
});
} else {
if (!PrintContext) {
this.logMessages(['"react-to-print" requires React ^16.3.0 to be able to use "PrintContext"']);
return null;
}
const value = { handlePrint: this.handleClick };
return (
<PrintContext.Provider value={value as IPrintContextProps}>
{children}
</PrintContext.Provider>
);
}
}
}
type UseReactToPrintHookReturn = () => void;
export const useReactToPrint = (props: IReactToPrintProps): UseReactToPrintHookReturn => {
if (!hooksEnabled) {
if (!props.suppressErrors) {
console.error('"react-to-print" requires React ^16.8.0 to be able to use "useReactToPrint"'); // eslint-disable-line no-console
}
return () => {
throw new Error('"react-to-print" requires React ^16.8.0 to be able to use "useReactToPrint"');
};
}
const reactToPrint = React.useMemo(
// TODO: is there a better way of applying the defaultProps?
() => new ReactToPrint({ ...defaultProps, ...props }),
[props]
);
return React.useCallback(() => reactToPrint.handleClick(), [reactToPrint]);
};