From 9dfe9dc18212b7a7742b519d4118081e4ca6a4c5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 14 Dec 2015 12:24:18 -0600 Subject: [PATCH 1/2] wip implement dialog wip implement dialog wip implement dialog Remove extraneous files Remove extra css Pin the beta packages for now wip implementation Remove extra css Remove extra css Finish initial dialog implementation --- example/index.css | 28 ++++ lib/index.css | 1 + lib/index.js | 32 +++-- package.json | 4 +- src/dialog.css | 51 +++++++ src/dialog.ts | 328 ++++++++++++++++++++++++++++++++++++++++++++++ src/index.css | 1 + src/index.ts | 18 +++ 8 files changed, 453 insertions(+), 10 deletions(-) create mode 100644 src/dialog.css create mode 100644 src/dialog.ts diff --git a/example/index.css b/example/index.css index c2a9295..ef812e8 100644 --- a/example/index.css +++ b/example/index.css @@ -10,6 +10,34 @@ } +.jp-Dialog { + background: rgba(33, 150, 243, .5); +} + + +.jp-Dialog-content { + background-color: #F5F5F5; + min-width: 50%; + border: 1px solid #757575; + border-radius: 5px; +} + + +.jp-Dialog-header { + border-bottom: 1px solid #757575; +} + + +.jp-Dialog-button { + border: 1px solid #757575; + font-size: 16px; + background-color: #FAFAFA; + line-height: 20px; + border-radius: 3px; + max-width: 100px; +} + + .jp-FileBrowser-button-item { background-color: #FAFAFA; font-size: 16px; diff --git a/lib/index.css b/lib/index.css index 7244205..57281e2 100644 --- a/lib/index.css +++ b/lib/index.css @@ -4,6 +4,7 @@ |----------------------------------------------------------------------------*/ .jp-FileBrowser { overflow: auto; + z-index: 0; } diff --git a/lib/index.js b/lib/index.js index 8df3896..8b25efa 100644 --- a/lib/index.js +++ b/lib/index.js @@ -12,6 +12,7 @@ var phosphor_domutil_1 = require('phosphor-domutil'); var phosphor_menus_1 = require('phosphor-menus'); var phosphor_signaling_1 = require('phosphor-signaling'); var phosphor_widget_1 = require('phosphor-widget'); +var dialog_1 = require('./dialog'); require('./index.css'); /** * The class name added to FileBrowser instances. @@ -207,6 +208,7 @@ var FileBrowserViewModel = (function () { return Promise.reject(new Error(file.name + " already exists")); } } + // Gather the file model parameters. var path = this._path ? this._path + '/' + file.name : file.name; var name = file.name; var isNotebook = file.name.indexOf('.ipynb') !== -1; @@ -221,8 +223,8 @@ var FileBrowserViewModel = (function () { reader.readAsArrayBuffer(file); } return new Promise(function (resolve, reject) { - var content = ''; reader.onload = function (event) { + var content = ''; if (isNotebook) { content = JSON.parse(reader.result); } @@ -305,6 +307,20 @@ var FileBrowser = (function (_super) { this._buttons = createButtons(buttons); var input = this._buttons[Button.Upload].lastChild; input.onchange = this._handleUploadEvent.bind(this); + this._buttons[Button.Refresh].onclick = function () { + // Show a dialog here + var test = document.createElement('textarea'); + test.value = 'Input some text'; + dialog_1.Dialog.showDialog('Test', _this.node, test).then(function (button) { + if (button) { + console.log(button.text); + } + else { + console.log('Escape!'); + } + console.log(test.value); + }); + }; // Create the "new" menu. var command = new phosphor_command_1.DelegateCommand(function (args) { _this._handleNewCommand(args); @@ -557,10 +573,10 @@ var FileBrowser = (function (_super) { nearestIndex = 0; } // Select the rows between the current and the nearest selected. - for (var i_1 = 0; i_1 < nodes.length; i_1++) { - if (nearestIndex >= i_1 && index <= i_1 || - nearestIndex <= i_1 && index >= i_1) { - nodes[i_1].classList.add(SELECTED_CLASS); + for (var i = 0; i < nodes.length; i++) { + if (nearestIndex >= i && index <= i || + nearestIndex <= i && index >= i) { + nodes[i].classList.add(SELECTED_CLASS); } } } @@ -573,9 +589,9 @@ var FileBrowser = (function (_super) { } // Set the selected items on the model. var selected = []; - for (var i_2 = 0; i_2 < nodes.length; i_2++) { - if (nodes[i_2].classList.contains(SELECTED_CLASS)) { - selected.push(i_2); + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].classList.contains(SELECTED_CLASS)) { + selected.push(i); } } this._model.selected = selected; diff --git a/package.json b/package.json index c624dca..ce25d62 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "moment": "^2.10.6", "phosphor-command": "^0.5.0", "phosphor-domutil": "^1.2.0", - "phosphor-menus": "^1.0.0-beta.1", + "phosphor-menus": "1.0.0-beta.1", "phosphor-messaging": "^1.0.5", "phosphor-nodewrapper": "^1.0.4", - "phosphor-widget": "^1.0.0-beta.1" + "phosphor-widget": "1.0.0-beta.1" }, "devDependencies": { "expect.js": "^0.3.1", diff --git a/src/dialog.css b/src/dialog.css new file mode 100644 index 0000000..2c14475 --- /dev/null +++ b/src/dialog.css @@ -0,0 +1,51 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ +.jp-Dialog { + position: absolute; + z-index: 10000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 0; + padding-left: 3px; +} + + +.jp-Dialog-content { + text-align: center; + vertical-align: middle; +} + + +.jp-Dialog-header { + padding: 5px; +} + + +.jp-Dialog-body { + text-align: center; + vertical-align: middle; + padding: 5px; +} + + +.jp-Dialog-footer { + list-style-type: none; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin: 0; + padding: 5px; +} + + +.jp-Dialog-button { + flex: 1 1 auto; + margin: 0; + margin-left: 5px; + padding: 5px; +} diff --git a/src/dialog.ts b/src/dialog.ts new file mode 100644 index 0000000..de1148e --- /dev/null +++ b/src/dialog.ts @@ -0,0 +1,328 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +'use strict'; + +import { + ICommand +} from 'phosphor-command'; + +import { + hitTest +} from 'phosphor-domutil'; + +import { + Message +} from 'phosphor-messaging'; + +import { + Widget +} from 'phosphor-widget'; + +import './index.css'; + +import './dialog.css'; + + +/** + * The class name added to Dialog instances. + */ +const DIALOG_CLASS = 'jp-Dialog'; + +/** + * The class name added to Dialog content node. + */ +const CONTENT_CLASS = 'jp-Dialog-content'; + +/** + * The class name added to Dialog header node. + */ +const HEADER_CLASS = 'jp-Dialog-header'; + +/** + * The class name added to Dialog body node. + */ +const BODY_CLASS = 'jp-Dialog-body'; + +/** + * The class name added to Dialog content node. + */ +const FOOTER_CLASS = 'jp-Dialog-footer'; + +/** + * The class name added to Dialog button nodes. + */ +const BUTTON_CLASS = 'jp-Dialog-button'; + +/** + * The class name added to Dialog OK buttons. + */ +const OK_BUTTON_CLASS = 'jp-Dialog-ok-button'; + +/** + * The class name added to Dialog Cancel buttons. + */ +const CANCEL_BUTTON_CLASS = 'jp-Dialog-cancel-button'; + + +/** + * A button applied to a dialog. + */ +export +interface IButtonItem { + /** + * The text for the button. + */ + text: string; + + /** + * The icon class for the button. + */ + icon?: string; + + /** + * The extra class name to associate with the button. + */ + className?: string; + + /** + * The command for the button. + */ + command?: ICommand; + + /** + * The args object for the button command. + */ + commandArgs?: any; +} + + +/** + * Define a default "OK" button. + */ +export +const okButton: IButtonItem = { + text: 'OK', + className: 'jp-Dialog-ok-button' +} + + +/** + * Define a default "Cancel" button. + */ +export +const cancelButton: IButtonItem = { + text: 'Cancel', + className: 'jp-Dialog-cancel-button' +} + + +/** + * An implementation of a modal dialog. + */ +export +class Dialog extends Widget { + /** + * Create the DOM node for dialog. + */ + static createNode(): HTMLElement { + let node = document.createElement('div'); + let content = document.createElement('div'); + let header = document.createElement('div'); + let body = document.createElement('div'); + let footer = document.createElement('ul'); + content.className = CONTENT_CLASS; + header.className = HEADER_CLASS; + body.className = BODY_CLASS; + footer.className = FOOTER_CLASS; + node.appendChild(content); + content.appendChild(header); + content.appendChild(body); + content.appendChild(footer); + return node; + } + + /** + * Create a dialog and show it. + */ + static showDialog(title: string, host: HTMLElement, body: HTMLElement, buttons?: IButtonItem[]): Promise{ + let dialog = new Dialog(title, host, body, buttons); + return dialog.show(); + } + + /** + * Construct a new dialog. + */ + constructor(title: string, host: HTMLElement, body: HTMLElement, buttons?: IButtonItem[]) { + super(); + this.addClass(DIALOG_CLASS); + this._title = title; + this._host = host; + this._body = body; + this._buttons = buttons || [okButton, cancelButton]; + } + + /** + * Show the dialog over the host node. + * + * @returns The button item that was selected or `null` if the dialog was Escaped. + */ + show(): Promise { + // Set up the geometry of the dialog. + let rect = this._host.getBoundingClientRect(); + this.node.style.left = rect.left + 'px'; + this.node.style.top = rect.top + 'px'; + this.node.style.width = rect.width + 'px'; + this.node.style.height = rect.height + 'px'; + + // Add the dialog contents and attach to the document. + let header = this.node.getElementsByClassName(HEADER_CLASS)[0]; + header.textContent = this._title; + let body = this.node.getElementsByClassName(BODY_CLASS)[0]; + body.appendChild(this._body); + let footer = this.node.getElementsByClassName(FOOTER_CLASS)[0]; + this._buttonNodes = []; + for (let item of this._buttons) { + let button = createButton(item); + button.tabIndex = this._buttonNodes.length; + this._buttonNodes.push(button); + footer.appendChild(button); + } + Widget.attach(this, document.body); + + // Return a promise to be resolved when the dialog is closed. + return new Promise((resolve, reject) => { + this._resolver = resolve; + }); + } + + /** + * Handle the DOM events for the dialog. + * + * @param event - The DOM event sent to the panel. + * + * #### Notes + * This method implements the DOM `EventListener` interface and is + * called in response to events on the panel's DOM node. It should + * not be called directly by user code. + */ + handleEvent(event: Event): void { + switch (event.type) { + case 'click': + this._evtClick(event as MouseEvent); + break; + case 'keydown': + this._evtKeyDown(event as KeyboardEvent); + break; + } + } + + /** + * A message handler invoked on an `'after-attach'` message. + */ + protected onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + this.node.addEventListener('click', this); + document.addEventListener('keydown', this, true); + } + + /** + * A message handler invoked on a `'before-detach'` message. + */ + protected onBeforeDetach(msg: Message): void { + super.onBeforeDetach(msg); + this.node.removeEventListener('click', this); + document.removeEventListener('keydown', this, true); + } + + /** + * Handle the `'keydown'` event for the dialog. + */ + private _evtKeyDown(event: KeyboardEvent): void { + var index = this._buttonNodes.indexOf(event.target as HTMLElement); + if (index === -1 && event.target !== document.body) { + return; + } + event.stopPropagation(); + switch (event.keyCode) { + case 9: // Tab + event.preventDefault(); + if (index === -1) { + this._buttonNodes[0].focus(); + } else { + index = (index + 1) % this._buttonNodes.length; + this._buttonNodes[index].focus(); + } + break; + case 13: // Enter + event.preventDefault(); + if (index !== -1) this._handleSelect(this._buttons[index]); + break; + case 27: // Escape + Widget.detach(this); + event.preventDefault(); + this._resolver(void 0); + break; + } + } + + /** + * Handle the `'click'` event for the dialog. + */ + private _evtClick(event: MouseEvent): void { + // Do nothing if it's not a left mouse press. + if (event.button !== 0) { + return; + } + + // Check for a click on a button. + let index = hitTestNodes(this._buttonNodes, event.clientX, event.clientY); + if (index !== -1) { + // Stop the event propagation. + event.preventDefault(); + event.stopPropagation(); + this._handleSelect(this._buttons[index]); + } + } + + /** + * Handle a selection of one of the button nodes. + */ + private _handleSelect(item: IButtonItem): void { + Widget.detach(this); + if (item.command && item.command.isEnabled) { + item.command.execute(item.commandArgs); + } + this._resolver(item); + } + + private _title = ''; + private _host: HTMLElement = null; + private _body: HTMLElement = null; + private _buttons: IButtonItem[] = []; + private _resolver: (value: IButtonItem) => void = null; + private _buttonNodes: HTMLElement[] = []; +} + + +/** + * Create a node for a button item. + */ +function createButton(item: IButtonItem): HTMLElement { + let el = document.createElement('li'); + el.textContent = item.text; + el.className = BUTTON_CLASS; + if (item.icon) el.classList.add(item.icon); + if (item.className) el.classList.add(item.className); + return el; +} + + +/** + * Get the index of the node at a client position, or `-1`. + */ +function hitTestNodes(nodes: HTMLElement[], x: number, y: number): number { + for (let i = 0, n = nodes.length; i < n; ++i) { + if (hitTest(nodes[i], x, y)) return i; + } + return -1; +} diff --git a/src/index.css b/src/index.css index 7244205..57281e2 100644 --- a/src/index.css +++ b/src/index.css @@ -4,6 +4,7 @@ |----------------------------------------------------------------------------*/ .jp-FileBrowser { overflow: auto; + z-index: 0; } diff --git a/src/index.ts b/src/index.ts index b03e177..9772c9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,10 @@ import { Widget } from 'phosphor-widget'; +import { + Dialog +} from './dialog'; + import './index.css'; @@ -382,6 +386,20 @@ class FileBrowser extends Widget { let input = this._buttons[Button.Upload].lastChild; (input as HTMLElement).onchange = this._handleUploadEvent.bind(this); + this._buttons[Button.Refresh].onclick = () => { + // Show a dialog here + let test = document.createElement('textarea'); + test.value = 'Input some text'; + Dialog.showDialog('Test', this.node, test).then(button => { + if (button) { + console.log(button.text); + } else { + console.log('Escape!'); + } + console.log(test.value); + }); + } + // Create the "new" menu. let command = new DelegateCommand((args: string) => { this._handleNewCommand(args); From e4103401a7766954c64a1d3da97e64805f768472 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 15 Dec 2015 13:46:35 -0600 Subject: [PATCH 2/2] Use phosphor-dialog to create dialogs for upload --- example/index.css | 90 +++++++----- example/index.js | 6 +- example/index.ts | 5 +- example/package.json | 2 +- lib/index.d.ts | 2 +- lib/index.js | 51 ++++--- package.json | 3 +- src/dialog.css | 51 ------- src/dialog.ts | 328 ------------------------------------------- src/index.ts | 57 ++++---- 10 files changed, 128 insertions(+), 467 deletions(-) delete mode 100644 src/dialog.css delete mode 100644 src/dialog.ts diff --git a/example/index.css b/example/index.css index ef812e8..0553fa6 100644 --- a/example/index.css +++ b/example/index.css @@ -10,34 +10,6 @@ } -.jp-Dialog { - background: rgba(33, 150, 243, .5); -} - - -.jp-Dialog-content { - background-color: #F5F5F5; - min-width: 50%; - border: 1px solid #757575; - border-radius: 5px; -} - - -.jp-Dialog-header { - border-bottom: 1px solid #757575; -} - - -.jp-Dialog-button { - border: 1px solid #757575; - font-size: 16px; - background-color: #FAFAFA; - line-height: 20px; - border-radius: 3px; - max-width: 100px; -} - - .jp-FileBrowser-button-item { background-color: #FAFAFA; font-size: 16px; @@ -64,12 +36,6 @@ } - -.jp-EditorWidget { - min-height: 200px; -} - - .jp-FileBrowser-row.jp-mod-selected { background-color: #2196F3; color: #F5F5F5; @@ -91,6 +57,62 @@ } +.jp-EditorWidget { + min-height: 200px; +} + + +.p-Dialog { + padding-left: 3px; + background: rgba(245, 245, 245, .6); +} + + +.p-Dialog-content { + background-color: #F5F5F5; + min-width: 50%; + border: 1px solid #757575; + border-radius: 5px; +} + + +.p-Dialog-header { + padding: 5px; + border-bottom: 1px solid #757575; +} + + +.p-Dialog-body { + padding: 5px; +} + + +.p-Dialog-footer { + padding: 5px; +} + + +.p-Dialog-button { + margin-left: 5px; + padding: 5px; + border: 1px solid #757575; + font-size: 16px; + background-color: #FAFAFA; + line-height: 20px; + border-radius: 3px; + max-width: 100px; +} + +.p-Dialog-close { + line-height: 21px; +} + + +.p-Dialog-close:before { + content: '\f00d'; + font-family: FontAwesome; +} + .p-MenuBar { padding-left: 5px; diff --git a/example/index.js b/example/index.js index 1c140ed..6244d62 100644 --- a/example/index.js +++ b/example/index.js @@ -8,7 +8,6 @@ var jupyter_js_editor_1 = require('jupyter-js-editor'); var jupyter_js_filebrowser_1 = require('jupyter-js-filebrowser'); var jupyter_js_services_1 = require('jupyter-js-services'); var phosphor_splitpanel_1 = require('phosphor-splitpanel'); -var phosphor_widget_1 = require('phosphor-widget'); function main() { var baseUrl = 'http://localhost:8888'; var contents = new jupyter_js_services_1.Contents(baseUrl); @@ -22,8 +21,9 @@ function main() { } }); var panel = new phosphor_splitpanel_1.SplitPanel(); - panel.children.assign([fileBrowser, editor]); - phosphor_widget_1.Widget.attach(panel, document.body); + panel.addChild(fileBrowser); + panel.addChild(editor); + panel.attach(document.body); window.onresize = function () { return panel.update(); }; } main(); diff --git a/example/index.ts b/example/index.ts index 2020726..0b0f495 100644 --- a/example/index.ts +++ b/example/index.ts @@ -45,9 +45,10 @@ function main(): void { }); let panel = new SplitPanel(); - panel.children.assign([fileBrowser, editor]); + panel.addChild(fileBrowser); + panel.addChild(editor); - Widget.attach(panel, document.body); + panel.attach(document.body); window.onresize = () => panel.update(); } diff --git a/example/package.json b/example/package.json index a14a7a7..582f194 100644 --- a/example/package.json +++ b/example/package.json @@ -8,7 +8,7 @@ "jupyter-js-editor": "jupyter/jupyter-js-editor", "jupyter-js-filebrowser": "file:..", "phosphor-splitpanel": "^1.0.0-beta", - "phosphor-widget": "^1.0.0-beta.1", + "phosphor-widget": "^1.0.0-beta.2", "steal": "^0.12.7" }, "devDependencies": { diff --git a/lib/index.d.ts b/lib/index.d.ts index 54d8759..2ee6575 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -54,7 +54,7 @@ export declare class FileBrowserViewModel { /** * Upload a file object. */ - upload(file: File): Promise; + upload(file: File, overwrite?: boolean): Promise; /** * Refresh the model contents. */ diff --git a/lib/index.js b/lib/index.js index 8b25efa..2a12086 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,11 +8,11 @@ var __extends = (this && this.__extends) || function (d, b) { }; var moment = require('moment'); var phosphor_command_1 = require('phosphor-command'); +var phosphor_dialog_1 = require('phosphor-dialog'); var phosphor_domutil_1 = require('phosphor-domutil'); var phosphor_menus_1 = require('phosphor-menus'); var phosphor_signaling_1 = require('phosphor-signaling'); var phosphor_widget_1 = require('phosphor-widget'); -var dialog_1 = require('./dialog'); require('./index.css'); /** * The class name added to FileBrowser instances. @@ -192,20 +192,20 @@ var FileBrowserViewModel = (function () { /** * Upload a file object. */ - FileBrowserViewModel.prototype.upload = function (file) { + FileBrowserViewModel.prototype.upload = function (file, overwrite) { var _this = this; // Skip large files with a warning. if (file.size > this._max_upload_size_mb * 1024 * 1024) { - var msg = "Cannot upload file (>" + this._max_upload_size_mb + " MB)"; - msg += "'" + file.name + "'"; + var msg = "Cannot upload file (>" + this._max_upload_size_mb + " MB) "; + msg += "\"" + file.name + "\""; console.warn(msg); return Promise.reject(new Error(msg)); } // Check for existing file. for (var _i = 0, _a = this._items; _i < _a.length; _i++) { var model = _a[_i]; - if (model.name === file.name) { - return Promise.reject(new Error(file.name + " already exists")); + if (model.name === file.name && !overwrite) { + return Promise.reject(new Error("\"" + file.name + "\" already exists")); } } // Gather the file model parameters. @@ -307,20 +307,6 @@ var FileBrowser = (function (_super) { this._buttons = createButtons(buttons); var input = this._buttons[Button.Upload].lastChild; input.onchange = this._handleUploadEvent.bind(this); - this._buttons[Button.Refresh].onclick = function () { - // Show a dialog here - var test = document.createElement('textarea'); - test.value = 'Input some text'; - dialog_1.Dialog.showDialog('Test', _this.node, test).then(function (button) { - if (button) { - console.log(button.text); - } - else { - console.log('Escape!'); - } - console.log(test.value); - }); - }; // Create the "new" menu. var command = new phosphor_command_1.DelegateCommand(function (args) { _this._handleNewCommand(args); @@ -600,9 +586,32 @@ var FileBrowser = (function (_super) { * Handle a file upload event. */ FileBrowser.prototype._handleUploadEvent = function (event) { + var _this = this; for (var _i = 0, _a = event.target.files; _i < _a.length; _i++) { var file = _a[_i]; - this._model.upload(file); + this._model.upload(file).catch(function (error) { + if (error.message.indexOf('already exists') !== -1) { + var options = { + title: 'Overwrite file?', + host: _this.node, + body: error.message + ', overwrite?' + }; + phosphor_dialog_1.showDialog(options).then(function (button) { + if (button.text === 'OK') { + _this._model.upload(file, true); + } + }); + } + else { + var options = { + title: 'Upload Error', + host: _this.node, + body: error.message, + buttons: [phosphor_dialog_1.okButton] + }; + phosphor_dialog_1.showDialog(options); + } + }); } }; /** diff --git a/package.json b/package.json index ce25d62..94ace39 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,12 @@ "jupyter-js-services": "^0.2.2", "moment": "^2.10.6", "phosphor-command": "^0.5.0", + "phosphor-dialog": "blink1073/phosphor-dialog#f5afb2b", "phosphor-domutil": "^1.2.0", "phosphor-menus": "1.0.0-beta.1", "phosphor-messaging": "^1.0.5", "phosphor-nodewrapper": "^1.0.4", - "phosphor-widget": "1.0.0-beta.1" + "phosphor-widget": "^1.0.0-beta.2" }, "devDependencies": { "expect.js": "^0.3.1", diff --git a/src/dialog.css b/src/dialog.css deleted file mode 100644 index 2c14475..0000000 --- a/src/dialog.css +++ /dev/null @@ -1,51 +0,0 @@ -/*----------------------------------------------------------------------------- -| Copyright (c) Jupyter Development Team. -| Distributed under the terms of the Modified BSD License. -|----------------------------------------------------------------------------*/ -.jp-Dialog { - position: absolute; - z-index: 10000; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - margin: 0; - padding-left: 3px; -} - - -.jp-Dialog-content { - text-align: center; - vertical-align: middle; -} - - -.jp-Dialog-header { - padding: 5px; -} - - -.jp-Dialog-body { - text-align: center; - vertical-align: middle; - padding: 5px; -} - - -.jp-Dialog-footer { - list-style-type: none; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - margin: 0; - padding: 5px; -} - - -.jp-Dialog-button { - flex: 1 1 auto; - margin: 0; - margin-left: 5px; - padding: 5px; -} diff --git a/src/dialog.ts b/src/dialog.ts deleted file mode 100644 index de1148e..0000000 --- a/src/dialog.ts +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -'use strict'; - -import { - ICommand -} from 'phosphor-command'; - -import { - hitTest -} from 'phosphor-domutil'; - -import { - Message -} from 'phosphor-messaging'; - -import { - Widget -} from 'phosphor-widget'; - -import './index.css'; - -import './dialog.css'; - - -/** - * The class name added to Dialog instances. - */ -const DIALOG_CLASS = 'jp-Dialog'; - -/** - * The class name added to Dialog content node. - */ -const CONTENT_CLASS = 'jp-Dialog-content'; - -/** - * The class name added to Dialog header node. - */ -const HEADER_CLASS = 'jp-Dialog-header'; - -/** - * The class name added to Dialog body node. - */ -const BODY_CLASS = 'jp-Dialog-body'; - -/** - * The class name added to Dialog content node. - */ -const FOOTER_CLASS = 'jp-Dialog-footer'; - -/** - * The class name added to Dialog button nodes. - */ -const BUTTON_CLASS = 'jp-Dialog-button'; - -/** - * The class name added to Dialog OK buttons. - */ -const OK_BUTTON_CLASS = 'jp-Dialog-ok-button'; - -/** - * The class name added to Dialog Cancel buttons. - */ -const CANCEL_BUTTON_CLASS = 'jp-Dialog-cancel-button'; - - -/** - * A button applied to a dialog. - */ -export -interface IButtonItem { - /** - * The text for the button. - */ - text: string; - - /** - * The icon class for the button. - */ - icon?: string; - - /** - * The extra class name to associate with the button. - */ - className?: string; - - /** - * The command for the button. - */ - command?: ICommand; - - /** - * The args object for the button command. - */ - commandArgs?: any; -} - - -/** - * Define a default "OK" button. - */ -export -const okButton: IButtonItem = { - text: 'OK', - className: 'jp-Dialog-ok-button' -} - - -/** - * Define a default "Cancel" button. - */ -export -const cancelButton: IButtonItem = { - text: 'Cancel', - className: 'jp-Dialog-cancel-button' -} - - -/** - * An implementation of a modal dialog. - */ -export -class Dialog extends Widget { - /** - * Create the DOM node for dialog. - */ - static createNode(): HTMLElement { - let node = document.createElement('div'); - let content = document.createElement('div'); - let header = document.createElement('div'); - let body = document.createElement('div'); - let footer = document.createElement('ul'); - content.className = CONTENT_CLASS; - header.className = HEADER_CLASS; - body.className = BODY_CLASS; - footer.className = FOOTER_CLASS; - node.appendChild(content); - content.appendChild(header); - content.appendChild(body); - content.appendChild(footer); - return node; - } - - /** - * Create a dialog and show it. - */ - static showDialog(title: string, host: HTMLElement, body: HTMLElement, buttons?: IButtonItem[]): Promise{ - let dialog = new Dialog(title, host, body, buttons); - return dialog.show(); - } - - /** - * Construct a new dialog. - */ - constructor(title: string, host: HTMLElement, body: HTMLElement, buttons?: IButtonItem[]) { - super(); - this.addClass(DIALOG_CLASS); - this._title = title; - this._host = host; - this._body = body; - this._buttons = buttons || [okButton, cancelButton]; - } - - /** - * Show the dialog over the host node. - * - * @returns The button item that was selected or `null` if the dialog was Escaped. - */ - show(): Promise { - // Set up the geometry of the dialog. - let rect = this._host.getBoundingClientRect(); - this.node.style.left = rect.left + 'px'; - this.node.style.top = rect.top + 'px'; - this.node.style.width = rect.width + 'px'; - this.node.style.height = rect.height + 'px'; - - // Add the dialog contents and attach to the document. - let header = this.node.getElementsByClassName(HEADER_CLASS)[0]; - header.textContent = this._title; - let body = this.node.getElementsByClassName(BODY_CLASS)[0]; - body.appendChild(this._body); - let footer = this.node.getElementsByClassName(FOOTER_CLASS)[0]; - this._buttonNodes = []; - for (let item of this._buttons) { - let button = createButton(item); - button.tabIndex = this._buttonNodes.length; - this._buttonNodes.push(button); - footer.appendChild(button); - } - Widget.attach(this, document.body); - - // Return a promise to be resolved when the dialog is closed. - return new Promise((resolve, reject) => { - this._resolver = resolve; - }); - } - - /** - * Handle the DOM events for the dialog. - * - * @param event - The DOM event sent to the panel. - * - * #### Notes - * This method implements the DOM `EventListener` interface and is - * called in response to events on the panel's DOM node. It should - * not be called directly by user code. - */ - handleEvent(event: Event): void { - switch (event.type) { - case 'click': - this._evtClick(event as MouseEvent); - break; - case 'keydown': - this._evtKeyDown(event as KeyboardEvent); - break; - } - } - - /** - * A message handler invoked on an `'after-attach'` message. - */ - protected onAfterAttach(msg: Message): void { - super.onAfterAttach(msg); - this.node.addEventListener('click', this); - document.addEventListener('keydown', this, true); - } - - /** - * A message handler invoked on a `'before-detach'` message. - */ - protected onBeforeDetach(msg: Message): void { - super.onBeforeDetach(msg); - this.node.removeEventListener('click', this); - document.removeEventListener('keydown', this, true); - } - - /** - * Handle the `'keydown'` event for the dialog. - */ - private _evtKeyDown(event: KeyboardEvent): void { - var index = this._buttonNodes.indexOf(event.target as HTMLElement); - if (index === -1 && event.target !== document.body) { - return; - } - event.stopPropagation(); - switch (event.keyCode) { - case 9: // Tab - event.preventDefault(); - if (index === -1) { - this._buttonNodes[0].focus(); - } else { - index = (index + 1) % this._buttonNodes.length; - this._buttonNodes[index].focus(); - } - break; - case 13: // Enter - event.preventDefault(); - if (index !== -1) this._handleSelect(this._buttons[index]); - break; - case 27: // Escape - Widget.detach(this); - event.preventDefault(); - this._resolver(void 0); - break; - } - } - - /** - * Handle the `'click'` event for the dialog. - */ - private _evtClick(event: MouseEvent): void { - // Do nothing if it's not a left mouse press. - if (event.button !== 0) { - return; - } - - // Check for a click on a button. - let index = hitTestNodes(this._buttonNodes, event.clientX, event.clientY); - if (index !== -1) { - // Stop the event propagation. - event.preventDefault(); - event.stopPropagation(); - this._handleSelect(this._buttons[index]); - } - } - - /** - * Handle a selection of one of the button nodes. - */ - private _handleSelect(item: IButtonItem): void { - Widget.detach(this); - if (item.command && item.command.isEnabled) { - item.command.execute(item.commandArgs); - } - this._resolver(item); - } - - private _title = ''; - private _host: HTMLElement = null; - private _body: HTMLElement = null; - private _buttons: IButtonItem[] = []; - private _resolver: (value: IButtonItem) => void = null; - private _buttonNodes: HTMLElement[] = []; -} - - -/** - * Create a node for a button item. - */ -function createButton(item: IButtonItem): HTMLElement { - let el = document.createElement('li'); - el.textContent = item.text; - el.className = BUTTON_CLASS; - if (item.icon) el.classList.add(item.icon); - if (item.className) el.classList.add(item.className); - return el; -} - - -/** - * Get the index of the node at a client position, or `-1`. - */ -function hitTestNodes(nodes: HTMLElement[], x: number, y: number): number { - for (let i = 0, n = nodes.length; i < n; ++i) { - if (hitTest(nodes[i], x, y)) return i; - } - return -1; -} diff --git a/src/index.ts b/src/index.ts index 9772c9b..ca424dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,10 @@ import { DelegateCommand, ICommand } from 'phosphor-command'; +import { + okButton, showDialog +} from 'phosphor-dialog'; + import { hitTest } from 'phosphor-domutil'; @@ -37,10 +41,6 @@ import { Widget } from 'phosphor-widget'; -import { - Dialog -} from './dialog'; - import './index.css'; @@ -232,20 +232,20 @@ class FileBrowserViewModel { /** * Upload a file object. */ - upload(file: File): Promise { + upload(file: File, overwrite?: boolean): Promise { // Skip large files with a warning. if (file.size > this._max_upload_size_mb * 1024 * 1024) { - let msg = `Cannot upload file (>${this._max_upload_size_mb} MB)`; - msg += `'${file.name}'` + let msg = `Cannot upload file (>${this._max_upload_size_mb} MB) `; + msg += `"${file.name}"` console.warn(msg); return Promise.reject(new Error(msg)); } // Check for existing file. for (let model of this._items) { - if (model.name === file.name) { - return Promise.reject(new Error(`${file.name} already exists`)); + if (model.name === file.name && !overwrite) { + return Promise.reject(new Error(`"${file.name}" already exists`)); } } @@ -386,20 +386,6 @@ class FileBrowser extends Widget { let input = this._buttons[Button.Upload].lastChild; (input as HTMLElement).onchange = this._handleUploadEvent.bind(this); - this._buttons[Button.Refresh].onclick = () => { - // Show a dialog here - let test = document.createElement('textarea'); - test.value = 'Input some text'; - Dialog.showDialog('Test', this.node, test).then(button => { - if (button) { - console.log(button.text); - } else { - console.log('Escape!'); - } - console.log(test.value); - }); - } - // Create the "new" menu. let command = new DelegateCommand((args: string) => { this._handleNewCommand(args); @@ -672,8 +658,29 @@ class FileBrowser extends Widget { * Handle a file upload event. */ private _handleUploadEvent(event: Event) { - for (let file of (event.target as any).files) { - this._model.upload(file); + for (var file of (event.target as any).files) { + this._model.upload(file).catch(error => { + if (error.message.indexOf('already exists') !== -1) { + let options = { + title: 'Overwrite file?', + host: this.node, + body: error.message + ', overwrite?' + } + showDialog(options).then(button => { + if (button.text === 'OK') { + this._model.upload(file, true); + } + }); + } else { + let options = { + title: 'Upload Error', + host: this.node, + body: error.message, + buttons: [okButton] + } + showDialog(options); + } + }); } }