|
@@ -7,7 +7,12 @@ import { AttachmentsResolver } from '@jupyterlab/attachments';
|
|
|
|
|
|
import { IClientSession } from '@jupyterlab/apputils';
|
|
|
|
|
|
-import { IChangedArgs, ActivityMonitor } from '@jupyterlab/coreutils';
|
|
|
+import {
|
|
|
+ IChangedArgs,
|
|
|
+ ActivityMonitor,
|
|
|
+ nbformat,
|
|
|
+ URLExt
|
|
|
+} from '@jupyterlab/coreutils';
|
|
|
|
|
|
import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';
|
|
|
|
|
@@ -28,10 +33,12 @@ import {
|
|
|
IRenderMimeRegistry
|
|
|
} from '@jupyterlab/rendermime';
|
|
|
|
|
|
-import { KernelMessage, Kernel } from '@jupyterlab/services';
|
|
|
+import { KernelMessage, Kernel, Contents } from '@jupyterlab/services';
|
|
|
|
|
|
import { JSONValue, PromiseDelegate, JSONObject } from '@phosphor/coreutils';
|
|
|
|
|
|
+import { IDragEvent } from '@phosphor/dragdrop';
|
|
|
+
|
|
|
import { Message } from '@phosphor/messaging';
|
|
|
|
|
|
import { PanelLayout, Panel, Widget } from '@phosphor/widgets';
|
|
@@ -48,6 +55,7 @@ import {
|
|
|
import { InputArea, IInputPrompt, InputPrompt } from './inputarea';
|
|
|
|
|
|
import {
|
|
|
+ IAttachmentsCellModel,
|
|
|
ICellModel,
|
|
|
ICodeCellModel,
|
|
|
IMarkdownCellModel,
|
|
@@ -143,6 +151,11 @@ const DEFAULT_MARKDOWN_TEXT = 'Type Markdown and LaTeX: $ α^2 $';
|
|
|
*/
|
|
|
const RENDER_TIMEOUT = 1000;
|
|
|
|
|
|
+/**
|
|
|
+ * The mime type for attachments
|
|
|
+ */
|
|
|
+const ATTACHMENTS_MIME = 'application/vnd.jupyter.attachments;closure=true';
|
|
|
+
|
|
|
/******************************************************************************
|
|
|
* Cell
|
|
|
******************************************************************************/
|
|
@@ -1056,6 +1069,179 @@ export namespace CodeCell {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * `AttachmentsCell` - A base class for a cell widget that allows
|
|
|
+ * attachments to be drag/drop'd or pasted onto it
|
|
|
+ */
|
|
|
+export class AttachmentsCell extends Cell {
|
|
|
+ /**
|
|
|
+ * Handle the DOM events for the widget.
|
|
|
+ *
|
|
|
+ * @param event - The DOM event sent to the widget.
|
|
|
+ *
|
|
|
+ * #### Notes
|
|
|
+ * This method implements the DOM `EventListener` interface and is
|
|
|
+ * called in response to events on the notebook panel's node. It should
|
|
|
+ * not be called directly by user code.
|
|
|
+ */
|
|
|
+ handleEvent(event: Event): void {
|
|
|
+ switch (event.type) {
|
|
|
+ case 'paste':
|
|
|
+ this._evtPaste(event as ClipboardEvent);
|
|
|
+ break;
|
|
|
+ case 'dragenter':
|
|
|
+ event.preventDefault();
|
|
|
+ break;
|
|
|
+ case 'dragover':
|
|
|
+ event.preventDefault();
|
|
|
+ break;
|
|
|
+ case 'drop':
|
|
|
+ this._evtNativeDrop(event as DragEvent);
|
|
|
+ break;
|
|
|
+ case 'p-dragover':
|
|
|
+ this._evtDragOver(event as IDragEvent);
|
|
|
+ break;
|
|
|
+ case 'p-drop':
|
|
|
+ this._evtDrop(event as IDragEvent);
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private _evtDragOver(event: IDragEvent) {
|
|
|
+ if (!event.mimeData.getData(ATTACHMENTS_MIME)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ event.preventDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handle the `paste` event for the widget
|
|
|
+ */
|
|
|
+ private _evtPaste(event: ClipboardEvent): void {
|
|
|
+ this._attachFiles(event.clipboardData.items);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handle the `drop` event for the widget
|
|
|
+ */
|
|
|
+ private _evtNativeDrop(event: DragEvent): void {
|
|
|
+ this._attachFiles(event.dataTransfer.items);
|
|
|
+ event.preventDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handle the `'p-drop'` event for the widget.
|
|
|
+ */
|
|
|
+ private _evtDrop(event: IDragEvent): void {
|
|
|
+ if (!event.mimeData.hasData(ATTACHMENTS_MIME)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ if (event.proposedAction === 'none') {
|
|
|
+ event.dropAction = 'none';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const thunks = event.mimeData.getData(ATTACHMENTS_MIME);
|
|
|
+ const promises: Promise<Contents.IModel>[] = [];
|
|
|
+ thunks.forEach((fn: Function) => {
|
|
|
+ promises.push(fn());
|
|
|
+ });
|
|
|
+ Promise.all(promises).then(models => {
|
|
|
+ models.forEach(model => {
|
|
|
+ if (model.type === 'file' && model.format === 'base64') {
|
|
|
+ this.model.attachments.set(model.name, {
|
|
|
+ [model.mimetype]: model.content
|
|
|
+ });
|
|
|
+ this._updateCellSourceWithAttachment(model.name);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handle `after-attach` messages for the widget.
|
|
|
+ */
|
|
|
+ protected onAfterAttach(msg: Message): void {
|
|
|
+ super.onAfterAttach(msg);
|
|
|
+ let node = this.node;
|
|
|
+ node.addEventListener('p-dragover', this);
|
|
|
+ node.addEventListener('p-drop', this);
|
|
|
+ node.addEventListener('dragenter', this);
|
|
|
+ node.addEventListener('dragover', this);
|
|
|
+ node.addEventListener('drop', this);
|
|
|
+ node.addEventListener('paste', this);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A message handler invoked on a `'before-detach'`
|
|
|
+ * message
|
|
|
+ */
|
|
|
+ protected onBeforeDetach(msg: Message): void {
|
|
|
+ let node = this.node;
|
|
|
+ node.removeEventListener('drop', this);
|
|
|
+ node.removeEventListener('dragover', this);
|
|
|
+ node.removeEventListener('dragenter', this);
|
|
|
+ node.removeEventListener('paste', this);
|
|
|
+ node.removeEventListener('p-dragover', this);
|
|
|
+ node.removeEventListener('p-drop', this);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Attaches all DataTransferItems (obtained from
|
|
|
+ * clipboard or native drop events) to the cell
|
|
|
+ */
|
|
|
+ private _attachFiles(items: DataTransferItemList) {
|
|
|
+ for (let i = 0; i < items.length; i++) {
|
|
|
+ const item = items[i];
|
|
|
+ if (item.kind === 'file') {
|
|
|
+ const blob = item.getAsFile();
|
|
|
+ this._attachFile(blob);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Takes in a file object and adds it to
|
|
|
+ * the cell attachments
|
|
|
+ */
|
|
|
+ private _attachFile(blob: File) {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.readAsDataURL(blob);
|
|
|
+ reader.onload = evt => {
|
|
|
+ const { href, protocol } = URLExt.parse(reader.result as string);
|
|
|
+ if (protocol !== 'data:') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const content = href.split(':')[1];
|
|
|
+ const [mimeType, encodedData] = content.split(';');
|
|
|
+ const data = encodedData.split(',')[1];
|
|
|
+ const bundle: nbformat.IMimeBundle = { [mimeType]: data };
|
|
|
+ this.model.attachments.set(blob.name, bundle);
|
|
|
+ this._updateCellSourceWithAttachment(blob.name);
|
|
|
+ };
|
|
|
+ reader.onerror = evt => {
|
|
|
+ console.error(`Failed to attach ${blob.name}` + evt);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Appends the text 
|
|
|
+ * to the cell source
|
|
|
+ */
|
|
|
+ private _updateCellSourceWithAttachment(attachmentName: string) {
|
|
|
+ const textToBeAppended = ``;
|
|
|
+ this.model.value.insert(this.model.value.text.length, textToBeAppended);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The model used by the widget.
|
|
|
+ */
|
|
|
+ readonly model: IAttachmentsCellModel;
|
|
|
+}
|
|
|
+
|
|
|
/******************************************************************************
|
|
|
* MarkdownCell
|
|
|
******************************************************************************/
|
|
@@ -1069,7 +1255,7 @@ export namespace CodeCell {
|
|
|
* or the input area model changes. We don't support automatically
|
|
|
* updating the rendered text in all of these cases.
|
|
|
*/
|
|
|
-export class MarkdownCell extends Cell {
|
|
|
+export class MarkdownCell extends AttachmentsCell {
|
|
|
/**
|
|
|
* Construct a Markdown cell widget.
|
|
|
*/
|