Prechádzať zdrojové kódy

Allow cell attachments to be added to the cell by dragging
files from filebrowser, native drag/drop or by pasting

madhu94 6 rokov pred
rodič
commit
dcc1578b37

+ 1 - 0
packages/cells/package.json

@@ -46,6 +46,7 @@
     "@jupyterlab/services": "^4.2.0-alpha.0",
     "@phosphor/algorithm": "^1.1.3",
     "@phosphor/coreutils": "^1.3.1",
+    "@phosphor/dragdrop": "^1.3.3",
     "@phosphor/messaging": "^1.2.3",
     "@phosphor/signaling": "^1.2.3",
     "@phosphor/virtualdom": "^1.1.3",

+ 189 - 3
packages/cells/src/widget.ts

@@ -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 ![attachment](attachment: <attachmentName>)
+   * to the cell source
+   */
+  private _updateCellSourceWithAttachment(attachmentName: string) {
+    const textToBeAppended = `![${attachmentName}](attachment:${attachmentName})`;
+    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.
    */

+ 32 - 0
packages/filebrowser/src/listing.ts

@@ -18,6 +18,8 @@ import {
 
 import { DocumentRegistry } from '@jupyterlab/docregistry';
 
+import { imageRendererFactory } from '@jupyterlab/rendermime';
+
 import { Contents } from '@jupyterlab/services';
 
 import { IIconRegistry } from '@jupyterlab/ui-components';
@@ -117,6 +119,11 @@ const MODIFIED_ID_CLASS = 'jp-id-modified';
  */
 const CONTENTS_MIME = 'application/x-jupyter-icontents';
 
+/**
+ * The mime type for attachments
+ */
+const ATTACHMENTS_MIME = 'application/vnd.jupyter.attachments;closure=true';
+
 /**
  * The class name added to drop targets.
  */
@@ -1189,12 +1196,37 @@ export class DirListing extends Widget {
       proposedAction: 'move'
     });
     let basePath = this._model.path;
+
     let paths = toArray(
       map(selectedNames, name => {
         return PathExt.join(basePath, name);
       })
     );
     this._drag.mimeData.setData(CONTENTS_MIME, paths);
+
+    let services = this.model.manager.services;
+    const selectedItems: Contents.IModel[] = [];
+    Object.keys(this._selection).forEach(itemname => {
+      if (this._selection[itemname]) {
+        const item = find(items, item => item.name === itemname);
+        if (
+          item.type === 'file' &&
+          imageRendererFactory.mimeTypes.indexOf(item.mimetype) !== -1
+        ) {
+          selectedItems.push(item);
+        }
+      }
+    });
+    // We thunk this so we don't try to make a network call
+    // when it's not needed. E.g. just moving files around
+    // in a filebrowser
+    if (selectedItems.length > 0) {
+      let attachmentThunks = paths.map(path => {
+        return () => services.contents.get(path);
+      });
+      this._drag.mimeData.setData(ATTACHMENTS_MIME, attachmentThunks);
+    }
+
     if (item && item.type !== 'directory') {
       const otherPaths = paths.slice(1).reverse();
       this._drag.mimeData.setData(FACTORY_MIME, () => {

+ 6 - 1
packages/filebrowser/tsconfig.json

@@ -4,7 +4,9 @@
     "outDir": "lib",
     "rootDir": "src"
   },
-  "include": ["src/*"],
+  "include": [
+    "src/*"
+  ],
   "references": [
     {
       "path": "../apputils"
@@ -18,6 +20,9 @@
     {
       "path": "../docregistry"
     },
+    {
+      "path": "../rendermime"
+    },
     {
       "path": "../services"
     },

+ 1 - 0
packages/notebook/src/widget.ts

@@ -1299,6 +1299,7 @@ export class Notebook extends StaticNotebook {
     node.addEventListener('mousedown', this);
     node.addEventListener('keydown', this);
     node.addEventListener('dblclick', this);
+
     node.addEventListener('focusin', this);
     node.addEventListener('focusout', this);
     // Capture drag events for the notebook widget