Browse Source

Refactor to use a toolbar and launcher from the file browser

Steven Silvester 8 years ago
parent
commit
e84a621d74

+ 25 - 2
examples/filebrowser/src/index.ts

@@ -3,6 +3,10 @@
 
 import 'es6-promise/auto';  // polyfill Promise on IE
 
+import {
+  each
+} from '@phosphor/algorithm';
+
 import {
   CommandRegistry
 } from '@phosphor/commands';
@@ -16,7 +20,7 @@ import {
 } from '@jupyterlab/services';
 
 import {
-  Dialog, showDialog
+  Dialog, ToolbarButton, showDialog
 } from '@jupyterlab/apputils';
 
 import {
@@ -106,6 +110,21 @@ function createApp(manager: ServiceManager.IManager): void {
     model: fbModel
   });
 
+  // Add a creator toolbar item.
+  let creator = new ToolbarButton({
+    className: 'jp-AddIcon',
+    onClick: () => {
+      docManager.newUntitled({
+        type: 'file',
+        path: fbModel.path
+      }).then(model => {
+        docManager.open(model.path);
+      });
+    }
+  });
+  creator.addClass('jp-MaterialIcon');
+  fbWidget.toolbar.insertItem(0, 'create', creator);
+
   let panel = new SplitPanel();
   panel.id = 'main';
   panel.addWidget(fbWidget);
