Browse Source

Merge pull request #3062 from ian-r-rose/loading_spinner

Loading spinner
Steven Silvester 7 years ago
parent
commit
c9182a0a17

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

@@ -14,6 +14,7 @@ export * from './iframe';
 export * from './instancetracker';
 export * from './mainmenu';
 export * from './sanitizer';
+export * from './spinner';
 export * from './splash';
 export * from './styling';
 export * from './thememanager';

+ 27 - 0
packages/apputils/src/spinner.ts

@@ -0,0 +1,27 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+
+import {
+  Widget 
+} from '@phosphor/widgets';
+
+
+/**
+ * The spinner class.
+ */
+export
+class Spinner extends Widget {
+  /**
+   * Construct a spinner widget.
+   */
+  constructor () {
+    super();
+    this.addClass('jp-Spinner');
+    let content = document.createElement('div');
+    content.className = 'jp-SpinnerContent';
+    this.node.appendChild(content);
+  }
+}

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

@@ -9,5 +9,6 @@
 @import './dialog.css';
 @import './hoverbox.css';
 @import './iframe.css';
+@import './spinner.css';
 @import './styling.css';
 @import './toolbar.css';

+ 76 - 0
packages/apputils/style/spinner.css

@@ -0,0 +1,76 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) 2017, Jupyter Development Team.
+|
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+
+.jp-Spinner {
+  position: absolute;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 10;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  background: var(--jp-layout-color0);
+}
+
+
+.jp-SpinnerContent {
+  font-size: 10px;
+  margin: 50px auto;
+  text-indent: -9999em;
+  width: 3em;
+  height: 3em;
+  border-radius: 50%;
+  background: var(--jp-brand-color3);
+  background: linear-gradient(to right, #F37626 10%, rgba(255, 255, 255, 0) 42%);
+  position: relative;
+  animation: load3 1.0s infinite linear, fadeIn 1.0s;
+}
+
+.jp-SpinnerContent:before {
+  width: 50%;
+  height: 50%;
+  background: #F37626;
+  border-radius: 100% 0 0 0;
+  position: absolute;
+  top: 0;
+  left: 0;
+  content: '';
+}
+
+.jp-SpinnerContent:after {
+  background: var(--jp-layout-color0);
+  width: 75%;
+  height: 75%;
+  border-radius: 50%;
+  content: '';
+  margin: auto;
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+}
+
+@keyframes fadeIn {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1.;
+  }
+}
+
+@keyframes load3 {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 6 - 1
packages/docmanager-extension/src/index.ts

@@ -6,7 +6,7 @@ import {
 } from '@jupyterlab/application';
 
 import {
-  showDialog, showErrorMessage, Dialog, ICommandPalette, IMainMenu
+  showDialog, showErrorMessage, Spinner, Dialog, ICommandPalette, IMainMenu
 } from '@jupyterlab/apputils';
 
 import {
@@ -90,6 +90,11 @@ const plugin: JupyterLabPlugin<IDocumentManager> = {
         };
         if (!widget.isAttached) {
           app.shell.addToMainArea(widget);
+
+          // Add a loading spinner, and remove it when the widget is ready.
+          let spinner = new Spinner();
+          widget.node.appendChild(spinner.node);
+          widget.ready.then(() => { widget.node.removeChild(spinner.node); });
         }
         app.shell.activateById(widget.id);
 

+ 7 - 7
packages/docmanager/src/manager.ts

@@ -145,7 +145,7 @@ class DocumentManager implements IDisposable {
    *  Uses the same widget factory and context as the source, or returns
    *  `undefined` if the source widget is not managed by this manager.
    */
-  cloneWidget(widget: Widget): Widget | undefined {
+  cloneWidget(widget: Widget): DocumentRegistry.IReadyWidget | undefined {
     return this._widgetManager.cloneWidget(widget);
   }
 
@@ -253,7 +253,7 @@ class DocumentManager implements IDisposable {
    * This can be used to use an existing widget instead of opening
    * a new widget.
    */
-  findWidget(path: string, widgetName='default'): Widget | undefined {
+  findWidget(path: string, widgetName='default'): DocumentRegistry.IReadyWidget | undefined {
     if (widgetName === 'default') {
       let factory = this.registry.defaultWidgetFactory(path);
       if (!factory) {
@@ -295,7 +295,7 @@ class DocumentManager implements IDisposable {
    * This function will return `undefined` if a valid widget factory
    * cannot be found.
    */
-  open(path: string, widgetName='default', kernel?: Partial<Kernel.IModel>): Widget | undefined {
+  open(path: string, widgetName='default', kernel?: Partial<Kernel.IModel>): DocumentRegistry.IReadyWidget | undefined {
     return this._createOrOpenDocument('open', path, widgetName, kernel);
   }
 
@@ -315,7 +315,7 @@ class DocumentManager implements IDisposable {
    * This function will return `undefined` if a valid widget factory
    * cannot be found.
    */
-  openOrReveal(path: string, widgetName='default', kernel?: Partial<Kernel.IModel>): Widget | undefined {
+  openOrReveal(path: string, widgetName='default', kernel?: Partial<Kernel.IModel>): DocumentRegistry.IReadyWidget | undefined {
     let widget = this.findWidget(path, widgetName);
     if (widget) {
       this._opener.open(widget);
@@ -378,7 +378,7 @@ class DocumentManager implements IDisposable {
    * Create a context from a path and a model factory.
    */
   private _createContext(path: string, factory: DocumentRegistry.ModelFactory, kernelPreference: IClientSession.IKernelPreference): Private.IContext {
-    let adopter = (widget: Widget) => {
+    let adopter = (widget: DocumentRegistry.IReadyWidget) => {
       this._widgetManager.adoptWidget(context, widget);
       this._opener.open(widget);
     };
@@ -434,7 +434,7 @@ class DocumentManager implements IDisposable {
    * The two cases differ in how the document context is handled, but the creation
    * of the widget and launching of the kernel are identical.
    */
-  private _createOrOpenDocument(which: 'open'|'create', path: string, widgetName='default', kernel?: Partial<Kernel.IModel>): Widget | undefined {
+  private _createOrOpenDocument(which: 'open'|'create', path: string, widgetName='default', kernel?: Partial<Kernel.IModel>): DocumentRegistry.IReadyWidget | undefined {
     let widgetFactory = this._widgetFactoryFor(path, widgetName);
     if (!widgetFactory) {
       return undefined;
@@ -528,7 +528,7 @@ namespace DocumentManager {
     /**
      * Open the given widget.
      */
-    open(widget: Widget): void;
+    open(widget: DocumentRegistry.IReadyWidget): void;
   }
 }
 

+ 4 - 4
packages/docmanager/src/widgetmanager.ts

@@ -125,7 +125,7 @@ class DocumentWidgetManager implements IDisposable {
    *
    * @param widget - The widget to adopt.
    */
-  adoptWidget(context: DocumentRegistry.Context, widget: Widget): void {
+  adoptWidget(context: DocumentRegistry.Context, widget: DocumentRegistry.IReadyWidget): void {
     let widgets = Private.widgetsProperty.get(context);
     widgets.push(widget);
     MessageLoop.installMessageHook(widget, this);
@@ -146,7 +146,7 @@ class DocumentWidgetManager implements IDisposable {
    * This can be used to use an existing widget instead of opening
    * a new widget.
    */
-  findWidget(context: DocumentRegistry.Context, widgetName: string): Widget | undefined {
+  findWidget(context: DocumentRegistry.Context, widgetName: string): DocumentRegistry.IReadyWidget | undefined {
     let widgets = Private.widgetsProperty.get(context);
     if (!widgets) {
       return undefined;
@@ -182,7 +182,7 @@ class DocumentWidgetManager implements IDisposable {
    *  Uses the same widget factory and context as the source, or throws
    *  if the source widget is not managed by this manager.
    */
-  cloneWidget(widget: Widget): Widget | undefined {
+  cloneWidget(widget: Widget): DocumentRegistry.IReadyWidget | undefined {
     let context = Private.contextProperty.get(widget);
     if (!context) {
       return undefined;
@@ -448,7 +448,7 @@ namespace Private {
    * A private attached property for the widgets associated with a context.
    */
   export
-  const widgetsProperty = new AttachedProperty<DocumentRegistry.Context, Widget[]>({
+  const widgetsProperty = new AttachedProperty<DocumentRegistry.Context, DocumentRegistry.IReadyWidget[]>({
     name: 'widgets',
     create: () => []
   });

+ 4 - 4
test/src/docmanager/widgetmanager.spec.ts

@@ -160,20 +160,20 @@ describe('@jupyterlab/docmanager', () => {
     describe('#adoptWidget()', () => {
 
       it('should install a message hook', () => {
-        let widget = new Widget();
+        let widget = new DocWidget();
         manager.adoptWidget(context, widget);
         MessageLoop.sendMessage(widget, new Message('foo'));
         expect(manager.methods).to.contain('messageHook');
       });
 
       it('should add the document class', () => {
-        let widget = new Widget();
+        let widget = new DocWidget();
         manager.adoptWidget(context, widget);
         expect(widget.hasClass('jp-Document')).to.be(true);
       });
 
       it('should be retrievable', () => {
-        let widget = new Widget();
+        let widget = new DocWidget();
         manager.adoptWidget(context, widget);
         expect(manager.contextForWidget(widget)).to.be(context);
       });
@@ -240,7 +240,7 @@ describe('@jupyterlab/docmanager', () => {
     describe('#messageHook()', () => {
 
       it('should be called for a message to a tracked widget', () => {
-        let widget = new Widget();
+        let widget = new DocWidget();
         manager.adoptWidget(context, widget);
         MessageLoop.sendMessage(widget, new Message('foo'));
         expect(manager.methods).to.contain('messageHook');