Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for printing widgets #5850

Merged
merged 45 commits into from May 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ef8213e
Add printd
saulshanabrook Jan 9, 2019
a788ee7
Move printint to apputils and add support docs
saulshanabrook Jan 9, 2019
ecf0a10
Add printing for images, JSON and inspector
saulshanabrook Jan 9, 2019
00349dc
Add documentation to printing module
saulshanabrook Jan 9, 2019
c58e78c
Check if printable again when executing
saulshanabrook Jan 9, 2019
1b01f68
Add newline back
saulshanabrook Jan 9, 2019
e441c50
Allow notebook printing
saulshanabrook Jan 9, 2019
7838425
Rename printd function to printWidget
saulshanabrook Jan 9, 2019
6f6b967
Fix linting
saulshanabrook Jan 9, 2019
1da3604
Reset iframe src when printing
saulshanabrook Jan 16, 2019
aa8e557
Start adding generic printing
saulshanabrook Jan 31, 2019
07fb2ed
Remove overly abstracted definition
saulshanabrook Mar 18, 2019
3a77854
Add more logging when processing invalid JSON
saulshanabrook Mar 20, 2019
67dbe5d
Start moving to new printing system
saulshanabrook Mar 20, 2019
8ad8972
Remove base URL references
saulshanabrook Mar 20, 2019
13c63ae
Simplify implementation
saulshanabrook Mar 20, 2019
cbc43d9
Fix printing typing
saulshanabrook Mar 21, 2019
4cd02f0
Merge origin/master into print-notebook
saulshanabrook Apr 3, 2019
3f93b58
Handle all async functions
saulshanabrook Apr 3, 2019
fe033cc
Remove unused callback arg
saulshanabrook Apr 3, 2019
0c65a50
Fix import
saulshanabrook Apr 3, 2019
45d181b
remove special case CSS inspector print rules
saulshanabrook Apr 3, 2019
b5b2527
Fix default print popup on firefox
saulshanabrook Apr 3, 2019
c4240cd
Fixx null check
saulshanabrook Apr 3, 2019
e442930
Upgrade to latest printd and remove firefox workaround
saulshanabrook Apr 10, 2019
61e978f
Remove printd
saulshanabrook Apr 11, 2019
5e415dc
Add comments
saulshanabrook Apr 24, 2019
58dbb5b
Merge origin/master into print-notebook
saulshanabrook Apr 24, 2019
3be993e
Remove printd dep
saulshanabrook Apr 24, 2019
80b3d5e
Fix iframe usage by downloading URL seperately
saulshanabrook Apr 24, 2019
055f59a
Remove logging
saulshanabrook Apr 24, 2019
310a272
Add default shortcut for printing
saulshanabrook Apr 24, 2019
3c9aadd
Merge origin/master into print-notebook
saulshanabrook May 6, 2019
959268f
Move printing to apputils
saulshanabrook May 6, 2019
2f9f672
Fix apputils print shortcut
saulshanabrook May 6, 2019
3bfc916
Change to use server connection instead of fetch
saulshanabrook May 6, 2019
8c7ea4c
Try waiting till done printing to clean up iframe
saulshanabrook May 6, 2019
8703549
Delete iframe after printing
saulshanabrook May 6, 2019
1593668
Since we can't use traditional event handlers in the print iframe when
ian-r-rose May 6, 2019
d225704
Merge branch 'master' into print-notebook
saulshanabrook May 13, 2019
ca9890d
Only create server settings once for printing
saulshanabrook May 13, 2019
a406241
Linting fix
saulshanabrook May 13, 2019
609799b
Remove unused import
saulshanabrook May 13, 2019
77cb8da
Remove `contextMenuWidget`
saulshanabrook May 13, 2019
208aa83
Get both widget from main app for printing
saulshanabrook May 13, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion buildutils/src/utils.ts
Expand Up @@ -54,7 +54,11 @@ export function writePackageData(pkgJsonPath: string, data: any): boolean {
* Read a json file.
*/
export function readJSONFile(filePath: string): any {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
throw `Cannot read JSON for path ${filePath}: ${e}`;
}
}

