UNPKG

5.91 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { ServerConnection } from '@jupyterlab/services';
5import { Widget } from '@lumino/widgets';
6
7/**
8 * Any object is "printable" if it implements the `IPrintable` interface.
9 *
10 * To do this it, it must have a method called `Printing.symbol` which returns either a function
11 * to print the object or null if it cannot be printed.
12 *
13 * One way of printing is to use the `printWidget` function, which creates a hidden iframe
14 * and copies the DOM nodes from your widget to that iframe and printing just that iframe.
15 *
16 * Another way to print is to use the `printURL` function, which takes a URL and prints that page.
17 */
18
19export namespace Printing {
20 /**
21 * Function that takes no arguments and when invoked prints out some object or null if printing is not defined.
22 */
23 export type OptionalAsyncThunk = (() => Promise<void>) | null;
24
25 /**
26 * Symbol to use for a method that returns a function to print an object.
27 */
28 export const symbol = Symbol('printable');
29
30 /**
31 * Objects who provide a custom way of printing themselves
32 * should implement this interface.
33 */
34 export interface IPrintable {
35 /**
36 * Returns a function to print this object or null if it cannot be printed.
37 */
38 [symbol]: () => OptionalAsyncThunk;
39 }
40
41 /**
42 * Returns whether an object implements a print method.
43 */
44 export function isPrintable(a: unknown): a is IPrintable {
45 if (typeof a !== 'object' || !a) {
46 return false;
47 }
48 return symbol in a;
49 }
50
51 /**
52 * Returns the print function for an object, or null if it does not provide a handler.
53 */
54 export function getPrintFunction(val: unknown): OptionalAsyncThunk {
55 if (isPrintable(val)) {
56 return val[symbol]();
57 }
58 return null;
59 }
60
61 /**
62 * Prints a widget by copying it's DOM node
63 * to a hidden iframe and printing that iframe.
64 */
65 export function printWidget(widget: Widget): Promise<void> {
66 return printContent(widget.node);
67 }
68
69 /**
70 * Prints a URL by loading it into an iframe.
71 *
72 * @param url URL to load into an iframe.
73 */
74 export async function printURL(url: string): Promise<void> {
75 const settings = ServerConnection.makeSettings();
76 const text = await (
77 await ServerConnection.makeRequest(url, {}, settings)
78 ).text();
79 return printContent(text);
80 }
81
82 /**
83 * Prints a URL or an element in an iframe and then removes the iframe after printing.
84 */
85 async function printContent(textOrEl: string | HTMLElement): Promise<void> {
86 const isText = typeof textOrEl === 'string';
87 const iframe = createIFrame();
88
89 const parent = window.document.body;
90 parent.appendChild(iframe);
91 if (isText) {
92 iframe.srcdoc = textOrEl as string;
93 await resolveWhenLoaded(iframe);
94 } else {
95 iframe.src = 'about:blank';
96 await resolveWhenLoaded(iframe);
97 setIFrameNode(iframe, textOrEl as HTMLElement);
98 }
99 const printed = resolveAfterEvent();
100 launchPrint(iframe.contentWindow!);
101 // Once the print dialog has been dismissed, we regain event handling,
102 // and it should be safe to discard the hidden iframe.
103 await printed;
104 parent.removeChild(iframe);
105 }
106
107 /**
108 * Creates a new hidden iframe and appends it to the document
109 *
110 * Modified from
111 * https://github.com/joseluisq/printd/blob/eb7948d602583c055ab6dee3ee294b6a421da4b6/src/index.ts#L24
112 */
113 function createIFrame(): HTMLIFrameElement {
114 const el = window.document.createElement('iframe');
115
116 // We need both allow-modals and allow-same-origin to be able to
117 // call print in the iframe.
118 // We intentionally do not allow scripts:
119 // https://github.com/jupyterlab/jupyterlab/pull/5850#pullrequestreview-230899790
120 el.setAttribute('sandbox', 'allow-modals allow-same-origin');
121 const css =
122 'visibility:hidden;width:0;height:0;position:absolute;z-index:-9999;bottom:0;';
123 el.setAttribute('style', css);
124 el.setAttribute('width', '0');
125 el.setAttribute('height', '0');
126
127 return el;
128 }
129
130 /**
131 * Copies a node from the base document to the iframe.
132 */
133 function setIFrameNode(iframe: HTMLIFrameElement, node: HTMLElement) {
134 iframe.contentDocument!.body.appendChild(node.cloneNode(true));
135 iframe.contentDocument!.close();
136 }
137
138 /**
139 * Promise that resolves when all resources are loaded in the window.
140 */
141 function resolveWhenLoaded(iframe: HTMLIFrameElement): Promise<void> {
142 return new Promise(resolve => {
143 iframe.onload = () => resolve();
144 });
145 }
146
147 /**
148 * A promise that resolves after the next mousedown, mousemove, or
149 * keydown event. We use this as a proxy for determining when the
150 * main window has regained control after the print dialog is removed.
151 *
152 * We can't use the usual window.onafterprint handler because we
153 * disallow Javascript execution in the print iframe.
154 */
155 function resolveAfterEvent(): Promise<void> {
156 return new Promise(resolve => {
157 const onEvent = () => {
158 document.removeEventListener('mousemove', onEvent, true);
159 document.removeEventListener('mousedown', onEvent, true);
160 document.removeEventListener('keydown', onEvent, true);
161 resolve();
162 };
163 document.addEventListener('mousemove', onEvent, true);
164 document.addEventListener('mousedown', onEvent, true);
165 document.addEventListener('keydown', onEvent, true);
166 });
167 }
168
169 /**
170 * Prints a content window.
171 */
172 function launchPrint(contentWindow: Window) {
173 const result = contentWindow.document.execCommand('print', false);
174 // execCommand won't work in firefox so we call the `print` method instead if it fails
175 // https://github.com/joseluisq/printd/blob/eb7948d602583c055ab6dee3ee294b6a421da4b6/src/index.ts#L148
176 if (!result) {
177 contentWindow.print();
178 }
179 }
180}