123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import {
- Kernel
- } from '@jupyterlab/services';
- import {
- each
- } from 'phosphor/lib/algorithm/iteration';
- import {
- DisposableSet
- } from 'phosphor/lib/core/disposable';
- import {
- CommandRegistry
- } from 'phosphor/lib/ui/commandregistry';
- import {
- Keymap
- } from 'phosphor/lib/ui/keymap';
- import {
- Menu
- } from 'phosphor/lib/ui/menu';
- import {
- Widget
- } from 'phosphor/lib/ui/widget';
- import {
- showDialog
- } from '../dialog';
- import {
- DocumentManager
- } from '../docmanager';
- import {
- createFromDialog
- } from './dialogs';
- 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._buttons.create.onmousedown = this._onCreateButtonPressed;
- this._buttons.upload.onclick = this._onUploadButtonClicked;
- this._buttons.refresh.onclick = this._onRefreshButtonClicked;
- this._input.onchange = this._onInputChanged;
- let node = this.node;
- node.appendChild(this._buttons.create);
- node.appendChild(this._buttons.upload);
- node.appendChild(this._buttons.refresh);
- this._commands = options.commands;
- this._keymap = options.keymap;
- this._manager = options.manager;
- }
- /**
- * Dispose of the resources held by the widget.
- */
- dispose(): void {
- this._buttons = null;
- this._commands = null;
- this._input = null;
- this._keymap = null;
- this._manager = null;
- this._model = null;
- super.dispose();
- }
- /**
- * Get the model used by the widget.
- */
- get model(): FileBrowserModel {
- return this._model;
- }
- /**
- * Get the document manager used by the widget.
- */
- get manager(): DocumentManager {
- return this._manager;
- }
- /**
- * 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;
- }
- /**
- * Create a file from a creator.
- *
- * @param creatorName - The name of the file creator.
- *
- * @returns A promise that resolves with the created widget.
- */
- createFrom(creatorName: string): Promise<Widget> {
- return createFromDialog(this.model, this.manager, creatorName);
- }
- /**
- * 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 keymap = this._keymap;
- let dropdown = Private.createDropdownMenu(this, commands, keymap);
- 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(() => {
- requestAnimationFrame(() => { dropdown.dispose(); });
- });
- // 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(() => {
- button.classList.remove(ACTIVE_CLASS);
- });
- // Popup the menu aligned with the bottom of the create button.
- dropdown.open(rect.left, rect.bottom, { forceX: false, forceY: false });
- };
- /**
- * 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.cd('.');
- };
- /**
- * The 'change' handler for the input field.
- */
- private _onInputChanged = () => {
- let files = Array.prototype.slice.call(this._input.files);
- Private.uploadFiles(this, files as File[]);
- };
- private _buttons = Private.createButtons();
- private _commands: CommandRegistry = null;
- private _input = Private.createUploadInput();
- private _keymap: Keymap = null;
- private _manager: DocumentManager = null;
- private _model: FileBrowserModel;
- }
- /**
- * 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;
- /**
- * The keymap for use with the file buttons.
- */
- keymap: Keymap;
- /**
- * A file browser model instance.
- */
- model: FileBrowserModel;
- /**
- * A document manager instance.
- */
- manager: DocumentManager;
- }
- }
- /**
- * The namespace for the `FileButtons` private data.
- */
- namespace Private {
- /**
- * The ID counter prefix for new commands.
- *
- * #### Notes
- * Even though the commands are disposed when the dropdown menu is disposed,
- * in order to guarantee there are no race conditions with other `FileButtons`
- * instances, each set of commands is prefixed.
- */
- let id = 0;
- /**
- * 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 folder.
- */
- export
- function createNewFolder(widget: FileButtons): void {
- widget.model.newUntitled({ type: 'directory' }).catch(error => {
- utils.showErrorMessage('New Folder Error', error);
- });
- }
- /**
- * Create a new dropdown menu for the create new button.
- */
- export
- function createDropdownMenu(widget: FileButtons, commands: CommandRegistry, keymap: Keymap): Menu {
- let menu = new Menu({ commands, keymap });
- let prefix = `file-buttons-${++id}`;
- let disposables = new DisposableSet();
- let registry = widget.manager.registry;
- let command: string;
- // Remove all the commands associated with this menu upon disposal.
- menu.disposed.connect(() => disposables.dispose());
- command = `${prefix}:new-text-folder`;
- disposables.add(commands.addCommand(command, {
- execute: () => { createNewFolder(widget); },
- label: 'Folder'
- }));
- menu.addItem({ command });
- each(registry.creators(), creator => {
- command = `${prefix}:new-${creator.name}`;
- disposables.add(commands.addCommand(command, {
- execute: () => {
- widget.createFrom(creator.name);
- },
- label: creator.name
- }));
- menu.addItem({ 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 options = {
- title: 'Overwrite File?',
- body: `"${file.name}" already exists, overwrite?`
- };
- return showDialog(options).then(button => {
- if (widget.isDisposed || button.text !== 'Ok') {
- return;
- }
- return widget.model.upload(file, true);
- });
- }
- }
|