/**
Expand Down
14 changes: 14 additions & 0 deletions packages/apputils-extension/schema/print.json
@@ -0,0 +1,14 @@
{
"title": "Print",
"description": "Print settings.",
"jupyter.lab.shortcuts": [
{
"command": "apputils:print",
"keys": ["Accel P"],
"selector": "body"
}
],
"additionalProperties": false,
"properties": {},
"type": "object"
}
29 changes: 27 additions & 2 deletions packages/apputils-extension/src/index.ts
Expand Up @@ -17,7 +17,8 @@ import {
IThemeManager,
IWindowResolver,
ThemeManager,
WindowResolver
WindowResolver,
Printing
} from '@jupyterlab/apputils';

import {
Expand Down Expand Up @@ -69,6 +70,8 @@ namespace CommandIDs {
export const resetOnLoad = 'apputils:reset-on-load';

export const saveState = 'apputils:save-statedb';

export const print = 'apputils:print';
}

/**
Expand Down Expand Up @@ -298,6 +301,27 @@ const splash: JupyterFrontEndPlugin<ISplashScreen> = {
}
};

const print: JupyterFrontEndPlugin<void> = {
id: '@jupyterlab/apputils-extension:print',
autoStart: true,
activate: (app: JupyterFrontEnd) => {
app.commands.addCommand(CommandIDs.print, {
label: 'Print...',
isEnabled: () => {
const widget = app.shell.currentWidget;
return Printing.getPrintFunction(widget) !== null;
},
execute: async () => {
const widget = app.shell.currentWidget;
const printFunction = Printing.getPrintFunction(widget);
if (printFunction) {
await printFunction();
}
}
});
}
};

/**
* The default state database for storing application state.
*/
Expand Down Expand Up @@ -567,7 +591,8 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
state,
splash,
themes,
themesPaletteMenu
themesPaletteMenu,
print
];
export default plugins;

Expand Down
1 change: 1 addition & 0 deletions packages/apputils/src/index.ts
Expand Up @@ -15,6 +15,7 @@ export * from './iframe';
export * from './inputdialog';
export * from './instancetracker';
export * from './mainareawidget';
export * from './printing';
export * from './sanitizer';
export * from './spinner';
export * from './splash';
Expand Down
12 changes: 11 additions & 1 deletion packages/apputils/src/mainareawidget.ts
Expand Up @@ -11,6 +11,8 @@ import { Toolbar } from './toolbar';

import { DOMUtils } from './domutils';

import { Printing } from './printing';

