Browse Source

Merge pull request #5571 from ian-r-rose/text-drag-drop

Text drag drop
Steven Silvester 6 years ago
parent
commit
a904d01f6a

+ 1 - 0
packages/codeeditor/package.json

@@ -35,6 +35,7 @@
     "@jupyterlab/observables": "^2.1.1",
     "@phosphor/coreutils": "^1.3.0",
     "@phosphor/disposable": "^1.1.2",
+    "@phosphor/dragdrop": "^1.3.0",
     "@phosphor/messaging": "^1.2.2",
     "@phosphor/signaling": "^1.2.2",
     "@phosphor/widgets": "^1.6.0",

+ 154 - 0
packages/codeeditor/src/widget.ts

@@ -1,6 +1,10 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import { MimeData } from '@phosphor/coreutils';
+
+import { IDragEvent } from '@phosphor/dragdrop';
+
 import { Message } from '@phosphor/messaging';
 
 import { Widget } from '@phosphor/widgets';
@@ -18,6 +22,11 @@ const HAS_SELECTION_CLASS = 'jp-mod-has-primary-selection';
  */
 const HAS_IN_LEADING_WHITESPACE_CLASS = 'jp-mod-in-leading-whitespace';
 
+/**
+ * A class used to indicate a drop target.
+ */
+const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
+
 /**
  * RegExp to test for leading whitespace
  */
@@ -68,6 +77,35 @@ export class CodeEditorWrapper extends Widget {
     this.editor.dispose();
   }
 
+  /**
+   * 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 'p-dragenter':
+        this._evtDragEnter(event as IDragEvent);
+        break;
+      case 'p-dragleave':
+        this._evtDragLeave(event as IDragEvent);
+        break;
+      case 'p-dragover':
+        this._evtDragOver(event as IDragEvent);
+        break;
+      case 'p-drop':
+        this._evtDrop(event as IDragEvent);
+        break;
+      default:
+        break;
+    }
+  }
+
   /**
    * Handle `'activate-request'` messages.
    */
@@ -80,11 +118,27 @@ export class CodeEditorWrapper extends Widget {
    */
   protected onAfterAttach(msg: Message): void {
     super.onAfterAttach(msg);
+    let node = this.node;
+    node.addEventListener('p-dragenter', this);
+    node.addEventListener('p-dragleave', this);
+    node.addEventListener('p-dragover', this);
+    node.addEventListener('p-drop', this);
     if (this.isVisible) {
       this.update();
     }
   }
 
+  /**
+   * Handle `before-detach` messages for the widget.
+   */
+  protected onBeforeDetach(msg: Message): void {
+    let node = this.node;
+    node.removeEventListener('p-dragenter', this);
+    node.removeEventListener('p-dragleave', this);
+    node.removeEventListener('p-dragover', this);
+    node.removeEventListener('p-drop', this);
+  }
+
   /**
    * A message handler invoked on an `'after-show'` message.
    */
@@ -136,6 +190,89 @@ export class CodeEditorWrapper extends Widget {
       }
     }
   }
