react-to-print
Version:
Print React components in the browser
388 lines (387 loc) • 13 kB
JavaScript
import { useCallback as Y } from "react";
const q = "printWindow";
function V(e) {
const t = document.createElement("iframe");
return t.width = `${document.documentElement.clientWidth}px`, t.height = `${document.documentElement.clientHeight}px`, t.style.position = "absolute", t.style.top = `-${document.documentElement.clientHeight + 100}px`, t.style.left = `-${document.documentElement.clientWidth + 100}px`, t.id = q, t.srcdoc = "<!DOCTYPE html>", e && (e.allow && (t.allow = e.allow), e.referrerPolicy !== void 0 && (t.referrerPolicy = e.referrerPolicy), e.sandbox !== void 0 && (t.sandbox = e.sandbox)), t;
}
function f({ level: e = "error", messages: t, suppressErrors: n = !1 }) {
n || (e === "error" ? console.error(t) : e === "warning" ? console.warn(t) : console.debug(t));
}
function $(e, t) {
if (t || !e) {
const n = document.getElementById(q);
n && document.body.removeChild(n);
}
}
function P(e) {
return e instanceof Error ? e : new Error("Unknown Error");
}
function H(e, t) {
const {
documentTitle: n,
onAfterPrint: l,
onPrintError: p,
preserveAfterPrint: m,
print: h,
suppressErrors: g
} = t;
setTimeout(() => {
if (e.contentWindow) {
let a = function() {
l?.(), $(m);
};
if (e.contentWindow.focus(), h)
h(e).then(a).catch((c) => {
p ? p("print", P(c)) : f({
messages: ["An error was thrown by the specified `print` function"],
suppressErrors: g
});
});
else {
if (e.contentWindow.print) {
const c = e.contentDocument?.title ?? "", E = e.ownerDocument.title, y = typeof n == "function" ? n() : n;
y && (e.ownerDocument.title = y, e.contentDocument && (e.contentDocument.title = y)), e.contentWindow.print(), y && (e.ownerDocument.title = E, e.contentDocument && (e.contentDocument.title = c));
} else
f({
messages: ["Printing for this browser is not currently possible: the browser does not have a `print` method available for iframes."],
suppressErrors: g
});
z() ? setTimeout(a, 500) : a();
}
} else
f({
messages: ["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/MatthewHerbst/react-to-print/issues/"],
suppressErrors: g
});
}, 500);
}
function z() {
return [
/Android/i,
/webOS/i,
/iPhone/i,
/iPad/i,
/iPod/i,
/BlackBerry/i,
/Windows Phone/i
].some((t) => (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(navigator.userAgent ?? // Retained for compatibility with browsers that use `navigator.vendor` to identify the browser.
// eslint-disable-next-line @typescript-eslint/no-deprecated
navigator.vendor ?? // Retained for compatibility with older versions of Opera that use `window.opera`.
("opera" in window && window.opera)).match(t)
));
}
function O(e) {
const t = [], n = document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT, null);
let l = n.nextNode();
for (; l; )
t.push(l), l = n.nextNode();
return t;
}
function j(e, t, n) {
const l = O(e), p = O(t);
if (l.length !== p.length) {
f({
messages: ["When cloning shadow root content, source and target elements have different size. `onBeforePrint` likely resolved too early.", e, t],
suppressErrors: n
});
return;
}
for (let m = 0; m < l.length; m++) {
const h = l[m], g = p[m], a = h.shadowRoot;
if (a !== null) {
const c = g.attachShadow({ mode: a.mode });
c.innerHTML = a.innerHTML, j(a, c, n);
}
}
}
const G = `
@page {
/* Remove browser default header (title) and footer (url) */
margin: 0;
}
@media print {
body {
/* Tell browsers to print background colors */
color-adjust: exact; /* Firefox. This is an older version of "print-color-adjust" */
print-color-adjust: exact; /* Firefox/Safari */
-webkit-print-color-adjust: exact; /* Chrome/Safari/Edge/Opera */
}
}
`;
function W(e, t, n) {
const {
contentNode: l,
clonedContentNode: p,
clonedImgNodes: m,
clonedVideoNodes: h,
numResourcesToLoad: g,
originalCanvasNodes: a
} = t, {
bodyClass: c,
fonts: E,
ignoreGlobalStyles: y,
pageStyle: C,
nonce: T,
suppressErrors: A,
copyShadowRoots: F
} = n, L = [], _ = [];
function i(k, x) {
if (L.includes(k)) {
f({
level: "debug",
messages: ["Tried to mark a resource that has already been handled", k],
suppressErrors: A
});
return;
}
x ? (f({
messages: [
'"react-to-print" was unable to load a resource but will continue attempting to print the page',
...x
],
suppressErrors: A
}), _.push(k)) : L.push(k), L.length + _.length === g && H(e, n);
}
e.onload = null;
const d = e.contentDocument ?? e.contentWindow?.document;
if (d) {
const k = d.body.appendChild(p);
F && j(l, k, !!A), E && (e.contentDocument?.fonts && e.contentWindow?.FontFace ? E.forEach((s) => {
const o = new FontFace(
s.family,
s.source,
{ weight: s.weight, style: s.style }
);
e.contentDocument.fonts.add(o), o.loaded.then(() => {
i(o);
}).catch((b) => {
i(o, ["Failed loading the font:", o, "Load error:", P(b)]);
});
}) : (E.forEach((s) => {
i(s);
}), f({
messages: ['"react-to-print" is not able to load custom fonts because the browser does not support the FontFace API but will continue attempting to print the page'],
suppressErrors: A
})));
const x = C ?? G, D = d.createElement("style");
T && (D.setAttribute("nonce", T), d.head.setAttribute("nonce", T)), D.appendChild(d.createTextNode(x)), d.head.appendChild(D), c && d.body.classList.add(...c.split(" "));
const U = d.querySelectorAll("canvas");
for (let s = 0; s < a.length; ++s) {
const o = a[s], b = U[s];
if (b === void 0) {
f({
messages: ["A canvas element could not be copied for printing, has it loaded? `onBeforePrint` likely resolved too early.", o],
suppressErrors: A
});
continue;
}
const r = b.getContext("2d");
r && r.drawImage(o, 0, 0);
}
for (let s = 0; s < m.length; s++) {
const o = m[s], b = o.getAttribute("src");
if (!b)
i(o, ['Found an <img> tag with an empty "src" attribute. This prevents pre-loading it.', o]);
else {
const r = new Image();
r.onload = () => {
i(o);
}, r.onerror = (u, w, S, v, N) => {
i(o, ["Error loading <img>", o, "Error", N]);
}, r.src = b;
}
}
for (let s = 0; s < h.length; s++) {
const o = h[s];
o.preload = "auto";
const b = o.getAttribute("poster");
if (b) {
const r = new Image();
r.onload = () => {
i(o);
}, r.onerror = (u, w, S, v, N) => {
i(o, ["Error loading video poster", b, "for video", o, "Error:", N]);
}, r.src = b;
} else
o.readyState >= 2 ? i(o) : o.src ? (o.onloadeddata = () => {
i(o);
}, o.onerror = (r, u, w, S, v) => {
i(o, ["Error loading video", o, "Error", v]);
}, o.onstalled = () => {
i(o, ["Loading video stalled, skipping", o]);
}) : i(o, ["Error loading video, `src` is empty", o]);
}
const R = "select", M = l.querySelectorAll(R), B = d.querySelectorAll(R);
for (let s = 0; s < M.length; s++)
B[s].value = M[s].value;
if (!y) {
const s = document.querySelectorAll("style, link[rel~='stylesheet'], link[as='style']");
for (let o = 0, b = s.length; o < b; ++o) {
const r = s[o];
if (r.tagName.toLowerCase() === "style") {
const u = d.createElement(r.tagName), w = r.sheet;
if (w) {
let S = "";
try {
const v = w.cssRules.length;
for (let N = 0; N < v; ++N)
typeof w.cssRules[N].cssText == "string" && (S += `${w.cssRules[N].cssText}\r
`);
} catch (v) {
f({
messages: [
"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/MatthewHerbst/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.",
// eslint-disable-line max-len
r,
`Original error: ${P(v).message}`
],
level: "warning"
});
}
u.setAttribute("id", `react-to-print-${o}`), T && u.setAttribute("nonce", T), u.appendChild(d.createTextNode(S)), d.head.appendChild(u);
}
} else if (r.getAttribute("href"))
if (r.hasAttribute("disabled"))
f({
messages: ["`react-to-print` encountered a <link> tag with a `disabled` attribute and will ignore it. Note that the `disabled` attribute is deprecated, and some browsers ignore it. You should stop using it. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled. The <link> is:", r],
level: "warning"
}), i(r);
else {
const u = d.createElement(r.tagName);
for (let w = 0, S = r.attributes.length; w < S; ++w) {
const v = r.attributes[w];
v && u.setAttribute(v.nodeName, v.nodeValue ?? "");
}
u.onload = () => {
i(u);
}, u.onerror = (w, S, v, N, I) => {
i(u, ["Failed to load", u, "Error:", I]);
}, T && u.setAttribute("nonce", T), d.head.appendChild(u);
}
else
f({
messages: ["`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:", r],
level: "warning"
}), i(r);
}
}
}
g === 0 && H(e, n);
}
function J(e, t, n) {
e.onload = () => {
W(
e,
t,
n
);
}, document.body.appendChild(e);
}
function K({ contentRef: e, optionalContent: t, suppressErrors: n }) {
if (t && typeof t == "function")
return e && f({
level: "warning",
messages: ['"react-to-print" received a `contentRef` option and an optional-content param passed to its callback. The `contentRef` option will be ignored.']
}), t();
if (e)
return e.current;
f({
messages: ['"react-to-print" did not receive a `contentRef` option or a optional-content param pass to its callback.'],
suppressErrors: n
});
}
function Q(e, t) {
const {
contentRef: n,
fonts: l,
ignoreGlobalStyles: p,
suppressErrors: m
} = t, h = K({
contentRef: n,
optionalContent: e,
suppressErrors: m
});
if (!h)
return;
const g = h.cloneNode(!0), a = document.querySelectorAll("link[rel~='stylesheet'], link[as='style']"), c = g.querySelectorAll("img"), E = g.querySelectorAll("video"), y = l ? l.length : 0, C = (p ? 0 : a.length) + c.length + E.length + y;
return {
contentNode: h,
clonedContentNode: g,
clonedImgNodes: c,
clonedVideoNodes: E,
numResourcesToLoad: C,
originalCanvasNodes: h.querySelectorAll("canvas")
};
}
function Z({
bodyClass: e,
contentRef: t,
copyShadowRoots: n,
documentTitle: l,
fonts: p,
ignoreGlobalStyles: m,
nonce: h,
onAfterPrint: g,
onBeforePrint: a,
onPrintError: c,
pageStyle: E,
preserveAfterPrint: y,
print: C,
printIframeProps: T,
suppressErrors: A
}) {
return Y((L) => {
$(y, !0);
function _() {
const i = {
bodyClass: e,
contentRef: t,
copyShadowRoots: n,
documentTitle: l,
fonts: p,
ignoreGlobalStyles: m,
nonce: h,
onAfterPrint: g,
onPrintError: c,
pageStyle: E,
preserveAfterPrint: y,
print: C,
suppressErrors: A
}, d = V(T), k = Q(L, i);
if (!k) {
f({
messages: ["There is nothing to print"],
suppressErrors: A
});
return;
}
J(d, k, i);
}
a ? a().then(() => {
_();
}).catch((i) => {
c?.("onBeforePrint", P(i));
}) : _();
}, [
e,
t,
n,
l,
p,
m,
h,
g,
a,
c,
E,
y,
T,
C,
A
]);
}
export {
Z as useReactToPrint
};