/**
* A widget meant to be contained in the JupyterLab main area.
*
Expand All @@ -20,7 +22,8 @@ import { DOMUtils } from './domutils';
* This widget is automatically disposed when closed.
* This widget ensures its own focus when activated.
*/
export class MainAreaWidget<T extends Widget = Widget> extends Widget {
export class MainAreaWidget<T extends Widget = Widget> extends Widget
implements Printing.IPrintable {
/**
* Construct a new main area widget.
*
Expand Down Expand Up @@ -96,6 +99,13 @@ export class MainAreaWidget<T extends Widget = Widget> extends Widget {
}
}

/**
* Print method. Defered to content.
*/
[Printing.symbol](): Printing.OptionalAsyncThunk {
return Printing.getPrintFunction(this._content);
}

/**
* The content hosted by the widget.
*/
Expand Down
178 changes: 178 additions & 0 deletions packages/apputils/src/printing.ts
@@ -0,0 +1,178 @@
/**
* Any object is "printable" if it implements the `IPrintable` interface.
*
* To do this it, it must have a method called `Printing.symbol` which returns either a function
* to print the object or null if it cannot be printed.
*
* One way of printing is to use the `printWidget` function, which creates a hidden iframe
* and copies the DOM nodes from your widget to that iframe and printing just that iframe.
*
* Another way to print is to use the `printURL` function, which takes a URL and prints that page.
*/

import { Widget } from '@phosphor/widgets';
import { ServerConnection } from '@jupyterlab/services';

export namespace Printing {
/**
* Function that takes no arguments and when invoked prints out some object or null if printing is not defined.
*/
export type OptionalAsyncThunk = () => Promise<void> | null;
saulshanabrook marked this conversation as resolved.
Show resolved Hide resolved

/**
* Symbol to use for a method that returns a function to print an object.
*/
export const symbol = Symbol('printable');

/**
* Objects who provide a custom way of printing themselves
* should implement this interface.
*/
export interface IPrintable {
/**
* Returns a function to print this object or null if it cannot be printed.
*/
[symbol]: () => OptionalAsyncThunk;
}

/**
* Returns whether an object implements a print method.
*/
export function isPrintable(a: unknown): a is IPrintable {
if (typeof a !== 'object' || !a) {
return false;
}
return symbol in a;
}

/**
* Returns the print function for an object, or null if it does not provide a handler.
*/
export function getPrintFunction(val: unknown): OptionalAsyncThunk {
if (isPrintable(val)) {
return val[symbol]();
}
return null;
}

/**
* Prints a widget by copying it's DOM node
* to a hidden iframe and printing that iframe.
*/
export function printWidget(widget: Widget): Promise<void> {
return printContent(widget.node);
}

const settings = ServerConnection.makeSettings();
/**
* Prints a URL by loading it into an iframe.
*
* @param url URL to load into an iframe.
*/
export async function printURL(url: string): Promise<void> {
const text = await (await ServerConnection.makeRequest(
url,
{},
settings
)).text();
return printContent(text);
}

/**
* Prints a URL or an element in an iframe and then removes the iframe after printing.
*/
async function printContent(textOrEl: string | HTMLElement): Promise<void> {
const isText = typeof textOrEl === 'string';
const iframe = createIFrame();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could use the IFrame widget here. Not necessary, but worth thinking about.


const parent = window.document.body;
parent.appendChild(iframe);
if (isText) {
iframe.srcdoc = textOrEl as string;
await resolveWhenLoaded(iframe);
} else {
iframe.src = 'about:blank';
setIFrameNode(iframe, textOrEl as HTMLElement);
}
const printed = resolveAfterEvent();
launchPrint(iframe.contentWindow);
ian-r-rose marked this conversation as resolved.
Show resolved Hide resolved
// Once the print dialog has been dismissed, we regain event handling,
// and it should be safe to discard the hidden iframe.
await printed;
parent.removeChild(iframe);
}

/**
* Creates a new hidden iframe and appends it to the document
*
* Modified from
* https://github.com/joseluisq/printd/blob/eb7948d602583c055ab6dee3ee294b6a421da4b6/src/index.ts#L24
*/
function createIFrame(): HTMLIFrameElement {
const el = window.document.createElement('iframe');

// We need both allow-modals and allow-same-origin to be able to
// call print in the iframe.
// We intentionally do not allow scripts:
// https://github.com/jupyterlab/jupyterlab/pull/5850#pullrequestreview-230899790
el.setAttribute('sandbox', 'allow-modals allow-same-origin');
const css =
'visibility:hidden;width:0;height:0;position:absolute;z-index:-9999;bottom:0;';
el.setAttribute('style', css);
el.setAttribute('width', '0');
el.setAttribute('height', '0');

return el;
}

/**
* Copies a node from the base document to the iframe.
*/
function setIFrameNode(iframe: HTMLIFrameElement, node: HTMLElement) {
iframe.contentDocument.body.appendChild(node.cloneNode(true));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to wait for the load event? My experience with the PDF viewer is that it was necessary, but maybe that's not the case here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't found we needed to wait.

iframe.contentDocument.close();
}

/**
* Promise that resolves when all resources are loaded in the window.
*/
function resolveWhenLoaded(iframe: HTMLIFrameElement): Promise<void> {
return new Promise(resolve => {
iframe.onload = () => resolve();
});
}

/**
* A promise that resolves after the next mousedown, mousemove, or
* keydown event. We use this as a proxy for determining when the
* main window has regained control after the print dialog is removed.
*
* We can't use the usual window.onafterprint handler because we
* disallow Javascript execution in the print iframe.
*/
function resolveAfterEvent(): Promise<void> {
return new Promise(resolve => {
const onEvent = () => {
document.removeEventListener('mousemove', onEvent, true);
document.removeEventListener('mousedown', onEvent, true);
document.removeEventListener('keydown', onEvent, true);
resolve();
};
document.addEventListener('mousemove', onEvent, true);
document.addEventListener('mousedown', onEvent, true);
document.addEventListener('keydown', onEvent, true);
});
}

/**
* Prints a content window.
*/
function launchPrint(contentWindow: Window) {
const result = contentWindow.document.execCommand('print', false, null);
// execCommand won't work in firefox so we call the `print` method instead if it fails
// https://github.com/joseluisq/printd/blob/eb7948d602583c055ab6dee3ee294b6a421da4b6/src/index.ts#L148
if (!result) {
contentWindow.print();
}
}
}
21 changes: 21 additions & 0 deletions packages/coreutils/src/pageconfig.ts
Expand Up @@ -154,6 +154,27 @@ export namespace PageConfig {
return URLExt.normalize(wsUrl);
}

/**
* Returns the URL converting this notebook to a certain
* format with nbconvert.
*/
export function getNBConvertURL({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afshin Can you comment on how this would relate to your recent work with JupyterFrontEnd.IPaths and PageConfig? What would be the preferred way for a notebook widget to get a handle on the NBConvert URL?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping @afshin on this

path,
format,
download
}: {
path: string;
format: string;
download: boolean;
}): string {
const notebookPath = URLExt.encodeParts(path);
const url = URLExt.join(getBaseUrl(), 'nbconvert', format, notebookPath);
if (download) {
return url + '?download=true';
}
return url;
}

/**
* Get the authorization token for a Jupyter application.
*/
Expand Down
9 changes: 8 additions & 1 deletion packages/docregistry/src/mimedocument.ts
@@ -1,7 +1,7 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { showErrorMessage } from '@jupyterlab/apputils';
import { showErrorMessage, Printing } from '@jupyterlab/apputils';

import { ActivityMonitor } from '@jupyterlab/coreutils';

Expand Down Expand Up @@ -78,6 +78,13 @@ export class MimeContent extends Widget {
*/
readonly mimeType: string;

/**
* Print method. Defered to the renderer.
*/
[Printing.symbol]() {
return Printing.getPrintFunction(this.renderer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I think we could probably get this working for PDFs as well.

}

/**
* A promise that resolves when the widget is ready.
*/
Expand Down
11 changes: 10 additions & 1 deletion packages/imageviewer/src/widget.ts
Expand Up @@ -3,6 +3,8 @@

import { PathExt } from '@jupyterlab/coreutils';

import { Printing } from '@jupyterlab/apputils';

import {
ABCWidgetFactory,
DocumentRegistry,
Expand All @@ -24,7 +26,7 @@ const IMAGE_CLASS = 'jp-ImageViewer';
/**
* A widget for images.
*/
export class ImageViewer extends Widget {
export class ImageViewer extends Widget implements Printing.IPrintable {
/**
* Construct a new image widget.
*/
Expand Down Expand Up @@ -54,6 +56,13 @@ export class ImageViewer extends Widget {
});
}

/**
* Print in iframe.
*/
[Printing.symbol]() {
return () => Printing.printWidget(this);
}

/**
* The image widget's context.
*/
Expand Down