+
+  /**
+   * Handle the `'p-dragenter'` event for the widget.
+   */
+  private _evtDragEnter(event: IDragEvent): void {
+    if (this.editor.getOption('readOnly') === true) {
+      return;
+    }
+    const data = Private.findTextData(event.mimeData);
+    if (data === undefined) {
+      return;
+    }
+    event.preventDefault();
+    event.stopPropagation();
+    this.addClass('jp-mod-dropTarget');
+  }
+
+  /**
+   * Handle the `'p-dragleave'` event for the widget.
+   */
+  private _evtDragLeave(event: IDragEvent): void {
+    this.removeClass(DROP_TARGET_CLASS);
+    if (this.editor.getOption('readOnly') === true) {
+      return;
+    }
+    const data = Private.findTextData(event.mimeData);
+    if (data === undefined) {
+      return;
+    }
+    event.preventDefault();
+    event.stopPropagation();
+  }
+
+  /**
+   * Handle the `'p-dragover'` event for the widget.
+   */
+  private _evtDragOver(event: IDragEvent): void {
+    this.removeClass(DROP_TARGET_CLASS);
+    if (this.editor.getOption('readOnly') === true) {
+      return;
+    }
+    const data = Private.findTextData(event.mimeData);
+    if (data === undefined) {
+      return;
+    }
+    event.preventDefault();
+    event.stopPropagation();
+    event.dropAction = 'copy';
+    this.addClass(DROP_TARGET_CLASS);
+  }
+
+  /**
+   * Handle the `'p-drop'` event for the widget.
+   */
+  private _evtDrop(event: IDragEvent): void {
+    if (this.editor.getOption('readOnly') === true) {
+      return;
+    }
+    const data = Private.findTextData(event.mimeData);
+    if (data === undefined) {
+      return;
+    }
+    this.removeClass(DROP_TARGET_CLASS);
+    event.preventDefault();
+    event.stopPropagation();
+    if (event.proposedAction === 'none') {
+      event.dropAction = 'none';
+      return;
+    }
+    const coordinate = {
+      top: event.y,
+      bottom: event.y,
+      left: event.x,
+      right: event.x,
+      x: event.x,
+      y: event.y,
+      width: 0,
+      height: 0
+    };
+    const position = this.editor.getPositionForCoordinate(coordinate);
+    const offset = this.editor.getOffsetAt(position);
+    this.model.value.insert(offset, data);
+  }
 }
 
 /**
@@ -176,3 +313,20 @@ export namespace CodeEditorWrapper {
     selectionStyle?: CodeEditor.ISelectionStyle;
   }
 }
+
+/**
+ * A namespace for private functionality.
+ */
+namespace Private {
+  /**
+   * Given a MimeData instance, extract the first text data, if any.
+   */
+  export function findTextData(mime: MimeData): string | undefined {
+    const types = mime.types();
+    const textType = types.find(t => t.indexOf('text') === 0);
+    if (textType === undefined) {
+      return undefined;
+    }
+    return mime.getData(textType) as string;
+  }
+}

+ 5 - 0
packages/codeeditor/style/index.css

@@ -75,3 +75,8 @@
   border: 1px solid var(--jp-input-active-border-color);
   box-shadow: var(--jp-input-box-shadow);
 }
+
+.jp-Editor.jp-mod-dropTarget {
+  border: var(--jp-border-width) solid var(--jp-input-active-border-color);
+  box-shadow: var(--jp-input-box-shadow);
+}

+ 15 - 8
packages/notebook/src/widget.ts

@@ -1195,10 +1195,13 @@ export class Notebook extends StaticNotebook {
     node.addEventListener('dblclick', this);
     node.addEventListener('focusin', this);
     node.addEventListener('focusout', this);
-    node.addEventListener('p-dragenter', this);
-    node.addEventListener('p-dragleave', this);
-    node.addEventListener('p-dragover', this);
-    node.addEventListener('p-drop', this);
+    // Capture drag events for the notebook widget
+    // in order to preempt the drag/drop handlers in the
+    // code editor widgets, which can take text data.
+    node.addEventListener('p-dragenter', this, true);
+    node.addEventListener('p-dragleave', this, true);
+    node.addEventListener('p-dragover', this, true);
+    node.addEventListener('p-drop', this, true);
   }
 
   /**
@@ -1213,10 +1216,10 @@ export class Notebook extends StaticNotebook {
     node.removeEventListener('dblclick', this);
     node.removeEventListener('focusin', this);
     node.removeEventListener('focusout', this);
-    node.removeEventListener('p-dragenter', this);
-    node.removeEventListener('p-dragleave', this);
-    node.removeEventListener('p-dragover', this);
-    node.removeEventListener('p-drop', this);
+    node.removeEventListener('p-dragenter', this, true);
+    node.removeEventListener('p-dragleave', this, true);
+    node.removeEventListener('p-dragover', this, true);
+    node.removeEventListener('p-drop', this, true);
     document.removeEventListener('mousemove', this, true);
     document.removeEventListener('mouseup', this, true);
   }
@@ -1852,6 +1855,10 @@ export class Notebook extends StaticNotebook {
     // case where the target is in the same notebook and we
     // can just move the cells.
     this._drag.mimeData.setData('internal:cells', toMove);
+    // Add mimeData for the text content of the selected cells,
+    // allowing for drag/drop into plain text fields.
+    const textContent = toMove.map(cell => cell.model.value.text).join('\n');
+    this._drag.mimeData.setData('text/plain', textContent);
 
     // Remove mousemove and mouseup listeners and start the drag.
     document.removeEventListener('mousemove', this, true);