فهرست منبع

Merge pull request #5585 from Madhu94/console-drag-cells

Drag drop console cells into notebook
Ian Rose 6 سال پیش
والد
کامیت
6bd774aee4

+ 1 - 1
examples/notebook/package.json

@@ -12,11 +12,11 @@
     "@jupyterlab/completer": "^0.19.1",
     "@jupyterlab/docmanager": "^0.19.1",
     "@jupyterlab/docregistry": "^0.19.1",
+    "@jupyterlab/mathjax2": "^0.7.1",
     "@jupyterlab/notebook": "^0.19.2",
     "@jupyterlab/rendermime": "^0.19.1",
     "@jupyterlab/services": "^3.2.1",
     "@jupyterlab/theme-light-extension": "^0.19.1",
-    "@jupyterlab/mathjax2": "^0.7.1",
     "@phosphor/commands": "^1.6.1",
     "@phosphor/widgets": "^1.6.0",
     "es6-promise": "~4.1.1"

+ 2 - 0
packages/cells/package.json

@@ -40,9 +40,11 @@
     "@jupyterlab/outputarea": "^0.19.1",
     "@jupyterlab/rendermime": "^0.19.1",
     "@jupyterlab/services": "^3.2.1",
+    "@phosphor/algorithm": "^1.1.2",
     "@phosphor/coreutils": "^1.3.0",
     "@phosphor/messaging": "^1.2.2",
     "@phosphor/signaling": "^1.2.2",
+    "@phosphor/virtualdom": "^1.1.2",
     "@phosphor/widgets": "^1.6.0",
     "react": "~16.4.2"
   },

+ 210 - 0
packages/cells/src/celldragutils.ts