@@ -129,7 +148,11 @@ function createApp(manager: ServiceManager.IManager): void {
     label: 'Open',
     icon: 'fa fa-folder-open-o',
     mnemonic: 0,
-    execute: () => { fbWidget.open(); }
+    execute: () => {
+      each(fbWidget.selectedItems(), item => {
+        docManager.openOrReveal(item.path);
+      });
+    }
   });
   commands.addCommand('file-rename', {
     label: 'Rename',

+ 1 - 1
packages/apputils/src/toolbar.ts

@@ -310,7 +310,7 @@ class ToolbarButton extends Widget {
   handleEvent(event: Event): void {
     switch (event.type) {
     case 'click':
-      if (this._onClick) {
+      if (this._onClick && (event as MouseEvent).button === 0) {
         this._onClick();
       }
       break;

+ 21 - 2
packages/filebrowser-extension/src/index.ts

@@ -6,7 +6,8 @@ import {
 } from '@jupyterlab/application';
 
 import {
-  ICommandPalette, ILayoutRestorer, IMainMenu, InstanceTracker
+  ICommandPalette, ILayoutRestorer, IMainMenu, InstanceTracker,
+  ToolbarButton
 } from '@jupyterlab/apputils';
 
 import {
@@ -25,6 +26,10 @@ import {
   FileBrowserModel, FileBrowser, IFileBrowserFactory
 } from '@jupyterlab/filebrowser';
 
+import {
+  each
+} from '@phosphor/algorithm';
+
 import {
   CommandRegistry
 } from '@phosphor/commands';
@@ -134,6 +139,17 @@ function activateFactory(app: JupyterLab, docManager: IDocumentManager, state: I
       });
       const { registry } = docManager;
 
+      // Add a launcher toolbar item.
+      let launcher = new ToolbarButton({
+        className: 'jp-AddIcon',
+        onClick: () => {
+          let cwd = widget.model.path;
+          commands.execute('launcher-jupyterlab:create', { cwd });
+        }
+      });
+      launcher.addClass('jp-MaterialIcon');
+      widget.toolbar.insertItem(0, 'launch', launcher);
+
       // Add a context menu handler to the file browser's directory listing.
       let node = widget.node.getElementsByClassName('jp-DirListing-content')[0];
       node.addEventListener('contextmenu', (event: MouseEvent) => {
@@ -275,7 +291,10 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, mai
         return;
       }
 
-      return widget.open();
+      each(widget.selectedItems(), item => {
+        let path = item.path;
+        commands.execute('file-operations:open', { path });
+      });
     },
     iconClass: 'jp-MaterialIcon jp-OpenFolderIcon',
     label: 'Open',

+ 45 - 68
packages/filebrowser/src/browser.ts

@@ -1,6 +1,10 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import {
+  Toolbar, ToolbarButton
+} from '@jupyterlab/apputils';
+
 import {
   DocumentManager
 } from '@jupyterlab/docmanager';
@@ -10,7 +14,7 @@ import {
 } from '@jupyterlab/services';
 
 import {
-  each
+  IIterator
 } from '@phosphor/algorithm';
 
 import {
@@ -21,10 +25,6 @@ import {
   PanelLayout, Widget
 } from '@phosphor/widgets';
 
-import {
-  FileButtons
-} from './buttons';
-
 import {
   BreadCrumbs
 } from './crumbs';
@@ -37,6 +37,10 @@ import {
   FileBrowserModel
 } from './model';
 
+import {
+  Uploader
+} from './upload';
+
 import {
   showErrorMessage
 } from './utils';
@@ -53,15 +57,25 @@ const FILE_BROWSER_CLASS = 'jp-FileBrowser';
 const CRUMBS_CLASS = 'jp-FileBrowser-crumbs';
 
 /**
- * The class name added to the filebrowser buttons node.
+ * The class name added to the filebrowser toolbar node.
  */
-const BUTTON_CLASS = 'jp-FileBrowser-buttons';
+const TOOLBAR_CLASS = 'jp-FileBrowser-toolbar';
 
 /**
  * The class name added to the filebrowser listing node.
  */
 const LISTING_CLASS = 'jp-FileBrowser-listing';
 
+/**
+ * The class name added to the refresh button.
+ */
+const REFRESH_BUTTON = 'jp-RefreshIcon';
+
+/**
+ * The class name added to a material icon button.
+ */
+const MATERIAL_CLASS = 'jp-MaterialIcon';
+
 
 /**
  * A widget which hosts a file browser.
@@ -82,22 +96,33 @@ class FileBrowser extends Widget {
     this.addClass(FILE_BROWSER_CLASS);
     this.id = options.id;
 
-    const commands = this._commands = options.commands;
-    const model = this._model = options.model;
+    const model = this.model = options.model;
     const renderer = options.renderer;
 
     model.connectionFailure.connect(this._onConnectionFailure, this);
     this._manager = model.manager;
     this._crumbs = new BreadCrumbs({ model });
-    this._buttons = new FileButtons({ commands, model });
+    this.toolbar = new Toolbar<Widget>();
+    let uploader = new Uploader({ model });
+    let refresher = new ToolbarButton({
+      className: REFRESH_BUTTON,
+      onClick: () => {
+        model.refresh();
+      },
+      tooltip: 'Refresh File List'
+    });
+    refresher.addClass(MATERIAL_CLASS);
+    this.toolbar.addItem('upload', uploader);
+    this.toolbar.addItem('refresher', refresher);
+
     this._listing = new DirListing({ model, renderer });
 
     this._crumbs.addClass(CRUMBS_CLASS);
-    this._buttons.addClass(BUTTON_CLASS);
+    this.toolbar.addClass(TOOLBAR_CLASS);
     this._listing.addClass(LISTING_CLASS);
 
     let layout = new PanelLayout();
-    layout.addWidget(this._buttons);
+    layout.addWidget(this.toolbar);
     layout.addWidget(this._crumbs);
     layout.addWidget(this._listing);
 
@@ -106,24 +131,19 @@ class FileBrowser extends Widget {
   }
 
   /**
-   * Get the command registry used by the file browser.
+   * The model used by the file browser.
    */
-  get commands(): CommandRegistry {
-    return this._commands;
-  }
+  readonly model: FileBrowserModel;
 
   /**
-   * Get the model used by the file browser.
+   * The toolbar used by the file browser.
    */
-  get model(): FileBrowserModel {
-    return this._model;
-  }
+  readonly toolbar: Toolbar<Widget>;
 
   /**
    * Dispose of the resources held by the file browser.
    */
   dispose() {
-    this._buttons = null;
     this._crumbs = null;
     this._listing = null;
     this._manager = null;
@@ -132,53 +152,12 @@ class FileBrowser extends Widget {
   }
 
   /**
-   * Open the currently selected item(s).
+   * Create an iterator over the listing's selected items.
    *
-   * Changes to the first directory encountered.
+   * @returns A new iterator over the listing's selected items.
    */
-  open(): void {
-    let foundDir = false;
-    each(this._model.items(), item => {
-      if (!this._listing.isSelected(item.name)) {
-        return;
-      }
-      if (item.type === 'directory') {
-        if (!foundDir) {
-          foundDir = true;
-          this._model.cd(item.name);
-        }
-      } else {
-        this.openPath(item.path);
-      }
-    });
-  }
-
-  /**
-   * Open a file by path.
-   *
-   * @param path - The path to of the file to open.
-   *
-   * @param widgetName - The name of the widget factory to use.
-   *
-   * @returns The widget for the file.
-   */
-  openPath(path: string, widgetName='default'): Widget {
-    return this._buttons.open(path, widgetName);
-  }
-
-  /**
-   * Create a new untitled file in the current directory.
-   *
-   * @param options - The options used to create the file.
-   *
-   * @returns A promise that resolves with the created widget.
-   */
-  createNew(options: Contents.ICreateOptions): Promise<Widget> {
-    return this._manager.newUntitled(options).then(contents => {
-      if (!this.isDisposed) {
-        return this._buttons.createNew(contents.path);
-      }
-    });
+  selectedItems(): IIterator<Contents.IModel> {
+    return this._listing.selectedItems();
   }
 
   /**
@@ -285,8 +264,6 @@ class FileBrowser extends Widget {
     });
   }
 
-  private _buttons: FileButtons | null = null;
-  private _commands: CommandRegistry | null = null;
   private _crumbs: BreadCrumbs | null = null;
   private _listing: DirListing | null = null;
   private _manager: DocumentManager | null = null;

+ 0 - 472
packages/filebrowser/src/buttons.ts

@@ -1,472 +0,0 @@
-// Copyright (c) Jupyter Development Team.
-// Distributed under the terms of the Modified BSD License.
-
-import {
-  Dialog, showDialog
-} from '@jupyterlab/apputils';
-
-import {
-  IDocumentManager
-} from '@jupyterlab/docmanager';
-
-import {
-  Kernel
-} from '@jupyterlab/services';
-
-import {
-  each, toArray
-} from '@phosphor/algorithm';
-
-import {
-  CommandRegistry
-} from '@phosphor/commands';
-
-import {
-  Menu, Widget
-} from '@phosphor/widgets';
-
-import {
-  FileBrowserModel
-} from './model';
-
-import * as utils
-  from './utils';
-
-
-/**
- * The class name added to a file buttons widget.
- */
-const FILE_BUTTONS_CLASS = 'jp-FileButtons';
-
-/**
- * The class name added to a button node.
- */
-const BUTTON_CLASS = 'jp-FileButtons-button';
-
-/**
- * The class name added to a button content node.
- */
-const CONTENT_CLASS = 'jp-FileButtons-buttonContent';
-
-/**
- * The class name added to a button icon node.
- */
-const ICON_CLASS = 'jp-FileButtons-buttonIcon';
-
-/**
- * The class name added to the create button.
- */
-const CREATE_CLASS = 'jp-id-create';
-
-/**
- * The class name added to the add button.
- */
-const MATERIAL_CREATE = 'jp-AddIcon';
-
-/**
- * The class name added to the upload button.
- */
-const MATERIAL_UPLOAD = 'jp-UploadIcon';
-
-/**
- * The class name added to the refresh button.
- */
-const MATERIAL_REFRESH = 'jp-RefreshIcon';
-
-/**
- * The class name added to the down caret.
- */
-const MATERIAL_DOWNCARET = 'jp-DownCaretIcon';
-
-/**
- * The class name added to a material icon button.
- */
-const MATERIAL_CLASS = 'jp-MaterialIcon';
-
-/**
- * The class name added to the upload button.
- */
-const UPLOAD_CLASS = 'jp-id-upload';
-
-/**
- * The class name added to the refresh button.
- */
-const REFRESH_CLASS = 'jp-id-refresh';
-
-/**
- * The class name added to an active create button.
- */
-const ACTIVE_CLASS = 'jp-mod-active';
-
-/**
- * The class name added to a dropdown icon.
- */
-const DROPDOWN_CLASS = 'jp-FileButtons-dropdownIcon';
-
-
-/**
- * A widget which hosts the file browser buttons.
- */
-export
-class FileButtons extends Widget {
-  /**
-   * Construct a new file browser buttons widget.
-   */
-  constructor(options: FileButtons.IOptions) {
-    super();
-    this.addClass(FILE_BUTTONS_CLASS);
-    this.model = options.model;
-    this.manager = this.model.manager;
-
-    this._buttons.create.onmousedown = this._onCreateButtonPressed.bind(this);
-    this._buttons.upload.onclick = this._onUploadButtonClicked.bind(this);
-    this._buttons.refresh.onclick = this._onRefreshButtonClicked.bind(this);
-    this._input.onchange = this._onInputChanged.bind(this);
-    this._input.onclick = this._onInputClicked.bind(this);
-
-    this.node.appendChild(this._buttons.create);
-    this.node.appendChild(this._buttons.upload);
-    this.node.appendChild(this._buttons.refresh);
-
-    this._commands = options.commands;
-  }
-
-  /**
-   * The document manager used by the widget.
-   */
-  readonly manager: IDocumentManager;
-
-  /**
-   * The underlying file browser model for the widget.
-   */
-  readonly model: FileBrowserModel;
-
-  /**
-   * Dispose of the resources held by the widget.
-   */
-  dispose(): void {
-    this._buttons = null;
-    this._commands = null;
-    this._input = null;
-    super.dispose();
-  }
-
-  /**
-   * Get the create button node.
-   */
-  get createNode(): HTMLButtonElement {
-    return this._buttons.create;
-  }
-
-  /**
-   * Get the upload button node.
-   */
-  get uploadNode(): HTMLButtonElement {
-    return this._buttons.upload;
-  }
-
-  /**
-   * Get the refresh button node.
-   */
-  get refreshNode(): HTMLButtonElement {
-    return this._buttons.refresh;
-  }
-
-  /**
-   * Open a file by path.
-   *
-   * @param path - The path of the file.
-   *
-   * @param widgetName - The name of the widget factory to use.
-   *
-   * @param kernel - The kernel model to use.
-   *
-   * @return The widget for the path.
-   */
-  open(path: string, widgetName='default', kernel?: Kernel.IModel): Widget {
-    let widget = this.manager.openOrReveal(path, widgetName, kernel);
-    return widget;
-  }
-
-  /**
-   * Create a new file by path.
-   *
-   * @param path - The path of the file.
-   *
-   * @param widgetName - The name of the widget factory to use.
-   *
-   * @param kernel - The kernel model to use.
-   *
-   * @return The widget for the path.
-   */
-  createNew(path: string, widgetName='default', kernel?: Kernel.IModel): Widget {
-    return this.manager.createNew(path, widgetName, kernel);
-  }
-
-  /**
-   * The 'mousedown' handler for the create button.
-   */
-  private _onCreateButtonPressed(event: MouseEvent) {
-    // Do nothing if nothing if it's not a left press.
-    if (event.button !== 0) {
-      return;
-    }
-
-    // Do nothing if the create button is already active.
-    let button = this._buttons.create;
-    if (button.classList.contains(ACTIVE_CLASS)) {
-      return;
-    }
-
-    // Create a new dropdown menu and snap the button geometry.
-    let commands = this._commands;
-    let dropdown = Private.createDropdownMenu(this, commands);
-    let rect = button.getBoundingClientRect();
-
-    // Mark the button as active.
-    button.classList.add(ACTIVE_CLASS);
-
-    // Setup the `aboutToClose` signal handler. The menu is disposed on an
-    // animation frame to allow a mouse press event which closed the
-    // menu to run its course. This keeps the button from re-opening.
-    dropdown.aboutToClose.connect(this._onDropDownAboutToClose, this);
-
-    // Setup the `disposed` signal handler. This restores the button
-    // to the non-active state and allows a new menu to be opened.
-    dropdown.disposed.connect(this._onDropDownDisposed, this);
-
-    // Popup the menu aligned with the bottom of the create button.
-    dropdown.open(rect.left, rect.bottom, { forceX: false, forceY: false });
-  };
-
-  /**
-   * Handle a dropdwon about to close.
-   */
-  private _onDropDownAboutToClose(sender: Menu): void {
-    requestAnimationFrame(() => { sender.dispose(); });
-  }
-
-  /**
-   * Handle a dropdown disposal.
-   */
-  private _onDropDownDisposed(sender: Menu): void {
-    this._buttons.create.classList.remove(ACTIVE_CLASS);
-  }
-
-  /**
-   * The 'click' handler for the upload button.
-   */
-  private _onUploadButtonClicked(event: MouseEvent) {
-    if (event.button !== 0) {
-      return;
-    }
-    this._input.click();
-  }
-
-  /**
-   * The 'click' handler for the refresh button.
-   */
-  private _onRefreshButtonClicked(event: MouseEvent) {
-    if (event.button !== 0) {
-      return;
-    }
-    // Force a refresh of the current directory.
-    this.model.refresh();
-  }
-
-  /**
-   * The 'change' handler for the input field.
-   */
-  private _onInputChanged(): void {
-    let files = Array.prototype.slice.call(this._input.files);
-    Private.uploadFiles(this, files as File[]);
-  }
-
-  /**
-   * The 'click' handler for the input field.
-  */
-  private _onInputClicked(): void {
-    // In order to allow repeated uploads of the same file (with delete in between),
-    // we need to null out the input value to trigger a change event.
-    this._input.value = null;
-  }
-
-  private _buttons = Private.createButtons();
-  private _commands: CommandRegistry = null;
-  private _input = Private.createUploadInput();
-}
-
-
-/**
- * The namespace for the `FileButtons` class statics.
- */
-export
-namespace FileButtons {
-  /**
-   * An options object for initializing a file buttons widget.
-   */
-  export
-  interface IOptions {
-    /**
-     * The command registry for use with the file buttons.
-     */
-    commands: CommandRegistry;
-
-    /**
-     * A file browser model instance.
-     */
-    model: FileBrowserModel;
-  }
-}
-
-
-/**
- * The namespace for the `FileButtons` private data.
- */
-namespace Private {
-  /**
-   * An object which holds the button nodes for a file buttons widget.
-   */
-  export
-  interface IButtonGroup {
-    create: HTMLButtonElement;
-    upload: HTMLButtonElement;
-    refresh: HTMLButtonElement;
-  }
-
-  /**
-   * Create the button group for a file buttons widget.
-   */
-  export
-  function createButtons(): IButtonGroup {
-    let create = document.createElement('button');
-    let upload = document.createElement('button');
-    let refresh = document.createElement('button');
-
-    let createContent = document.createElement('span');
-    let uploadContent = document.createElement('span');
-    let refreshContent = document.createElement('span');
-
-    let createIcon = document.createElement('span');
-    let uploadIcon = document.createElement('span');
-    let refreshIcon = document.createElement('span');
-    let dropdownIcon = document.createElement('span');
-
-    create.type = 'button';
-    upload.type = 'button';
-    refresh.type = 'button';
-
-    create.title = 'Create New...';
-    upload.title = 'Upload File(s)';
-    refresh.title = 'Refresh File List';
-
-    create.className = `${BUTTON_CLASS} ${CREATE_CLASS}`;
-    upload.className = `${BUTTON_CLASS} ${UPLOAD_CLASS}`;
-    refresh.className = `${BUTTON_CLASS} ${REFRESH_CLASS}`;
-
-    createContent.className = CONTENT_CLASS;
-    uploadContent.className = CONTENT_CLASS;
-    refreshContent.className = CONTENT_CLASS;
-
-    // TODO make these icons configurable.
-    createIcon.className = ICON_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_CREATE;
-    uploadIcon.className = ICON_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_UPLOAD;
-    refreshIcon.className = ICON_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_REFRESH;
-    dropdownIcon.className = DROPDOWN_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_DOWNCARET;
-
-    createContent.appendChild(createIcon);
-    createContent.appendChild(dropdownIcon);
-    uploadContent.appendChild(uploadIcon);
-    refreshContent.appendChild(refreshIcon);
-
-    create.appendChild(createContent);
-    upload.appendChild(uploadContent);
-    refresh.appendChild(refreshContent);
-
-    return { create, upload, refresh };
-  }
-
-  /**
-   * Create the upload input node for a file buttons widget.
-   */
-  export
-  function createUploadInput(): HTMLInputElement {
-    let input = document.createElement('input');
-    input.type = 'file';
-    input.multiple = true;
-    return input;
-  }
-
-
-  /**
-   * Create a new dropdown menu for the create new button.
-   */
-  export
-  function createDropdownMenu(widget: FileButtons, commands: CommandRegistry): Menu {
-    const menu = new Menu({ commands });
-
-    // Add new folder menu item.
-    menu.addItem({
-      args: {
-        error: 'New Folder Error',
-        label: 'Folder',
-        path: widget.model.path,
-        type: 'directory'
-      },
-      command: 'file-operations:new-untitled'
-    });
-
-    const { registry } = widget.manager;
-    const items = toArray(widget.model.items()).map(item => item.path);
-    const path = widget.model.path;
-    each(registry.creators(), creator => {
-      const command = 'file-operations:create-from';
-      const creatorName = creator.name;
-      const args = { creatorName, items, path };
-      menu.addItem({ args, command });
-    });
-    return menu;
-  }
-
-  /**
-   * Upload an array of files to the server.
-   */
-  export
-  function uploadFiles(widget: FileButtons, files: File[]): void {
-    let pending = files.map(file => uploadFile(widget, file));
-    Promise.all(pending).catch(error => {
-      utils.showErrorMessage('Upload Error', error);
-    });
-  }
-
-  /**
-   * Upload a file to the server.
-   */
-  function uploadFile(widget: FileButtons, file: File): Promise<any> {
-    return widget.model.upload(file).catch(error => {
-      let exists = error.message.indexOf('already exists') !== -1;
-      if (exists) {
-        return uploadFileOverride(widget, file);
-      }
-      throw error;
-    });
-  }
-
-  /**
-   * Upload a file to the server checking for override.
-   */
-  function uploadFileOverride(widget: FileButtons, file: File): Promise<any> {
-    let overwrite = Dialog.warnButton({ label: 'OVERWRITE' });
-    let options = {
-      title: 'Overwrite File?',
-      body: `"${file.name}" already exists, overwrite?`,
-      buttons: [Dialog.cancelButton(), overwrite]
-    };
-    return showDialog(options).then(button => {
-      if (widget.isDisposed || button.accept) {
-        return;
-      }
-      return widget.model.upload(file, true);
-    });
-  }
-}

+ 1 - 1
packages/filebrowser/src/index.ts

@@ -4,8 +4,8 @@
 import '../style/index.css';
 
 export * from './browser';
-export * from './buttons';
 export * from './crumbs';
 export * from './factory';
 export * from './listing';
 export * from './model';
+export * from './upload';

+ 16 - 13
packages/filebrowser/src/listing.ts

@@ -288,6 +288,16 @@ class DirListing extends Widget {
     return this._sortState;
   }
 
+  /**
+   * Create an iterator over the listing's selected items.
+   *
+   * @returns A new iterator over the listing's selected items.
+   */
+  selectedItems(): IIterator<Contents.IModel> {
+    let items = this._sortedItems;
+    return filter(items, item => this._selection[item.name]);
+  }
+
   /**
    * Create an iterator over the listing's sorted items.
    *
@@ -405,11 +415,11 @@ class DirListing extends Widget {
     const basePath = this._model.path;
     let promises: Promise<Contents.IModel>[] = [];
 
-    for (let item of this._getSelectedItems()) {
+    each(this.selectedItems(), item => {
       if (item.type !== 'directory') {
         promises.push(this._model.manager.copy(item.name, '.', basePath));
       }
-    }
+    });
     return Promise.all(promises).catch(error => {
       utils.showErrorMessage('Duplicate file', error);
     });
@@ -419,11 +429,11 @@ class DirListing extends Widget {
    * Download the currently selected item(s).
    */
   download(): void {
-    for (let item of this._getSelectedItems()) {
+    each(this.selectedItems(), item => {
       if (item.type !== 'directory') {
         this._model.download(item.path);
       }
-    }
+    });
   }
 
   /**
@@ -1209,25 +1219,18 @@ class DirListing extends Widget {
     }
   }
 
-  /**
-   * Get the currently selected items.
-   */
-  private _getSelectedItems(): Contents.IModel[] {
-    let items = this._sortedItems;
-    return toArray(filter(items, item => this._selection[item.name]));
-  }
 
   /**
    * Copy the selected items, and optionally cut as well.
    */
   private _copy(): void {
     this._clipboard.length = 0;
-    for (let item of this._getSelectedItems()) {
+    each(this.selectedItems(), item => {
       if (item.type !== 'directory') {
         // Store the absolute path of the item.
         this._clipboard.push('/' + item.path);
       }
-    }
+    });
     this.update();
   }
 

+ 161 - 0
packages/filebrowser/src/upload.ts

@@ -0,0 +1,161 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  Dialog, ToolbarButton, showDialog
+} from '@jupyterlab/apputils';
+
+import {
+  FileBrowserModel
+} from './model';
+
+import * as utils
+  from './utils';
+
+
+/**
+ * The class name added to a button content node.
+ */
+const CONTENT_CLASS = 'jp-FileButtons-buttonContent';
+
+/**
+ * The class name added to a button icon node.
+ */
+const ICON_CLASS = 'jp-FileButtons-buttonIcon';
+
+/**
+ * The class name added to the upload button.
+ */
+const MATERIAL_UPLOAD = 'jp-UploadIcon';
+
+/**
+ * The class name added to a material icon button.
+ */
+const MATERIAL_CLASS = 'jp-MaterialIcon';
+
+/**
+ * The class name added to the upload button.
+ */
+const UPLOAD_CLASS = 'jp-id-upload';
+
+
+/**
+ * A widget which provides an upload button.
+ */
+export
+class Uploader extends ToolbarButton {
+  /**
+   * Construct a new file browser buttons widget.
+   */
+  constructor(options: Uploader.IOptions) {
+    super({
+      className: UPLOAD_CLASS,
+      onClick: () => {
+        this._input.click();
+      },
+      tooltip: 'Upload File(s)'
+    });
+    let uploadContent = document.createElement('span');
+    let uploadIcon = document.createElement('span');
+    uploadContent.className = CONTENT_CLASS;
+    uploadIcon.className = ICON_CLASS + ' ' + MATERIAL_CLASS + ' ' + MATERIAL_UPLOAD;
+    uploadContent.appendChild(uploadIcon);
+    this.node.appendChild(uploadContent);
+    this.model = options.model;
+    this._input.onclick = this._onInputClicked.bind(this);
+    this._input.onchange = this._onInputChanged.bind(this);
+  }
+
+  /**
+   * The underlying file browser model for the widget.
+   */
+  readonly model: FileBrowserModel;
+
+  /**
+   * The 'change' handler for the input field.
+   */
+  private _onInputChanged(): void {
+    let files = Array.prototype.slice.call(this._input.files) as File[];
+    let pending = files.map(file => this._uploadFile(file));
+    Promise.all(pending).catch(error => {
+      utils.showErrorMessage('Upload Error', error);
+    });
+  }
+
+  /**
+   * The 'click' handler for the input field.
+   */
+  private _onInputClicked(): void {
+    // In order to allow repeated uploads of the same file (with delete in between),
+    // we need to null out the input value to trigger a change event.
+    this._input.value = null;
+  }
+
+  /**
+   * Upload a file to the server.
+   */
+  private _uploadFile(file: File): Promise<any> {
+    return this.model.upload(file).catch(error => {
+      let exists = error.message.indexOf('already exists') !== -1;
+      if (exists) {
+        return this._uploadFileOverride(file);
+      }
+      throw error;
+    });
+  }
+
+  /**
+   * Upload a file to the server checking for override.
+   */
+  private _uploadFileOverride(file: File): Promise<any> {
+    let overwrite = Dialog.warnButton({ label: 'OVERWRITE' });
+    let options = {
+      title: 'Overwrite File?',
+      body: `"${file.name}" already exists, overwrite?`,
+      buttons: [Dialog.cancelButton(), overwrite]
+    };
+    return showDialog(options).then(button => {
+      if (this.isDisposed || button.accept) {
+        return;
+      }
+      return this.model.upload(file, true);
+    });
+  }
+
+  private _input = Private.createUploadInput();
+}
+
+
+/**
+ * The namespace for Uploader class statics.
+ */
+export
+namespace Uploader {
+  /**
+   * The options used to create an uploader.
+   */
+  export
+  interface IOptions {
+    /**
+     * A file browser model instance.
+     */
+    model: FileBrowserModel;
+  }
+}
+
+
+/**
+ * The namespace for module private data.
+ */
+namespace Private {
+  /**
+   * Create the upload input node for a file buttons widget.
+   */
+  export
+  function createUploadInput(): HTMLInputElement {
+    let input = document.createElement('input');
+    input.type = 'file';
+    input.multiple = true;
+    return input;
+  }
+}

+ 1 - 40
packages/filebrowser/style/index.css

@@ -55,56 +55,17 @@
 |----------------------------------------------------------------------------*/
 
 
-.jp-FileButtons {
-  flex: 0 0 auto;
-  display: flex;
-  flex-direction: row;
-  border-bottom: none;
-}
-
-
-.jp-FileButtons-buttonContent {
-  display: flex;
-  flex-direction: row;
-  align-items: baseline;
-}
-
-
-.jp-FileButtons-button {
+.jp-FileBrowser-toolbar > .jp-Toolbar-button {
   flex: 1 1 auto;
   height: var(--jp-private-filebrowser-button-height);
   line-height: var(--jp-private-filebrowser-button-height);
   color: var(--jp-ui-font-color1);
-  background: var(--jp-layout-color1);
   border: none;
   font-size: var(--jp-ui-icon-font-size);
   outline: 0;
 }
 
 
-.jp-FileButtons-buttonIcon {
-  margin-left: auto;
-  margin-right: auto;
-}
-
-
-.jp-FileButtons-button::-moz-focus-inner {
-  border: 0;
-}
-
-
-.jp-FileButtons-button:hover {
-  background: var(--jp-layout-color2);
-}
-
-
-.jp-FileButtons-button:active,
-.jp-FileButtons-button.jp-id-create.jp-mod-active {
-  background: var(--jp-layout-color3);
-  border-color: var(--jp-border-color3);
-}
-
-
 /*-----------------------------------------------------------------------------
 | DirListing
 |----------------------------------------------------------------------------*/

+ 2 - 1
packages/launcher/src/index.ts

@@ -261,7 +261,8 @@ class LauncherWidget extends VDomRenderer<LauncherModel> {
       };
       let imageClass = `${item.iconClass} ${IMAGE_CLASS}`;
       let icon = h.div({ className: imageClass, onclick }, item.iconLabel);
-      let text = h.span({className: TEXT_CLASS, onclick }, item.displayName);
+      let title = item.displayName;
+      let text = h.span({className: TEXT_CLASS, onclick, title }, title);
       let category = h.span({className: TEXT_CLASS, onclick }, item.category);
       return h.div({
         className: ITEM_CLASS,