From 3d38d4bc41bd2cd5fc4eb3492f9c70dbd8b8360b Mon Sep 17 00:00:00 2001 From: madhu94 Date: Sat, 15 Dec 2018 18:51:39 +0530 Subject: [PATCH 1/3] Allow cells from console to be dragged into notebook --- packages/cells/package.json | 2 + packages/cells/src/celldragutils.ts | 209 ++++++++++++++++++++++++++++ packages/cells/src/index.ts | 1 + packages/console/package.json | 1 + packages/console/src/widget.ts | 143 ++++++++++++++++++- packages/console/style/index.css | 1 + 6 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 packages/cells/src/celldragutils.ts diff --git a/packages/cells/package.json b/packages/cells/package.json index 39d92dab4e13..1ea9789bb6ba 100644 --- a/packages/cells/package.json +++ b/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" }, diff --git a/packages/cells/src/celldragutils.ts b/packages/cells/src/celldragutils.ts new file mode 100644 index 000000000000..9fca357a5a81 --- /dev/null +++ b/packages/cells/src/celldragutils.ts @@ -0,0 +1,209 @@ +/*----------------------------------------------------------------------------- +| 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 '../../coreutils/lib'; + + +/** + * 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. + */ + export function findCell( + node: HTMLElement, + cells: IterableOrArrayLike, + 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) + ) + ) + ); + } + } + } +} diff --git a/packages/cells/src/index.ts b/packages/cells/src/index.ts index dab1b3e03e3a..bfd0e0cb30a9 100644 --- a/packages/cells/src/index.ts +++ b/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'; diff --git a/packages/console/package.json b/packages/console/package.json index 1685f5a75246..1e69f6976441 100644 --- a/packages/console/package.json +++ b/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" diff --git a/packages/console/src/widget.ts b/packages/console/src/widget.ts index 538973c0d1da..cf82ccc94db9 100644 --- a/packages/console/src/widget.ts +++ b/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,118 @@ 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); + } + + 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._focussedCell = 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._focussedCell.model as ICodeCellModel; + let selected: nbformat.ICell[] = [cellModel.toJSON()]; + + const dragImage = CellDragUtils.createCellDragImage( + this._focussedCell, + 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._focussedCell = 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,9 +512,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); default: break; } @@ -403,6 +532,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 +617,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 +814,9 @@ export class CodeConsole extends Widget { private _msgIds = new Map(); private _msgIdCells = new Map(); private _promptCellCreated = new Signal(this); + private _dragData: { pressX: number; pressY: number; index: number } = null; + private _drag: Drag = null; + private _focussedCell: Cell = null; } /** diff --git a/packages/console/style/index.css b/packages/console/style/index.css index a44027a781d4..220a7684c07a 100644 --- a/packages/console/style/index.css +++ b/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 { From e778602ba4a79eb40c92d083090c532b84eff34e Mon Sep 17 00:00:00 2001 From: madhu94 Date: Sat, 5 Jan 2019 14:28:08 +0530 Subject: [PATCH 2/3] Fix lint issues --- packages/cells/src/celldragutils.ts | 14 ++++++-------- packages/console/src/widget.ts | 3 ++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/cells/src/celldragutils.ts b/packages/cells/src/celldragutils.ts index 9fca357a5a81..fd6e7cc1309c 100644 --- a/packages/cells/src/celldragutils.ts +++ b/packages/cells/src/celldragutils.ts @@ -15,9 +15,8 @@ import { Cell } from './widget'; import { h, VirtualDOM } from '@phosphor/virtualdom'; import { nbformat } from '../../coreutils/lib'; - /** - * Constants for drag + * Constants for drag */ /** @@ -50,7 +49,6 @@ const CELL_DRAG_PROMPT_CLASS = 'jp-dragImage-prompt'; const CELL_DRAG_MULTIPLE_BACK = 'jp-dragImage-multipleBack'; export namespace CellDragUtils { - export type ICellTargetArea = 'input' | 'prompt' | 'cell' | 'unknown'; /** @@ -86,8 +84,8 @@ export namespace CellDragUtils { /** * Detect which part of the cell triggered the MouseEvent - * - * @param cell - The cell which contains the MouseEvent's target + * + * @param cell - The cell which contains the MouseEvent's target * @param target - The DOM node which triggered the MouseEvent */ export function detectTargetArea( @@ -112,7 +110,7 @@ export namespace CellDragUtils { /** * 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 @@ -131,9 +129,9 @@ export namespace CellDragUtils { /** * 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 + * @param selectedCells - The cells to be dragged */ export function createCellDragImage( activeCell: Cell, diff --git a/packages/console/src/widget.ts b/packages/console/src/widget.ts index cf82ccc94db9..3fcd06dae6bb 100644 --- a/packages/console/src/widget.ts +++ b/packages/console/src/widget.ts @@ -389,7 +389,7 @@ export class CodeConsole extends Widget { return cells; } -/** + /** * Handle `mousedown` events for the widget. */ private _evtMouseDown(event: MouseEvent): void { @@ -520,6 +520,7 @@ export class CodeConsole extends Widget { break; case 'mouseup': this._evtMouseUp(event as MouseEvent); + break; default: break; } From a6c25041c8ac462abcc28390b580d9f6e28e3267 Mon Sep 17 00:00:00 2001 From: madhu94 Date: Sat, 12 Jan 2019 12:51:41 +0530 Subject: [PATCH 3/3] Addressing review comments --- examples/notebook/package.json | 2 +- packages/cells/src/celldragutils.ts | 5 ++++- packages/console/src/widget.ts | 14 +++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/examples/notebook/package.json b/examples/notebook/package.json index 994b21b2783d..6e1229e52e10 100644 --- a/examples/notebook/package.json +++ b/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" diff --git a/packages/cells/src/celldragutils.ts b/packages/cells/src/celldragutils.ts index fd6e7cc1309c..d67bd588df2f 100644 --- a/packages/cells/src/celldragutils.ts +++ b/packages/cells/src/celldragutils.ts @@ -13,7 +13,7 @@ import { each, IterableOrArrayLike } from '@phosphor/algorithm'; import { ICodeCellModel } from './model'; import { Cell } from './widget'; import { h, VirtualDOM } from '@phosphor/virtualdom'; -import { nbformat } from '../../coreutils/lib'; +import { nbformat } from '@jupyterlab/coreutils'; /** * Constants for drag @@ -60,6 +60,9 @@ export namespace CellDragUtils { * @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, diff --git a/packages/console/src/widget.ts b/packages/console/src/widget.ts index 3fcd06dae6bb..0e4a2c8b4bb3 100644 --- a/packages/console/src/widget.ts +++ b/packages/console/src/widget.ts @@ -421,6 +421,10 @@ export class CodeConsole extends Widget { cellIndex = CellDragUtils.findCell(target, this._cells, cellFilter); } + if (cellIndex === -1) { + return; + } + const cell = this._cells.get(cellIndex); let targetArea: CellDragUtils.ICellTargetArea = CellDragUtils.detectTargetArea( @@ -435,7 +439,7 @@ export class CodeConsole extends Widget { index: cellIndex }; - this._focussedCell = cell; + this._focusedCell = cell; document.addEventListener('mouseup', this, true); document.addEventListener('mousemove', this, true); @@ -464,11 +468,11 @@ export class CodeConsole extends Widget { * Start a drag event */ private _startDrag(index: number, clientX: number, clientY: number) { - const cellModel = this._focussedCell.model as ICodeCellModel; + const cellModel = this._focusedCell.model as ICodeCellModel; let selected: nbformat.ICell[] = [cellModel.toJSON()]; const dragImage = CellDragUtils.createCellDragImage( - this._focussedCell, + this._focusedCell, selected ); @@ -484,7 +488,7 @@ export class CodeConsole extends Widget { const textContent = cellModel.value.text; this._drag.mimeData.setData('text/plain', textContent); - this._focussedCell = null; + this._focusedCell = null; document.removeEventListener('mousemove', this, true); document.removeEventListener('mouseup', this, true); @@ -817,7 +821,7 @@ export class CodeConsole extends Widget { private _promptCellCreated = new Signal(this); private _dragData: { pressX: number; pressY: number; index: number } = null; private _drag: Drag = null; - private _focussedCell: Cell = null; + private _focusedCell: Cell = null; } /**