@@ -0,0 +1,210 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+/**
+ * This module contains some utility functions to operate on cells. This
+ * could be shared by widgets that contain cells, like the CodeConsole or
+ * Notebook widgets.
+ */
+
+import { each, IterableOrArrayLike } from '@phosphor/algorithm';
+import { ICodeCellModel } from './model';
+import { Cell } from './widget';
+import { h, VirtualDOM } from '@phosphor/virtualdom';
+import { nbformat } from '@jupyterlab/coreutils';
+
+/**
+ * Constants for drag
+ */
+
+/**
+ * The threshold in pixels to start a drag event.
+ */
+const DRAG_THRESHOLD = 5;
+/**
+ * The class name added to drag images.
+ */
+const DRAG_IMAGE_CLASS = 'jp-dragImage';
+
+/**
+ * The class name added to singular drag images
+ */
+const SINGLE_DRAG_IMAGE_CLASS = 'jp-dragImage-singlePrompt';
+
+/**
+ * The class name added to the drag image cell content.
+ */
+const CELL_DRAG_CONTENT_CLASS = 'jp-dragImage-content';
+
+/**
+ * The class name added to the drag image cell content.
+ */
+const CELL_DRAG_PROMPT_CLASS = 'jp-dragImage-prompt';
+
+/**
+ * The class name added to the drag image cell content.
+ */
+const CELL_DRAG_MULTIPLE_BACK = 'jp-dragImage-multipleBack';
+
+export namespace CellDragUtils {
+  export type ICellTargetArea = 'input' | 'prompt' | 'cell' | 'unknown';
+
+  /**
+   * Find the cell index containing the target html element.
+   * This function traces up the DOM hierarchy to find the root cell
+   * node. Then find the corresponding child and select it.
+   *
+   * @param node - the cell node or a child of the cell node.
+   * @param cells - an iterable of Cells
+   * @param isCellNode - a function that takes in a node and checks if
+   * it is a cell node.
+   *
+   * @returns index of the cell we're looking for. Returns -1 if
+   * the cell is not founds
+   */
+  export function findCell(
+    node: HTMLElement,
+    cells: IterableOrArrayLike<Cell>,
+    isCellNode: (node: HTMLElement) => boolean
+  ): number {
+    let cellIndex: number = -1;
+    while (node && node.parentElement) {
+      if (isCellNode(node)) {
+        each(cells, (cell, index) => {
+          if (cell.node === node) {
+            cellIndex = index;
+            return false;
+          }
+        });
+        break;
+      }
+      node = node.parentElement;
+    }
+    return cellIndex;
+  }
+
+  /**
+   * Detect which part of the cell triggered the MouseEvent
+   *
+   * @param cell - The cell which contains the MouseEvent's target
+   * @param target - The DOM node which triggered the MouseEvent
+   */
+  export function detectTargetArea(
+    cell: Cell,
+    target: HTMLElement
+  ): ICellTargetArea {
+    let targetArea: ICellTargetArea = null;
+    if (cell) {
+      if (cell.editorWidget.node.contains(target)) {
+        targetArea = 'input';
+      } else if (cell.promptNode.contains(target)) {
+        targetArea = 'prompt';
+      } else {
+        targetArea = 'cell';
+      }
+    } else {
+      targetArea = 'unknown';
+    }
+    return targetArea;
+  }
+
+  /**
+   * Detect if a drag event should be started. This is down if the
+   * mouse is moved beyond a certain distance (DRAG_THRESHOLD).
+   *
+   * @param prevX - X Coordinate of the mouse pointer during the mousedown event
+   * @param prevY - Y Coordinate of the mouse pointer during the mousedown event
+   * @param nextX - Current X Coordinate of the mouse pointer
+   * @param nextY - Current Y Coordinate of the mouse pointer
+   */
+  export function shouldStartDrag(
+    prevX: number,
+    prevY: number,
+    nextX: number,
+    nextY: number
+  ): boolean {
+    let dx = Math.abs(nextX - prevX);
+    let dy = Math.abs(nextY - prevY);
+    return dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD;
+  }
+
+  /**
+   * Create an image for the cell(s) to be dragged
+   *
+   * @param activeCell - The cell from where the drag event is triggered
+   * @param selectedCells - The cells to be dragged
+   */
+  export function createCellDragImage(
+    activeCell: Cell,
+    selectedCells: nbformat.ICell[]
+  ): HTMLElement {
+    const count = selectedCells.length;
+    let promptNumber: string;
+    if (activeCell.model.type === 'code') {
+      let executionCount = (activeCell.model as ICodeCellModel).executionCount;
+      promptNumber = ' ';
+      if (executionCount) {
+        promptNumber = executionCount.toString();
+      }
+    } else {
+      promptNumber = '';
+    }
+
+    const cellContent = activeCell.model.value.text.split('\n')[0].slice(0, 26);
+    if (count > 1) {
+      if (promptNumber !== '') {
+        return VirtualDOM.realize(
+          h.div(
+            h.div(
+              { className: DRAG_IMAGE_CLASS },
+              h.span(
+                { className: CELL_DRAG_PROMPT_CLASS },
+                '[' + promptNumber + ']:'
+              ),
+              h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)
+            ),
+            h.div({ className: CELL_DRAG_MULTIPLE_BACK }, '')
+          )
+        );
+      } else {
+        return VirtualDOM.realize(
+          h.div(
+            h.div(
+              { className: DRAG_IMAGE_CLASS },
+              h.span({ className: CELL_DRAG_PROMPT_CLASS }),
+              h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)
+            ),
+            h.div({ className: CELL_DRAG_MULTIPLE_BACK }, '')
+          )
+        );
+      }
+    } else {
+      if (promptNumber !== '') {
+        return VirtualDOM.realize(
+          h.div(
+            h.div(
+              { className: `${DRAG_IMAGE_CLASS} ${SINGLE_DRAG_IMAGE_CLASS}` },
+              h.span(
+                { className: CELL_DRAG_PROMPT_CLASS },
+                '[' + promptNumber + ']:'
+              ),
+              h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)
+            )
+          )
+        );
+      } else {
+        return VirtualDOM.realize(
+          h.div(
+            h.div(
+              { className: `${DRAG_IMAGE_CLASS} ${SINGLE_DRAG_IMAGE_CLASS}` },
+              h.span({ className: CELL_DRAG_PROMPT_CLASS }),
+              h.span({ className: CELL_DRAG_CONTENT_CLASS }, cellContent)
+            )
+          )
+        );
+      }
+    }
+  }
+}

+ 1 - 0
packages/cells/src/index.ts

@@ -5,6 +5,7 @@
 
 import '../style/index.css';
 
+export * from './celldragutils';
 export * from './collapser';
 export * from './headerfooter';
 export * from './inputarea';

+ 1 - 0
packages/console/package.json

@@ -41,6 +41,7 @@
     "@phosphor/algorithm": "^1.1.2",
     "@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"

+ 143 - 5
packages/console/src/widget.ts

@@ -5,6 +5,7 @@ import { IClientSession } from '@jupyterlab/apputils';
 
 import {
   Cell,
+  CellDragUtils,
   CellModel,
   CodeCell,
   CodeCellModel,
@@ -27,6 +28,10 @@ import { KernelMessage } from '@jupyterlab/services';
 
 import { each } from '@phosphor/algorithm';
 
+import { MimeData } from '@phosphor/coreutils';
+
+import { Drag } from '@phosphor/dragdrop';
+
 import { Message } from '@phosphor/messaging';
 
 import { ISignal, Signal } from '@phosphor/signaling';
@@ -50,6 +55,11 @@ const CODE_RUNNER = 'jpCodeRunner';
  */
 const CONSOLE_CLASS = 'jp-CodeConsole';
 
+/**
+ * The class added to console cells
+ */
+const CONSOLE_CELL_CLASS = 'jp-Console-cell';
+
 /**
  * The class name added to the console banner.
  */
@@ -75,6 +85,11 @@ const INPUT_CLASS = 'jp-CodeConsole-input';
  */
 const EXECUTION_TIMEOUT = 250;
 
+/**
+ * The mimetype used for Jupyter cell data.
+ */
+const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
+
 /**
  * A widget containing a Jupyter console.
  *
@@ -196,6 +211,7 @@ export class CodeConsole extends Widget {
    * the execution message id).
    */
   addCell(cell: CodeCell, msgId?: string) {
+    cell.addClass(CONSOLE_CELL_CLASS);
     this._content.addWidget(cell);
     this._cells.push(cell);
     if (msgId) {
@@ -373,10 +389,122 @@ export class CodeConsole extends Widget {
     return cells;
   }
 
+  /**
+   * Handle `mousedown` events for the widget.
+   */
+  private _evtMouseDown(event: MouseEvent): void {
+    const { button, shiftKey } = event;
+
+    // We only handle main or secondary button actions.
+    if (
+      !(button === 0 || button === 2) ||
+      // Shift right-click gives the browser default behavior.
+      (shiftKey && button === 2)
+    ) {
+      return;
+    }
+
+    let target = event.target as HTMLElement;
+    let cellFilter = (node: HTMLElement) =>
+      node.classList.contains(CONSOLE_CELL_CLASS);
+    let cellIndex = CellDragUtils.findCell(target, this._cells, cellFilter);
+
+    if (cellIndex === -1) {
+      // `event.target` sometimes gives an orphaned node in
+      // Firefox 57, which can have `null` anywhere in its parent line. If we fail
+      // to find a cell using `event.target`, try again using a target
+      // reconstructed from the position of the click event.
+      target = document.elementFromPoint(
+        event.clientX,
+        event.clientY
+      ) as HTMLElement;
+      cellIndex = CellDragUtils.findCell(target, this._cells, cellFilter);
+    }
+
+    if (cellIndex === -1) {
+      return;
+    }
+
+    const cell = this._cells.get(cellIndex);
+
+    let targetArea: CellDragUtils.ICellTargetArea = CellDragUtils.detectTargetArea(
+      cell,
+      event.target as HTMLElement
+    );
+
+    if (targetArea === 'prompt') {
+      this._dragData = {
+        pressX: event.clientX,
+        pressY: event.clientY,
+        index: cellIndex
+      };
+
+      this._focusedCell = cell;
+
+      document.addEventListener('mouseup', this, true);
+      document.addEventListener('mousemove', this, true);
+      event.preventDefault();
+    }
+  }
+
+  /**
+   * Handle `mousemove` event of widget
+   */
+  private _evtMouseMove(event: MouseEvent) {
+    const data = this._dragData;
+    if (
+      CellDragUtils.shouldStartDrag(
+        data.pressX,
+        data.pressY,
+        event.clientX,
+        event.clientY
+      )
+    ) {
+      this._startDrag(data.index, event.clientX, event.clientY);
+    }
+  }
+
+  /**
+   * Start a drag event
+   */
+  private _startDrag(index: number, clientX: number, clientY: number) {
+    const cellModel = this._focusedCell.model as ICodeCellModel;
+    let selected: nbformat.ICell[] = [cellModel.toJSON()];
+
+    const dragImage = CellDragUtils.createCellDragImage(
+      this._focusedCell,
+      selected
+    );
+
+    this._drag = new Drag({
+      mimeData: new MimeData(),
+      dragImage,
+      proposedAction: 'copy',
+      supportedActions: 'copy',
+      source: this
+    });
+
+    this._drag.mimeData.setData(JUPYTER_CELL_MIME, selected);
+    const textContent = cellModel.value.text;
+    this._drag.mimeData.setData('text/plain', textContent);
+
+    this._focusedCell = null;
+
+    document.removeEventListener('mousemove', this, true);
+    document.removeEventListener('mouseup', this, true);
+    this._drag.start(clientX, clientY).then(() => {
+      if (this.isDisposed) {
+        return;
+      }
+      this._drag = null;
+      this._dragData = null;
+    });
+  }
+
   /**
    * Handle the DOM events for the widget.
    *
-   * @param event - The DOM event sent to the widget.
+   * @param event -The DOM event sent to the widget.
    *
    * #### Notes
    * This method implements the DOM `EventListener` interface and is
@@ -388,8 +516,14 @@ export class CodeConsole extends Widget {
       case 'keydown':
         this._evtKeyDown(event as KeyboardEvent);
         break;
-      case 'click':
-        this._evtClick(event as MouseEvent);
+      case 'mousedown':
+        this._evtMouseDown(event as MouseEvent);
+        break;
+      case 'mousemove':
+        this._evtMouseMove(event as MouseEvent);
+        break;
+      case 'mouseup':
+        this._evtMouseUp(event as MouseEvent);
         break;
       default:
         break;
@@ -403,6 +537,7 @@ export class CodeConsole extends Widget {
     let node = this.node;
     node.addEventListener('keydown', this, true);
     node.addEventListener('click', this);
+    node.addEventListener('mousedown', this);
     // Create a prompt if necessary.
     if (!this.promptCell) {
       this.newPromptCell();
@@ -487,9 +622,9 @@ export class CodeConsole extends Widget {
   }
 
   /**
-   * Handle the `'click'` event for the widget.
+   * Handle the `'mouseup'` event for the widget.
    */
-  private _evtClick(event: MouseEvent): void {
+  private _evtMouseUp(event: MouseEvent): void {
     if (
       this.promptCell &&
       this.promptCell.node.contains(event.target as HTMLElement)
@@ -684,6 +819,9 @@ export class CodeConsole extends Widget {
   private _msgIds = new Map<string, CodeCell>();
   private _msgIdCells = new Map<CodeCell, string>();
   private _promptCellCreated = new Signal<this, CodeCell>(this);
+  private _dragData: { pressX: number; pressY: number; index: number } = null;
+  private _drag: Drag = null;
+  private _focusedCell: Cell = null;
 }
 
 /**

+ 1 - 0
packages/console/style/index.css

@@ -47,6 +47,7 @@
 .jp-CodeConsole-content .jp-Cell:not(.jp-mod-active) .jp-InputPrompt {
   opacity: var(--jp-cell-prompt-not-active-opacity);
   color: var(--jp-cell-inprompt-font-color);
+  cursor: move;
 }
 
 .jp-CodeConsole-content .jp-Cell:not(.jp-mod-active) .jp-OutputPrompt {